Pick the path that matches how you want to run NETMapper. Each one below is self-contained: you do not need to read the others. All of them end at the same place, a running stack and a first admin user.
Requirements
- A Linux host with Docker Engine 24 or newer and the Compose v2 plugin
(
docker compose, not the legacydocker-composebinary). Docker is the only requirement for the container paths. - At least 2 GB of free RAM. The three containers (PostgreSQL, backend, frontend) are modest on their own but add up.
- Outbound network access so the host can pull the release images.
- SSH reachability from the host to the switches you want to scan, with switch credentials. NETMapper logs in to switches over SSH; password authentication is the supported method in the current release.
- A safe place to keep
NETMAPPER_ENCRYPTION_KEYout of band, such as a password manager or a secret store. Losing that key makes every stored switch password and notification secret permanently unrecoverable.
NETMapper holds your switch credentials. Run it on the management network, behind a firewall, ideally behind a zero-trust access proxy (Cloudflare Access, Tailscale, a VPN). Do not expose it to the internet directly.
One command: the installer
The fastest path. Docker with the Compose plugin is the only prerequisite.
curl -fsSL https://netmapper.nx1xlab.dev/install.sh | bash
What it does, in order:
- Checks that
curland Docker with the Compose plugin are present and that Docker is running and reachable by your user. - Resolves the latest release and downloads two small files from it:
release.env(version and image digests) andSHA256SUMS. - Creates
./netmapper(or$NETMAPPER_DIRif you set it), downloadsdocker-compose.ymlanddocker-compose.https.ymlfor that release, and checks each file againstSHA256SUMS. A mismatch deletes the file and aborts before anything starts. - Writes
.env: the release version, the two image digests fromrelease.env, and freshly generated secrets (POSTGRES_PASSWORD,NETMAPPER_JWT_SECRET,NETMAPPER_ENCRYPTION_KEY). It also setsNETMAPPER_COOKIE_SECURE=false, because the default is plain HTTP on port 80 and browsers refuse Secure cookies there. - Starts the stack and waits until
GET /readyanswers. - Creates the first admin user.
What it asks you: a username and a password for the first admin. That is the only interactive step. If there is no terminal attached (CI, a remote command), it skips the prompt and prints the command to run yourself.
Everything it downloads is public, and it never sends anything anywhere.
Re-running it in the same directory keeps the existing .env and data.
What the script does for your safety
https://netmapper.nx1xlab.dev/install.shredirects to thestabletag of the Core repository, not tomain. Work in progress on the installer never reaches your host.- The whole installer sits inside one function that is called on the last line. A download cut short defines the function and runs nothing, instead of executing half an install.
- Compose files and
.env.examplecome fromraw.githubusercontent.com;SHA256SUMScomes from the release assets ongithub.com. A tampered or corrupted answer on one of the two paths is caught. This does not protect you against someone who controls the repository itself; for that, verify the signature below. - Generated secrets are written without ever appearing on a command line, so
they do not show up in the process list of a shared host.
.envis created with mode 600 underumask 077.
Verify the installer before you run it
Every release publishes install.sh, its SHA-256 and a cosign signature. If
you would rather not pipe a script into bash:
TAG=v0.2.0 # the release you want
BASE=https://github.com/NX1X/NETMapper/releases/download/$TAG
curl -fsSLO $BASE/install.sh
curl -fsSL $BASE/install.sh.sha256 | sha256sum -c -
cosign verify-blob install.sh \
--bundle <(curl -fsSL $BASE/install.sh.cosign.bundle) \
--certificate-identity-regexp '^https://github.com/NX1X/NETMapper/' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
bash install.sh
The signature is keyless, through GitHub OIDC, so it proves the file was built
by the release workflow in this repository. SHA256SUMS is signed the same
way, as SHA256SUMS.cosign.bundle.
To install a specific release instead of the latest:
NETMAPPER_VERSION=0.1.0 bash -c "$(curl -fsSL https://netmapper.nx1xlab.dev/install.sh)"
When it finishes, open http://<host>/ and log in.
Useful commands afterwards, run in the install directory:
docker compose logs -f backend # follow the backend log
docker compose down # stop, data is kept
docker compose up -d # start again
Docker Compose by hand
Prefer to read every file before it runs? This is the same stack, assembled step by step. No source checkout is needed: the compose file runs the published release images, pinned by version and by digest.
1. Get the two files
mkdir netmapper && cd netmapper
curl -fsSLO https://raw.githubusercontent.com/NX1X/NETMapper/main/docker-compose.yml
curl -fsSL https://raw.githubusercontent.com/NX1X/NETMapper/main/.env.example -o .env
chmod 600 .env
2. Fill in the six required values
docker compose refuses to start while any of these is empty.
| Variable | Where it comes from |
|---|---|
NETMAPPER_VERSION |
The release notes of the release you want to run |
NETMAPPER_BACKEND_DIGEST |
The same release notes, sha256:... |
NETMAPPER_FRONTEND_DIGEST |
The same release notes, sha256:... |
POSTGRES_PASSWORD |
Generate it |
NETMAPPER_JWT_SECRET |
Generate it |
NETMAPPER_ENCRYPTION_KEY |
Generate it, then back it up out of band |
Open the latest release
and copy its three .env lines verbatim:
NETMAPPER_VERSION=0.1.0
NETMAPPER_BACKEND_DIGEST=sha256:...
NETMAPPER_FRONTEND_DIGEST=sha256:...
The digest is what pins the image. A tag can be repointed upstream, a digest cannot. Upgrading means replacing these three lines.
Generate the three secrets:
# POSTGRES_PASSWORD
python3 -c "import secrets; print(secrets.token_urlsafe(24))"
# NETMAPPER_JWT_SECRET (HS256 signing key; the production startup gate
# refuses to boot on anything shorter than 32 bytes)
python3 -c "import secrets; print(secrets.token_urlsafe(48))"
# NETMAPPER_ENCRYPTION_KEY (urlsafe-base64 Fernet key)
python3 -c "import base64, os; print(base64.urlsafe_b64encode(os.urandom(32)).decode())"
Back up NETMAPPER_ENCRYPTION_KEY somewhere independent of the database
dump. Every switch SSH password, every notification-channel secret and every
AI provider key is encrypted with it at rest. There is no key escrow and no
recovery code: lose it and those secrets are gone for good. Never commit it
to git.
One more value matters on a plain-HTTP install: set
NETMAPPER_COOKIE_SECURE=false when users reach the app over http:// with
no TLS terminator in front, otherwise the browser drops the Secure login
cookie and login fails. Leave it true when there is HTTPS in front.
3. Start the stack
docker compose up -d
On first boot the backend entrypoint runs alembic upgrade heads against the
database before starting the API, which creates the schema on a clean volume.
Later boots replay only new migrations. The Compose healthcheck calls
GET /ready, so the backend is marked healthy only once the database
round-trips.
4. Verify and create the admin user
curl -fsS http://localhost/ready
# expected: {"status":"ok","database":"ok"}
docker compose exec backend python -m netmapper.create_admin
If the backend is in a crash loop, read docker compose logs --tail=50 backend. The usual causes are a missing or malformed
NETMAPPER_ENCRYPTION_KEY and a database that was not reachable when the
migration ran.
The app is at http://<host>/ (or http://<host>:<NETMAPPER_HTTP_PORT>/).
The API’s own interactive reference is at /docs (Swagger UI) and /redoc
on the NETMapper host itself.
Quickstart zip
Every release also ships netmapper-quickstart-<version>.zip on the
releases page. It holds the
compose files, a .env template with the version and image digests already
filled in, and the same installer script. No source code, no build.
unzip netmapper-quickstart-<version>.zip
cd netmapper-quickstart-<version>
bash install.sh
Or do the same thing by hand from inside the unzipped folder:
cp .env.example .env # then set the three secrets, see the comments
docker compose up -d
docker compose exec backend python -m netmapper.create_admin
Back up .env: it holds NETMAPPER_ENCRYPTION_KEY.
HTTPS for a host reachable from outside
Read this first. NETMapper stores your switch credentials. The right answer for almost every deployment is a firewall plus a zero-trust access proxy (Cloudflare Access, Tailscale, a VPN) in front of plain port 80 on the management network. Use the HTTPS overlay only when the host genuinely has to be reachable from outside, and treat it as the floor, not the ceiling.
docker-compose.https.yml adds an nginx container that terminates TLS on 443
with automatic Let’s Encrypt certificates, and removes the frontend’s own
host port so nginx owns 80 and 443.
Prerequisites: a DNS name pointing at the host, and ports 80 and 443 reachable from the internet for the Let’s Encrypt HTTP-01 challenge.
echo "DOMAIN_NAME=netmapper.example.com" >> .env
# Obtain the certificate once, while port 80 is free
docker compose -f docker-compose.yml -f docker-compose.https.yml run --rm certbot
# Then start the full stack with TLS
docker compose -f docker-compose.yml -f docker-compose.https.yml up -d
Keep NETMAPPER_COOKIE_SECURE=true here: users now reach the app over HTTPS.
Renewal is not automatic on its own. Run the certbot service from a cron job or a systemd timer:
docker compose -f docker-compose.yml -f docker-compose.https.yml run --rm certbot renew
Bringing your own certificate, DNS details and renewal are covered in docs/operations/https.md in the repository.
Kubernetes with the Helm chart (beta)
The chart at helm/netmapper/ in the repository is beta: it installs, but
it is single-replica and not yet exercised in CI, and values may change before
1.0. Report problems as GitHub issues.
Prerequisites: Kubernetes 1.27 or newer, Helm 3.12 or newer, a StorageClass
that supports ReadWriteOnce for the PostgreSQL volume, and cluster access to
pull the images.
Four values have no safe default. Never deploy without them:
postgres.password, backend.encryptionKey, backend.jwtSecret, and
ingress.host if the ingress is enabled.
# my-values.yaml
postgres:
password: "a-strong-password"
backend:
encryptionKey: "your-fernet-key-here"
jwtSecret: "your-jwt-secret-here"
env: "production"
ingress:
enabled: true
host: "netmapper.example.com"
tls:
enabled: true
secretName: "netmapper-tls"
helm upgrade --install netmapper helm/netmapper \
--namespace netmapper --create-namespace \
--values my-values.yaml
kubectl rollout status deployment/netmapper-backend -n netmapper
kubectl rollout status deployment/netmapper-frontend -n netmapper
Create the first admin once the backend pod is running:
kubectl exec -it -n netmapper deployment/netmapper-backend -- \
python -m netmapper.create_admin --username admin
Migrations run automatically before the backend process starts, so the pod does not become Ready until they finish. Keep the backend at a single replica: each replica runs its own scheduler and would fire scheduled scans in parallel.
Bare metal
For hosts that cannot run Docker, or where you would rather manage the process with systemd. Supported distributions: Ubuntu 22.04 LTS or later, Debian 12 or later, and Rocky Linux 9 / RHEL 9 or later (AlmaLinux 9 and other compatible rebuilds included). Other Linux distributions work but are untested, and macOS is not a supported production target.
You also need Python 3.13, Node 24.x for the frontend build, a PostgreSQL 16 server, and a reverse proxy such as nginx. NETMapper does not terminate TLS itself on this path.
git clone https://github.com/NX1X/NETMapper
cd NETMapper
sudo sh scripts/install-baremetal.sh
The script is idempotent: re-running it skips work that is already done. It
creates the netmapper system user, installs the backend virtualenv, builds
the frontend static files, installs the systemd unit, and creates the config
directory at /etc/netmapper/. It deliberately does not start the service,
because the secrets are not in place yet.
sudo cp /etc/netmapper/.env.example /etc/netmapper/.env
sudo editor /etc/netmapper/.env
At minimum set DATABASE_URL, NETMAPPER_ENV=production,
NETMAPPER_JWT_SECRET and NETMAPPER_ENCRYPTION_KEY. Create the database
role and database, then start the service:
sudo -u postgres psql <<'SQL'
CREATE USER netmapper WITH PASSWORD 'a-strong-password';
CREATE DATABASE netmapper OWNER netmapper;
SQL
sudo systemctl enable --now netmapper
sudo systemctl status netmapper
sudo journalctl -u netmapper -n 50 -f
Then put nginx in front of it and create the first admin user. The full walkthrough, including a working nginx server block, is in docs/install/baremetal.md.
Build from source (contributors)
docker-compose.dev.yml builds the images from the checkout and bind-mounts
the backend source, so edits reload. Nothing in it is meant for production.
git clone https://github.com/NX1X/NETMapper
cd NETMapper
cp .env.example .env # set POSTGRES_PASSWORD, NETMAPPER_JWT_SECRET,
# NETMAPPER_ENCRYPTION_KEY
docker compose -f docker-compose.dev.yml up -d --build
Add NETMAPPER_ENV=development (it relaxes the production secret gate) and
NETMAPPER_COOKIE_SECURE=false (the dev frontend runs on plain
http://localhost:5173) to .env.
The frontend is on http://localhost:5173, the API on
http://localhost:8000, and PostgreSQL on localhost:5432.
Running the pieces natively instead, without Docker:
# Backend (Python 3.13)
cd backend
python -m venv .venv
. .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt -r requirements-dev.txt
uvicorn netmapper.main:app --reload # http://localhost:8000
# Frontend, in a second terminal (Node 24)
cd frontend
npm ci --ignore-scripts
npm run dev # http://localhost:5173, set VITE_API_URL
The quality gates a contribution has to pass:
cd backend && pytest # enforces at least 70% coverage
cd backend && ruff check . && black --check .
cd frontend && npm run lint && npm run build
See CONTRIBUTING.md before opening a pull request.
First login
NETMapper ships with no users. Until an admin exists, every mutating endpoint returns 401 and the login page has nothing to authenticate against.
# Docker Compose
docker compose exec backend python -m netmapper.create_admin
# Kubernetes
kubectl exec -it -n netmapper deployment/netmapper-backend -- \
python -m netmapper.create_admin
The script prompts for a username, then for a password twice through
getpass, so neither appears in your shell history or in the process list.
The password is checked against the password policy (length floor and cap, a
common-password blocklist, and it may not equal the username) and against the
Have I Been Pwned range API using k-anonymity, then stored as a bcrypt hash.
The plaintext is never written anywhere. The first user is created with
role=admin and is_superuser=true.
The account is created with must_change_password set, so the first login
is gated on a password change: NETMapper refuses every other route until
the new password is set. Pick the password you actually want at that prompt.
For automation, --username and --password can be passed on the command
line, and --no-must-change skips the forced rotation when the password was
machine-generated. Avoid both on a shared machine, where ps output is
visible to other users.
Once you are in, add a switch under Switches, then run a scan. What to configure next (SMTP for email alerts, scan schedules, an AI provider) is in docs/admin/first-time-setup.md.