On-premises setup: private pilot
Advanced / separate-node deployment. For one fresh VM, use the interactive installation guide. This manual path does not create an auto-repair baseline. Adapt paths carefully when recovering a partial single-VM install: its agent uses /opt/nodara, not /opt/nodara-node.
Nodara can run on your own server or VMs. This guide sets up the current development alpha as a private, single-panel-host pilot, using HTTPS and supervised services. It is not a production certification or a high-availability design. The application currently configures SQLite directly; setting DATABASE_URL will not enable PostgreSQL. PostgreSQL integration/concurrency acceptance, OS disk quotas, independent security review and real-game acceptance remain release gates.
One VM can run the panel, database, one worker, Docker agent and game servers together using the interactive installer, with Nginx, Apache or Caddy for HTTPS. Separate service accounts remain mandatory; the panel account must not have Docker access. Reserve memory/CPU for the panel and OS when allocating game capacity. Both share the same host failure and privilege boundary.
The manual instructions below use two Linux VMs for stronger host isolation: a panel VM for the website, database and worker, and a node VM for Docker, game data and the agent. A reserved node must have one customer and one registration; Phase 3E enforces panel allocations, not physical-host exclusivity. Docker remains the supported runtime. The Phase 3F native lab is not a deployment backend.
1. Prepare the machines
The commands below target a fresh Debian 13 system with systemd and Python 3.13. Other distributions need equivalent packages and paths. Debian 13's Python package uses Python 3.13. A Linux VM under an on-premises hypervisor is suitable; the WSL native lab does not certify a bare-metal deployment.
On both VMs:
sudo apt update
sudo apt install python3 python3-venv python3-pip git ca-certificates caddy
python3 --versionConfirm Python 3.13 before installing the locked dependencies. Choose storage and RAM for your workloads; declare only usable node capacity after reserving capacity for the OS, Docker and the agent. Store panel state and game volumes on persistent local storage, not a shared SQLite database on NFS.
Prepare DNS names such as panel.example.com and node1.example.com, replacing them everywhere below. Keep access behind your VPN/private network during the pilot. The HTTPS proxy must be directly reachable by permitted clients; these examples do not cover an additional load balancer.
| Endpoint | Access |
|---|---|
| Panel HTTPS 443 | Pilot users/admins; node also needs it for SFTP authorization |
| Optional HTTP 80 | Caddy redirects / HTTP certificate validation, if using that issuance method |
| Agent HTTPS 443 | Panel VM only; bearer authentication remains required |
| Panel 8000 / agent 8081 | Loopback only, never opened in the firewall |
| Allocated game TCP ports | Intended players only |
| Optional SFTP TCP 2022 | Intended customers only |
| Docker socket / RCON | Never exposed to customers or the network |
Public Caddy certificate issuance needs appropriate DNS and certificate-validation connectivity. For private-only DNS, use your organization's CA or Caddy's tls internal in each site block, and distribute the signing CA to browsers and both VMs. Add that CA under /usr/local/share/ca-certificates/, run sudo update-ca-certificates, and set SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt in both Nodara environment files so HTTPX uses the trusted system bundle. Obtain Caddy's internal CA certificate from the actual Caddy data directory on each VM; never copy private keys. Do not disable certificate verification. Caddy HTTPS documentation explains public and internal certificate behavior.
2. Install the panel code
Replace YOUR_REPOSITORY_URL and YOUR_TESTED_COMMIT with your repository and the reviewed revision you intend to run. Do not copy a Windows .venv or the development .local directory to the server.
sudo useradd --system --user-group --home-dir /var/lib/nodara-panel --create-home --shell /usr/sbin/nologin nodara-panel
sudo git clone YOUR_REPOSITORY_URL /opt/nodara
sudo git -C /opt/nodara checkout YOUR_TESTED_COMMIT
sudo python3 -m venv /opt/nodara/.venv
sudo /opt/nodara/.venv/bin/python -m pip install -r /opt/nodara/requirements-lock.txt
sudo install -d -o nodara-panel -g nodara-panel -m 0700 /opt/nodara/.local
sudo install -d -o nodara-panel -g nodara-panel -m 0755 /opt/nodara/staticfiles
sudo install -d -m 0700 /etc/nodara
sudo install -m 0600 /opt/nodara/deploy/onprem/panel.env.example /etc/nodara/panel.env
sudoedit /etc/nodara/panel.envKeep code and the venv administrator-owned; only the designated state/static directories need application ownership. The environment file must contain NODARA_DEBUG=0, your exact allowed hostname and HTTPS public origin, and a random, stable secret. Generate a secret locally with:
python3 -c 'import secrets; print(secrets.token_urlsafe(64))'Paste it into NODARA_SECRET_KEY using the protected editor. Configure working SMTP values for invitations/password reset. No .env file is loaded automatically: systemd reads the configured EnvironmentFile. Environment-file values are literal; do not put shell commands or export statements in it. Quote values containing spaces. Keep the file root-owned and mode 0600. Preserve the secret in protected backups: replacing it makes encrypted credentials/MFA secrets unreadable and invalidates sessions/signatures.
Initialize using the same identity/environment as the service. This shell helper runs only the management command you specify and prompts interactively when needed:
nodara_manage() {
sudo systemd-run --quiet --wait --pty --collect \
--uid=nodara-panel --gid=nodara-panel --working-directory=/opt/nodara \
--property=EnvironmentFile=/etc/nodara/panel.env \
/opt/nodara/.venv/bin/python manage.py "$@"
}
nodara_manage migrate
nodara_manage collectstatic --noinput
nodara_manage createsuperuser
nodara_manage check --deployCreate your own administrator credentials; there is no server default password. Do not use scripts.setup_local with this configuration. check --deploy currently warns about HSTS subdomain coverage and preload (W005, W021); these are intentionally off because Nodara cannot assume your entire DNS tree is HTTPS. Review those policies for your domain; do not silence unrelated warnings. DEBUG=0 already enables secure cookies, HTTPS redirection and HSTS for the panel host.
3. Start the panel and one worker
sudo install -m 0644 /opt/nodara/deploy/onprem/nodara-panel.service /etc/systemd/system/
sudo install -m 0644 /opt/nodara/deploy/onprem/nodara-worker.service /etc/systemd/system/
sudo systemd-analyze verify /etc/systemd/system/nodara-panel.service /etc/systemd/system/nodara-worker.service
sudo systemctl daemon-reload
sudo systemctl enable --now nodara-panel nodara-worker
sudo systemctl status nodara-panel nodara-workerThe panel uses the included nodara.asgi:application entrypoint and the locked Uvicorn dependency. It trusts forwarded headers only from the loopback proxy. The worker runs queued jobs and schedules; run exactly one worker and one panel process for this pilot. Do not use Django runserver, the Windows development supervisor or multiple hosts sharing this SQLite database. Django's ASGI guidance describes the server entrypoint.
Adapt the panel Caddy example to your domain/CA. On a fresh dedicated VM, install it as /etc/caddy/Caddyfile; on an existing proxy, merge the site block without replacing other sites:
sudoedit /etc/caddy/Caddyfile
# Use deploy/onprem/Caddyfile.panel.example with your hostname and TLS configuration.
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddyOpen https://panel.example.com/login/. Verify the certificate is trusted, sign in, enable MFA and keep recovery codes safely. Ensure CSS loads. Caddy forwards to loopback; WhiteNoise serves the collected static assets. The proxy sets the forwarded scheme, avoiding an HTTPS redirect loop. Caddy reverse-proxy behavior is the reference for trusted headers.
4. Install and connect the Docker node
On the node VM, install Docker Engine using the official Debian instructions, then verify sudo docker info reports Linux. Review Docker's published-port/firewall behavior: a generic host firewall rule alone may not filter published container ports. Apply appropriate Docker-aware rules or an upstream firewall, and test exposure from another machine.
sudo useradd --system --user-group --home-dir /var/lib/nodara-agent --create-home --shell /usr/sbin/nologin nodara-agent
sudo usermod -aG docker nodara-agent
sudo git clone YOUR_REPOSITORY_URL /opt/nodara-node
sudo git -C /opt/nodara-node checkout YOUR_TESTED_COMMIT
sudo python3 -m venv /opt/nodara-node/.venv
sudo /opt/nodara-node/.venv/bin/python -m pip install -r /opt/nodara-node/requirements-lock.txt
sudo install -d -m 0700 /etc/nodara
sudo install -m 0600 /opt/nodara-node/deploy/onprem/agent.env.example /etc/nodara/agent.env
sudoedit /etc/nodara/agent.envGenerate a separate random token (at least 32 characters) for this node and replace the example. Set NODARA_GAME_BIND_IP to the intended local game-facing interface IP, not a public NAT address absent from the VM. Keep the runtime journal path persistent at /var/lib/nodara-agent/runtime-state. Set the correct HTTPS panel URL. The Docker group is effectively privileged host access; the separate agent VM/account is a trust boundary, not a way to make Docker access unprivileged.
sudo install -m 0644 /opt/nodara-node/deploy/onprem/nodara-agent.service /etc/systemd/system/
sudo systemd-analyze verify /etc/systemd/system/nodara-agent.service
sudo systemctl daemon-reload
sudo systemctl enable --now nodara-agent
sudo systemctl status nodara-agentUse Caddyfile.agent.example on the node VM, replacing the hostname and documentation IP with the panel's actual source/egress IP. Apply the node firewall restriction too. Validate/reload Caddy as above. Do not add a proxy in front of this allowlist without reconsidering which remote address it sees.
In the panel's Nodes → Connect a node, enter the node's player address, HTTPS agent origin, private token and usable CPU/memory. Click Check connection. The panel verifies TLS and the agent's bearer token. Restart the agent after code/configuration changes; use a single agent process. Do not share one agent between multiple independent panel installations.
For SFTP, uncomment the three SFTP variables in the agent environment, choose the intended node interface, preserve the host-key path, permit port 2022 for intended customers, and restart the agent. The node must be able to call NODARA_PANEL_URL over trusted HTTPS. Optional S3 and managed SQL settings are documented in .env.example and the README; configure them only on the node, with private database/storage access.
5. Validate a pilot workload
- Create a customer and a small test service. The consenting operator must read and accept the Minecraft EULA in the form.
- Verify the worker completes provisioning before starting the service. Review Console and Ready for players, then connect a real client on the allocated game port.
- Test graceful restart and world persistence, then a backup and restore on test data. Check Files/SFTP, optional integrations and diagnostics as applicable.
- For Phase 3E, reserve a separate empty, clean, singly registered node for the customer. Confirm another customer's allocation is rejected. Reservation does not remove Docker limits or guarantee the absence of unmanaged workloads.
- Check logout/MFA/password reset, unreachable-node behavior, disk pressure alerts and firewall exposure. Keep logs of actual results. Our synthetic tests do not substitute for these real-game and site-specific checks.
Live file downloads use metadata change checks, not immutable snapshots. Same-size edits within one filesystem timestamp tick may escape those checks. Stop writes before downloading a file that must be a consistent copy.
6. Backup, upgrade and recover
Back up the panel's /opt/nodara/.local, protected environment files, deployed revision and proxy configuration. For a consistent SQLite snapshot, stop panel and worker, copy the state directory to protected backup storage, then restart both. Do not copy a live SQLite file alone and assume it is a consistent backup. Protect and test restore of the installation secret with the database.
Back up node Docker data/backup volumes and /var/lib/nodara-agent, including the runtime journal and SFTP key. Also preserve remote-store configuration and any managed SQL database using its own backup procedure. Nodara's service backups do not back up the whole panel or every external database. Stop game writes or use an application-consistent snapshot process before copying volumes. Never run docker volume prune as an upgrade step.
Supported single-VM installations with a healthy repair baseline should use the illustrated alpha upgrade guide. Remote agents and this separate-checkout layout require manual updates. Do not run the fresh installer or git pull over an exported single-VM installation.
For a manual separate-node upgrade, drain queued work, stop panel/worker/agent, take verified backups, update both code checkouts to the same reviewed revision and install its locked dependencies. Run panel migrate, collectstatic and checks with the management helper, then start agent, panel and worker. Check connection, job status and a disposable service. Worker recovery marks interrupted jobs for review; inspect actual node state before retrying. Allow the provided stop timeout for long backup operations.
Rollback after a schema/data change requires the matching code and a tested matching state backup; do not simply check out old code against a newer database. Restore node journals and volumes consistently. Validate restoration in an isolated pilot before reconnecting users.
Troubleshooting and current boundary
sudo journalctl -u nodara-panel -n 100 --no-pager
sudo journalctl -u nodara-worker -n 100 --no-pager
# On the node:
sudo journalctl -u nodara-agent -n 100 --no-pagerQueued jobs usually mean the worker is stopped or node access failed. A redirect loop suggests a missing/wrong trusted proxy scheme. A 400 response suggests a hostname mismatch. Agent connection errors may be TLS trust, token, source allowlist or Docker permissions. Missing styles require collectstatic with the deployed environment. Do not post secrets or raw logs publicly.
The live Ubuntu pilot records real Minecraft startup, console, file transfers, backup/restore and repair. The upgrade guide records its subsequent private-release update. These results do not validate every separate-node layout or your DNS, certificates, storage, firewall and actual game load. PostgreSQL has no configurable adapter in this release; production migration, concurrent-load acceptance, hard disk quotas, native backend integration and a full security assessment remain outstanding. See the whole Phase 3 report.
Configuration templates
These are examples, not credentials. Replace every example value. The interactive installer creates its own configuration; do not overwrite a working installation with these templates. Templates below use the separate-node layout described above.
panel.env.example
# systemd EnvironmentFile; replace EVERY example value. Do not commit the result.
NODARA_DEBUG=0
NODARA_SECRET_KEY=REPLACE_WITH_RANDOM_STABLE_SECRET
NODARA_ALLOWED_HOSTS=panel.example.com
NODARA_PUBLIC_URL=https://panel.example.com
NODARA_EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
NODARA_FROM_EMAIL=nodara@example.com
NODARA_EMAIL_HOST=smtp.example.com
NODARA_EMAIL_PORT=587
NODARA_EMAIL_USER=REPLACE
NODARA_EMAIL_PASSWORD=REPLACE
NODARA_EMAIL_TLS=1agent.env.example
NODARA_AGENT_TOKEN=REPLACE_WITH_A_RANDOM_TOKEN_OF_AT_LEAST_32_CHARACTERS
NODARA_GAME_BIND_IP=REPLACE_WITH_NODE_GAME_INTERFACE_IP
NODARA_RUNTIME_STATE_DIR=/var/lib/nodara-agent/runtime-state
NODARA_PANEL_URL=https://panel.example.com
NODARA_DOWNLOAD_USER_AGENT="Nodara/0.3 (contact: operator@example.com)"
# Optional SFTP: uncomment together only after firewall and callback setup.
# NODARA_SFTP_PORT=2022
# NODARA_SFTP_BIND=REPLACE_WITH_NODE_INTERFACE_IP
# NODARA_SFTP_HOST_KEY=/var/lib/nodara-agent/sftp-host-keyCaddyfile.panel.example
panel.example.com {
reverse_proxy 127.0.0.1:8000
}Caddyfile.agent.example
node1.example.com {
# Replace this documentation IP with the panel host's actual egress IP.
@panel remote_ip 192.0.2.10
handle @panel {
reverse_proxy 127.0.0.1:8081
}
handle {
respond "Forbidden" 403
}
}nodara-panel.service
[Unit]
Description=Nodara on-premises pilot panel
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=nodara-panel
Group=nodara-panel
WorkingDirectory=/opt/nodara
EnvironmentFile=/etc/nodara/panel.env
Environment=PYTHONDONTWRITEBYTECODE=1
ExecStart=/opt/nodara/.venv/bin/python -m uvicorn nodara.asgi:application --host 127.0.0.1 --port 8000 --workers 1 --proxy-headers --forwarded-allow-ips 127.0.0.1
Restart=on-failure
RestartSec=5
TimeoutStopSec=60
UMask=0077
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/opt/nodara/.local
[Install]
WantedBy=multi-user.targetnodara-worker.service
[Unit]
Description=Nodara single pilot job worker and scheduler
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=nodara-panel
Group=nodara-panel
WorkingDirectory=/opt/nodara
EnvironmentFile=/etc/nodara/panel.env
Environment=PYTHONDONTWRITEBYTECODE=1
ExecStart=/opt/nodara/.venv/bin/python manage.py runworker
Restart=on-failure
RestartSec=5
TimeoutStopSec=960
UMask=0077
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/opt/nodara/.local
[Install]
WantedBy=multi-user.targetnodara-agent.service
[Unit]
Description=Nodara Docker node agent
After=network-online.target docker.service
Requires=docker.service
[Service]
Type=simple
User=nodara-agent
Group=nodara-agent
SupplementaryGroups=docker
WorkingDirectory=/opt/nodara-node
EnvironmentFile=/etc/nodara/agent.env
Environment=PYTHONDONTWRITEBYTECODE=1
StateDirectory=nodara-agent
StateDirectoryMode=0700
ExecStart=/opt/nodara-node/.venv/bin/python -m uvicorn agent.app:create_app --factory --host 127.0.0.1 --port 8081 --workers 1 --no-proxy-headers
Restart=on-failure
RestartSec=5
TimeoutStopSec=960
UMask=0077
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
[Install]
WantedBy=multi-user.target