NetBox package artifacts (a wheel and a source distribution) can be built and verified locally. During the v4.6.x preview period, published artifacts are intended for maintainer validation only, and installing NetBox via pip is not a supported installation path; experimental support for installing from production PyPI is planned for NetBox v4.7.0. This page is intended for maintainers and contributors working on the packaging itself; routine development does not require building a package.
The artifacts are always built by CI from a clean checkout (see
.github/workflows/release.yml). A local build is useful for testing packaging changes before
they are merged.
Install the minimum local build tooling (all three are also included in the dev
optional dependency group):
python -m pip install --upgrade build packaging twine
Build both the source distribution (sdist) and the wheel into dist/:
python -m build
To build only the wheel (faster, and the form most useful for a quick local install test):
python -m build --wheel
The package version is derived from netbox/release.yaml by the build hook in
scripts/packaging/hatch_metadata.py; you do not set it in pyproject.toml. The wheel's runtime dependency
metadata (Requires-Dist) is generated from the pinned requirements.txt by the same hook,
so the wheel ships the exact dependency versions NetBox is tested against.
Always build release artifacts from a clean checkout. The build configuration keeps
deployment-local files out of the artifacts: the Hatch excludes drop every
configuration*.py and ldap_config*.py except the two tracked configuration templates
(configuration_example.py and configuration_testing.py, which are force-included
explicitly), and CI verifies the contents of both the wheel and the sdist before anything
is published.
These checks are defense in depth, not a license to build from a dirty tree: other untracked
files under netbox/ can still be picked up by a local build. CI builds from a clean
checkout, so the published artifacts are unaffected. For a comparable local build, use a
fresh git clone or a separate clean worktree rather than your day-to-day development tree.
Check the built artifacts for valid metadata and rendering:
twine check dist/*
Confirm the wheel's version, dependency metadata, and extras match netbox/release.yaml, the
pinned requirements.txt, and the declared optional-dependency groups:
python scripts/verify_wheel_metadata.py dist/*.whl
Confirm the artifacts ship only the two tracked configuration templates, and that the wheel
carries the runtime-critical bundled data (_data/release.yaml, templates, translations,
static assets, and the deployment examples; the same content checks CI runs before
publishing):
python scripts/verify_wheel_contents.py dist/*.whl
python scripts/verify_sdist_contents.py dist/*.tar.gz
Confirm requirements.txt is still consistent with the maintainer policy in
base_requirements.txt (the same drift guard CI runs before publishing):
python scripts/verify_dependencies.py
Install the wheel into a throwaway virtual environment and run the system checks to confirm the package is importable and runnable:
python -m venv /tmp/netbox-build-test
/tmp/netbox-build-test/bin/python -m pip install --upgrade pip
/tmp/netbox-build-test/bin/python -m pip install dist/*.whl
PYTHONPATH=$PWD/scripts \
NETBOX_CONFIGURATION=smoketest_configuration \
NETBOX_SMOKETEST_BASE=/tmp/netbox-build-test-root \
/tmp/netbox-build-test/bin/netbox check
Without configuration, a wheel-installed NetBox looks for $NETBOX_ROOT/conf/configuration.py
(default /opt/netbox/conf/configuration.py), which normally does not exist on a development
workstation. The environment variables above point netbox check at the same minimal
configuration module the release workflow's smoke-test job uses
(scripts/smoketest_configuration.py); run the command from the repository root so
PYTHONPATH can find it. NETBOX_SMOKETEST_BASE sets the writable scratch directory under
which the module creates its media, reports, scripts, and static roots. Any other importable
configuration module works the same way via NETBOX_CONFIGURATION (and PYTHONPATH, if the
configuration lives outside the package). To exercise the full post-install task sequence from
the wheel, run netbox upgrade --no-input against a throwaway database; this is what the
release workflow's smoke-test job does.
This section is a developer-facing overview of how the package is assembled and how a pip-installed NetBox behaves at runtime. User-facing installation documentation for the pip install path will be added alongside experimental PyPI support (planned for NetBox v4.7.0); this page does not cover end-user installation steps.
scripts/packaging/hatch_metadata.py is a Hatchling metadata hook (wired in via
[tool.hatch.metadata.hooks.custom]). It computes the package version from
netbox/release.yaml and the runtime dependencies from the pinned requirements.txt, so the
published wheel's Requires-Dist carries the exact versions NetBox is tested against. Both
fields are declared dynamic in pyproject.toml; the optional-dependency extras stay static.
python -m build produces both an sdist and a wheel (the wheel is built from the sdist). The
release workflow's verify-sdist job rebuilds a wheel from the published sdist and runs
scripts/verify_wheel_metadata.py and scripts/verify_wheel_contents.py against it, so a
missing build input (for example the metadata hook or base_requirements.txt) cannot regress
unnoticed.
Source assets that are not Python modules are force-included with a netbox/netbox/_data/
target path by [tool.hatch.build.targets.wheel.force-include]; because the wheel's
sources = ["netbox"] setting strips one leading netbox/, they install under netbox/_data/:
templates,
translations, the compiled project-static bundles, release.yaml, and the bundled
deployment examples under _data/examples/ (gunicorn.py, the systemd units, nginx.conf,
apache.conf, netbox.env).
The documentation is deliberately not part of the wheel: neither the docs/ sources nor
mkdocs.yml ship (the sdist carries both), so netbox upgrade --build-docs skips with a
notice and DOCS_ROOT resolves to None, disabling the embedded docs.
The service and web server examples are the canonical contrib/ files, bundled unmodified;
netbox setup adapts the systemd units for a pip install at render time (an optional
EnvironmentFile, no --pythonpath, console-script rqworker), and the path renderer
already collapses the source-layout static path for nginx/apache, so both install methods
share a single source. A unit test pins the transform anchors against contrib/, so an
upstream contrib change fails CI rather than netbox setup.
At runtime settings.py detects the bundled _data directory
and sets BASE_DIR to it (install mode wheel); a source checkout has no _data, so
BASE_DIR stays the project root (install mode checkout).
A pip-installed NetBox keeps mutable instance state out of the immutable, disposable virtual
environment. settings.py resolves NETBOX_ROOT (default /opt/netbox, overridable via the
environment) as the instance root and defaults the writable paths (MEDIA_ROOT,
REPORTS_ROOT, SCRIPTS_ROOT, STATIC_ROOT) beneath it. In a checkout NETBOX_ROOT equals
BASE_DIR, so archive and Git installs are unaffected.
Configuration loading is handled by load_configuration() in netbox/netbox/settings_utils.py.
An explicit NETBOX_CONFIGURATION module always wins; otherwise, in wheel mode it prefers
NETBOX_ROOT/conf/configuration.py, loading it by file path, and falls back to a legacy
NETBOX_ROOT/netbox/netbox/configuration.py with a migration warning. The configuration
directory is added to sys.path only while the configuration file executes, so sibling
imports can resolve; NETBOX_ROOT itself is never added, which avoids a stale source tree
shadowing the installed package. A checkout keeps importing netbox.configuration. For LDAP
deployments, settings.py exposes the active configuration file's directory as the
CONFIGURATION_DIR setting, and load_ldap_config() loads ldap_config.py from that same
directory: the active ldap_config.py is always the one beside the active configuration.py,
regardless of install method.
One compatibility exception: in checkout mode only, when no sibling file exists, the
historical netbox/netbox/ldap_config.py module is imported with a RuntimeWarning, so
existing source installs that use a custom NETBOX_CONFIGURATION keep working.
pyproject.toml registers a single entry point, netbox (netbox.cli:main). The wrapper
resolves a few commands itself before importing Django, so they work without a
configuration present:
netbox version / netbox --version print the installed package version.netbox setup copies the bundled _data/examples/* into the instance root and
--systemd-dir, and scaffolds the conf/ package from the bundled
configuration_example.py. It never overwrites an existing configuration.py,
conf/__init__.py, local_requirements.txt, or netbox.env, even with --force.netbox secret-key prints a new 50-character SECRET_KEY value.These names are reserved by the wrapper. Every other command falls through to the
Django management commands (netbox upgrade, netbox check, and so on), which
require a valid configuration.