NETMapper install guide

Pick the deployment shape you want. Every path below is self-contained, with copy-paste commands.

View raw Markdown

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 legacy docker-compose binary). 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_KEY out 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:

  1. Checks that curl and Docker with the Compose plugin are present and that Docker is running and reachable by your user.
  2. Resolves the latest release and downloads two small files from it: release.env (version and image digests) and SHA256SUMS.
  3. Creates ./netmapper (or $NETMAPPER_DIR if you set it), downloads docker-compose.yml and docker-compose.https.yml for that release, and checks each file against SHA256SUMS. A mismatch deletes the file and aborts before anything starts.
  4. Writes .env: the release version, the two image digests from release.env, and freshly generated secrets (POSTGRES_PASSWORD, NETMAPPER_JWT_SECRET, NETMAPPER_ENCRYPTION_KEY). It also sets NETMAPPER_COOKIE_SECURE=false, because the default is plain HTTP on port 80 and browsers refuse Secure cookies there.
  5. Starts the stack and waits until GET /ready answers.
  6. 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.sh redirects to the stable tag of the Core repository, not to main. 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.example come from raw.githubusercontent.com; SHA256SUMS comes from the release assets on github.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. .env is created with mode 600 under umask 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.