Troubleshooting
View as MarkdownTroubleshooting
Start with: which part is failing, the control plane (docker compose logs nebulactrl), the agent (journalctl -u nebulactrl-agent), or the proxy (docker logs nebulactrl-traefik on the agent host)?
The agent does not connect#
| Symptom | Cause / fix |
|---|---|
| Installer: "refusing plain http:// control plane URL" | The control plane URL is http://. Use an https:// URL (see serving the dashboard) or --insecure-dev on a trusted dev network. |
| Installer: "Docker Engine … too old" / "daemon is not reachable" | Install or start Docker 24+ (systemctl start docker). |
Journal: enrollment 401 / "invalid or expired token" |
Tokens are single use and live 1 hour. Create a new one. |
| Journal: TLS / certificate errors | The control plane's certificate is not valid for the hostname in --url, or the host's clock is wrong (timedatectl). |
| Journal: "agent revoked; re-enroll" | The server was revoked. Re-enroll. |
| Connects, drops, reconnects | A reverse proxy is closing idle WebSockets. Raise its read/idle timeout for /agent/v1/connect; the agent heartbeats every 10 s. |
Server shows offline but the host is fine |
Outbound HTTPS blocked, DNS broken, or the control plane URL changed. curl -v <url>/healthz from the host. |
Proxy: port 80 or 443 in use#
The server card shows Proxy error and adding a domain fails with 422 proxy_unavailable. Find the occupant:
sudo ss -ltnp 'sport = :80 or sport = :443'
docker logs nebulactrl-traefik 2>&1 | tail -20Typical culprits: a host nginx/Apache, or another reverse-proxy container. (The control plane's own compose file has no proxy: the host's agent's Traefik serves the dashboard.) Stop it, or move NebulaCtrl's proxy: sudo systemctl edit nebulactrl-agent, add --proxy-http-port / --proxy-https-port to ExecStart (or put NEBULACTRL_PROXY_HTTP_PORT=8080 into /etc/nebulactrl-agent.env) and restart. Public HTTPS certificate issuance needs the real 80/443 reachable from the internet.
The proxy also needs 127.0.0.1:9000 for its admin entrypoint. If something else (Portainer, for example) uses that port, the proxy fails to start the same way; move it with --proxy-admin-port (or NEBULACTRL_PROXY_ADMIN_PORT).
The agent creates nebulactrl-traefik when it starts, so after freeing the port run sudo systemctl restart nebulactrl-agent; the heartbeat then reports the proxy as running.
A domain does not work#
- DNS: the domain row shows DNS status. A records must point at the server's public IP; a job rechecks every 10 minutes (
dns_okis empty when the server has no public IP configured). Wildcard CNAMEs to a proxy such as Cloudflare in orange-cloud mode make the check say "mismatch" although it may work. - Certificate: Traefik needs port 80 reachable from the internet for the ACME HTTP-01 challenge and an ACME email on the server; only non-local names get Let's Encrypt certificates. Look at
docker logs nebulactrl-traefik 2>&1 | grep -i acme. Let's Encrypt rate limits apply to repeated failures; wait or use another hostname. - 502 / "no upstream": the service is not healthy or not on the edge network.
docker ps --filter name=nc-<slug>; check the deployment succeeded. - Hostname conflict:
409 domain_conflict: a hostname belongs to exactly one service in the whole installation. - Local testing:
*.localhost,*.local, bare names and IPs get Traefik's default self-signed certificate: usecurl -k. Unknown hosts get Traefik's default 404.
A deployment fails#
Open the deployment: the step timeline says where, the log says why.
| Message | Meaning |
|---|---|
| "Not delivered: server offline. No changes were made." | The agent never received the command within 15 min. Safe to retry once the server is online. |
| "Waiting for <server> (offline since …)" | The command was acknowledged; its outcome is unknown until the agent returns. Do not retry blindly; check the host or revoke the server. |
| pull error (unauthorized, not found) | Image name/tag or registry credentials wrong. |
| "Previous release is still serving" + container logs | The new container failed its health check; fix the app or the health check settings (path, port, start period, deadline). The old release is untouched. |
rolled_back |
The route verification failed; the old release was restored. Check the route/health path and that the app answers <500 for the first hostname. |
rollback_failed |
See recovering from rollback_failed. |
unresolved_reference / reference_cycle |
A ${{service.KEY}} variable points at an unknown service/key in the same environment, or variables reference each other in a loop. |
409 deployment_in_progress |
Another deployment of this service is still active; wait for it to reach a terminal state. |
| build fails | The full docker build output is in the deployment log. Builds run on the target host with its Docker daemon (timeout 30 min). |
Disk full on an agent host#
Images, the build cache and volumes share Docker's data root. Check the disk gauge (warns at the per-server threshold, default 80 %). The server's Docker cleanup runs daily and starts by itself when the alert is active; press Run cleanup now on the server page to reclaim at once, or lower a service's image retention. Never docker volume prune blindly: it removes service data (the cleanup's Remove unused volumes keeps every volume of your services).
The UI looks stale#
If the pill "Live updates paused — retrying" is visible, the event stream (/api/v1/events, Server-Sent Events) is blocked, usually by a proxy that buffers or has a short timeout. Data still refreshes every 15 s. Disable buffering for text/event-stream in your reverse proxy.
Control plane will not start#
| Log message | Fix |
|---|---|
NEBULACTRL_SECRET_KEY is required / invalid |
The key must be base64 of exactly 32 bytes (set NEBULACTRL_SECRET_KEY or NEBULACTRL_SECRET_KEY_FILE, not both). |
sealed values are not on the current key or format; run nebulactrl-server rekey (warning) |
Values are still on an older key or in the v1 format. Run nebulactrl-server rekey (secrets at rest). Everything keeps working while the old key is in NEBULACTRL_SECRET_KEY_PREVIOUS. |
decryption failed for a stored credential, rekey reports cannot decrypt |
The value was sealed with a key that is not in the ring (a previous key was removed too early), or its row was tampered with. Put the old key back into NEBULACTRL_SECRET_KEY_PREVIOUS, or re-enter the secret. |
database unreachable |
Postgres is not healthy or the password in NEBULACTRL_DATABASE_URL does not match POSTGRES_PASSWORD (the password is only applied when the database volume is first created). |
goose up errors |
A migration failed; the database may be on a newer schema than the binary (downgrade). Restore a backup or run the newer version. |
| Exits immediately under Compose | docker compose logs nebulactrl; missing required .env values are reported by Compose itself. |
Locked out#
Lost the password of the only Owner, or the phone with the authenticator app? Both are fixed from the control-plane host; nothing needs SMTP or another admin. Run the commands with the server's environment, in the standard install (/opt/nebulactrl; the Compose service is nebulactrl, or control-plane in deploy/compose):
cd /opt/nebulactrl
docker compose exec nebulactrl nebulactrl-server reset-password you@example.com
docker compose exec nebulactrl nebulactrl-server reset-2fa you@example.comreset-password <email>prints a one-time link (<dashboard>/reset-password?token=ncr_…, works once, valid 30 minutes). Open it and choose a new password. IfNEBULACTRL_PUBLIC_URLis not set the output shows the path only: put the address you open the dashboard at in front of it.--generatesets a random password instead and prints it once; use it for an account that has no password (passkey or single sign-on only). It asks you to type the account email first (--confirm <email>answers in advance).reset-2fa <email>removes the authenticator app (also a half-finished enrolment and its lockout), every passkey and every recovery code, after you type the account email (--confirm <email>). Afterwards the account signs in with its password alone until it enrols again in Settings → Security; a team that requires 2FA asks for that at the next sign-in. It refuses for an account with no password and no linked provider whose only way in is its passkeys: runreset-password --generatefirst.- A lost password and a lost second factor: run both. A reset link never bypasses 2FA.
- Both end every browser session of the account and are audited as system actions (
operator.password_reset_link,operator.password_set,operator.2fa_reset, in each of the account's teams, with the OS user that ran them; never the link or the password). API tokens are not revoked: revoke them in Settings → Tokens if you suspect a leak. - No accounts yet (
error: this instance has no accounts yet…): first-run setup is still pending. Open the setup URL the installer printed (<dashboard>/setup?token=…, also in/opt/nebulactrl/INSTALL-SUMMARY.txt). - Whoever can run these has the database and the secret key already; see login methods for the by-email flow and the team-admin reset.
Reporting a problem#
Include the NebulaCtrl version, the deployment ID, the relevant correlation_id log lines, and journalctl -u nebulactrl-agent around the time. Remove secrets first. Security problems: see SECURITY.md.