Docker
The Docker client runs ncclient, Nebula, and (on lighthouses) dnsmasq in one container. See Docker installation for the image and a full docker-compose.yml. The commands below assume that compose file, with the service named ncclient, run from the directory that holds it.
Where state lives
Everything that must survive a container recreate lives in the /data volume, as long as the compose file sets these two paths (the example does):
NEBULA_OUTPUT_DIR: "/data/nebula"
NEBULA_DEVICE_TOKEN_FILE: "/data/nebula-commander/token"
| Path in the container | What it is |
|---|---|
/data/nebula-commander/token | Device token |
/data/nebula-commander/settings.json | Server URL, node ID, accepted routes |
/data/nebula/ | Nebula config.yaml, certificates, status |
Without those two variables the image falls back to /etc/nebula and /etc/nebula-commander/token. Those paths are outside the volume, so the device loses its enrollment every time the container is recreated.
Enroll
In Nebula Commander, create or sign a certificate for the node, then click Enroll and copy the code.
Set
ENROLL_CODEin the compose file and start the container:docker compose up -d docker compose logs -f ncclient
The container enrolls once, when no token exists yet. After that, ENROLL_CODE is ignored and you can remove it.
Run and control
The container starts ncclient on its own and restarts with Docker (restart: unless-stopped).
docker compose logs -f ncclient # ncclient, Nebula, and dnsmasq output
docker compose restart ncclient # restart everything in the container
docker compose down # stop (the volume and enrollment are kept)
docker compose up -d again after editing the compose file.
Subnet routes and exit nodes
Run ncclient routes inside the container. Pass NEBULA_COMMANDER_CONFIG_DIR so the command sees the same settings.json as the running client, and --output-dir so it sees the routes offered to the node:
docker compose exec -e NEBULA_COMMANDER_CONFIG_DIR=/data/nebula-commander ncclient \
ncclient routes --output-dir /data/nebula list
docker compose exec -e NEBULA_COMMANDER_CONFIG_DIR=/data/nebula-commander ncclient \
ncclient routes --output-dir /data/nebula accept 192.168.1.0/24
reject, accept-exit-node --via <IP>, and reject-exit-node work the same way (see the ncclient CLI page). The running client applies changes on its next poll.
Split-horizon DNS
The Docker client is how a lighthouse serves Magic DNS. Set SERVE_DNS: "true". When the node is a lighthouse and DNS is enabled for the network, the container polls for the dnsmasq config (every NEBULA_DNS_POLL_INTERVAL seconds, default 60) and runs dnsmasq on the node’s Nebula IP, port 53. On non-lighthouses dnsmasq stays off.
The container does not apply split-horizon DNS to the host it runs on (it doesn’t pass --accept-dns). To resolve Nebula names on that host too, point its resolver at the lighthouse IP yourself.
Re-enroll
The container only uses ENROLL_CODE when no token exists. There are two ways to re-enroll.
Enroll inside the running container. This needs no restart unless the server changed:
docker compose exec -e NEBULA_COMMANDER_CONFIG_DIR=/data/nebula-commander ncclient \
ncclient enroll --code NEWCODE
The server URL comes from NEBULA_COMMANDER_SERVER. The old token is replaced, and the client switches to the new one on its next poll.
Or remove the token and let the container enroll on start:
docker compose exec ncclient rm /data/nebula-commander/token
# set ENROLL_CODE to the new code in docker-compose.yml, then:
docker compose up -d --force-recreate
docker compose logs -f ncclient
To move to a different server, change NEBULA_COMMANDER_SERVER, use either method above, then docker compose up -d --force-recreate.
Troubleshooting
Token file not found and ENROLL_CODE not set: the container has no token and nothing to enroll with. SetENROLL_CODE, or check that the volume is mounted and the token path points into it.- The device keeps asking to enroll after every recreate: the token path isn’t in the volume. See Where state lives.
Revoked, deleted, or re-enrolled elsewhere - Nebula stopped. Enroll again to reconnect.: the device was revoked, deleted, or re-enrolled elsewhere; Nebula was stopped and its config and key removed. Re-enroll it with a new code.- Nebula can’t create its tun device: the container needs
network_mode: hostandcap_add: NET_ADMIN. It also needs rootful Docker. Under rootless Docker or Podman, “host” networking is a private namespace owned by an unprivileged user, so Nebula can’t configure the real host’s network and inbound UDP never reaches it. - dnsmasq never starts: check
SERVE_DNSis set, the node is a lighthouse, and DNS is enabled for the network. The logs showWarning: listen-address … not assignedif Nebula’s interface never came up.