Skip to content

Install the panel

The panel is one container plus PostgreSQL. One compose file runs both, from the published images; your reverse proxy puts HTTPS in front.

You need a machine with Docker and the Compose plugin, a domain pointing to it (e.g. panel.example.com) and a reverse proxy (examples below).

  1. Copy compose.yml and .env.example from the deploy/ folder of the panel’s repository into an empty folder, e.g. /opt/panel.

  2. Create .env and set the two required values:

    Terminal window
    cp .env.example .env
    # POSTGRES_PASSWORD=<a long random password, e.g. from: openssl rand -hex 24>
    # PANEL_PUBLIC_URL=https://panel.example.com
  3. Start it:

    Terminal window
    docker compose up -d

    The first start creates the master key in keys/master.key and the database.

  4. Point your reverse proxy at 127.0.0.1:8080 (examples).

  5. Open the setup link from the log:

    Terminal window
    docker compose logs panel | grep setup

    The link carries the setup token, so nobody who finds a fresh panel on the internet can claim it. The setup wizard asks how you’ll use the panel (homelab or hosting, see Overview) and creates your operator account and first tenant.

Next: add a node. A node on the same machine: put its enrollment token into .env as AGENT_ENROLLMENT_TOKEN=… and run docker compose --profile agent up -d; other machines use compose.agent.yml or the command the panel shows.

Variable Meaning
POSTGRES_PASSWORD the database password (required)
PANEL_PUBLIC_URL the address agents and emails use to reach the panel, e.g. https://panel.example.com (required)
PANEL_CATALOGUE_URL the game catalogue to add games from (default: the public one). Also in Admin → Settings.
PANEL_DOCS_URL the documentation website for the panel’s help links (?), default: this site. Also in Admin → Settings.
PANEL_BIND, PANEL_PORT where the panel is published (default 127.0.0.1 and 8080)
TRUSTED_PROXY_NETWORK your reverse proxy’s network (default 172.16.0.0/12, Docker’s networks)
PANEL_KEYS_DIR the folder with the master key (default ./keys)
PANEL_IMAGE, AGENT_IMAGE the panel image, and the agent image the Add node command uses (default: the published latest)
AGENT_ENROLLMENT_TOKEN, AGENT_SFTP_PORT for the agent on this machine (--profile agent)

Sign-in providers go into panel.env (see panel.env.example, and Email and sign-in). All settings: Environment variables.

keys/master.key encrypts the keys that protect secrets (Steam passwords, secret variables, the push key) in the database, so a database dump alone reveals nothing. The compose file’s init step creates it on the first start and makes it readable only by the panel; it never replaces an existing one.

The hosting profile requires it: the setup and Admin → Settings refuse hosting without a master key, and a hosting panel without one doesn’t start. Other people’s secrets must not depend on the database alone.

The panel listens on 127.0.0.1:8080 and expects a reverse proxy in front for HTTPS. HTTPS is needed for passkeys, the phone app and push notifications. Whatever proxy you use:

  • Set TRUSTED_PROXY_NETWORK to the network the proxy connects from, so the panel sees the real client addresses and HTTPS. A proxy on the same machine connecting to 127.0.0.1:8080 arrives from Docker’s network: the default fits.
  • Pass WebSockets through: the live console and the agents use them.
  • Don’t buffer responses for the console stream (the panel sends X-Accel-Buffering: no for nginx).
  • Pass the Host header on unchanged: live connections from another site are refused by comparing it with the browser’s Origin.
  • Security headers (Content-Security-Policy, X-Frame-Options, nosniff, Referrer-Policy) come from the panel itself; the proxy only adds HSTS if you want it.
  • Allow large request bodies, or accept that browsers upload big files in smaller parts: uploads adapt when a proxy answers 413.

Caddy gets the certificate itself and passes WebSockets and the Host header on by default.

panel.example.com {
reverse_proxy 127.0.0.1:8080 {
flush_interval -1
}
}
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 443 ssl;
http2 on;
server_name panel.example.com;
ssl_certificate /etc/letsencrypt/live/panel.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/panel.example.com/privkey.pem;
client_max_body_size 100m;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 1h;
}
}

With Traefik in Docker, the panel joins Traefik’s network instead of publishing a port. Next to compose.yml, a compose.override.yml (Compose reads it automatically):

services:
panel:
networks: [default, traefik]
labels:
traefik.enable: "true"
traefik.docker.network: traefik
traefik.http.routers.panel.rule: Host(`panel.example.com`)
traefik.http.routers.panel.entrypoints: websecure
traefik.http.routers.panel.tls.certresolver: letsencrypt
traefik.http.services.panel.loadbalancer.server.port: "8080"
networks:
traefik:
external: true

Use your Traefik’s network, entry point and certificate resolver names. Set TRUSTED_PROXY_NETWORK to the Traefik network’s range (docker network inspect traefik).

Earlier versions had compose.panel.yml and a master.key next to it. The new compose.yml keeps the same project name, so the database stays:

Terminal window
docker compose -f compose.panel.yml down
mkdir -p keys && mv master.key keys/master.key
docker compose up -d

The compose file enables IPv6 for the panel’s network, so mail servers, registries and nodes reachable only over IPv6 work too.

  • GET /health/live: the process runs.
  • GET /health/ready: the database is reachable.