TLS and domains

Serve the panel over HTTPS with ACME, your own certificate, or a reverse proxy.

Last updated Aug 14, 2026View as Markdown

Overview

You should serve the panel over HTTPS in production, because secure session cookies require it. There are three ways to get there, and the app picks one at startup in a fixed order: manual certificate files → ACME → plain HTTP. This page is about TLS for the panel itself; certificates used by your proxy inbounds are managed on the Certificates page.

Before you start

  • A domain whose DNS already points at the server, if you want ACME.
  • The ACME challenge must be able to reach the server from the internet.
  • Decide whether the panel terminates TLS itself or sits behind nginx, Caddy, Traefik or a CDN.

Steps

Automatic certificates with ACME

  1. Point your domain at the server and confirm DNS has propagated.

  2. Set the ACME keys in .env:

    APP_BASE_URL=https://panel.example.com
    ACME_ENABLED=true
    ACME_EMAIL=you@example.com
    ACME_STAGING=true
    ACME_AUTO_RENEW=true
    ACME_CACHE_DIR=/app/data/acme
    JWT_COOKIE_DOMAIN=example.com
    JWT_COOKIE_SECURE=true
  3. Restart and confirm a staging certificate is issued. Then set ACME_STAGING=false and restart again for the real one. Staging first keeps you from burning Let’s Encrypt rate limits while you debug DNS.

  4. Issued certificates are cached in ACME_CACHE_DIR, which is a persistent volume under Docker, so restarts do not re-issue.

ACME settings are also editable in Settings → Server → HTTPS and applied on the next restart, so you can flip ACME on or off without touching .env.

Bringing your own certificate

Point the app at your PEM files. Manual files take precedence over ACME.

TLS_CERT_FILE=/path/to/fullchain.pem
TLS_KEY_FILE=/path/to/privkey.pem

Behind a reverse proxy

Terminate TLS at the proxy and let the app speak plain HTTP locally, but still set APP_BASE_URL to the public HTTPS URL.

APP_BASE_URL=https://panel.example.com
ACME_ENABLED=false
JWT_COOKIE_SECURE=true
JWT_COOKIE_DOMAIN=example.com

The panel uses WebSockets and server-sent events for the live terminal, charts and events, so the proxy must pass Upgrade and Connection headers and use a generous read timeout.

server {
    listen 443 ssl http2;
    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;

    location / {
        proxy_pass http://127.0.0.1:9761;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_http_version 1.1;
        proxy_set_header Upgrade    $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 86400;
    }
}

Reference

Goal Set
Automatic HTTPS on a domain ACME_ENABLED=true, ACME_EMAIL, APP_BASE_URL=https://…
Your own certificate TLS_CERT_FILE, TLS_KEY_FILE
Behind a proxy or CDN APP_BASE_URL=https://…, ACME off, secure cookies on
Plain IP, for testing only APP_BASE_URL=http://ip:port, ACME off, JWT_COOKIE_SECURE=false
Variable What it does Default
ACME_ENABLED Turns automatic issuance on. With it off, no certificate is issued even if APP_BASE_URL is HTTPS. false
ACME_EMAIL Contact address for Let’s Encrypt notices. Required when ACME is on. empty
ACME_STAGING Uses the staging directory while you test. false
ACME_AUTO_RENEW Keeps the certificate renewed. true
ACME_CACHE_DIR Where issued certificates are cached. /app/data/acme
TLS_CERT_FILE Path to your certificate chain. Overrides ACME. unset
TLS_KEY_FILE Path to your private key. unset
JWT_COOKIE_SECURE Marks session cookies secure. Needs HTTPS. false
JWT_COOKIE_DOMAIN Cookie domain; leave empty in IP mode. empty

Troubleshooting

Certificate issuance fails. Confirm the domain resolves to this server and that the ACME challenge can reach it, because a firewall or a proxy in front of the panel that does not pass the challenge path is the usual cause. Test with ACME_STAGING=true so failures do not count against your rate limit.