$ aoox

DocsStart here

Domain for the panel

Serve the aoox dashboard and API on your own domain with Let's Encrypt HTTPS, using the built-in reverse proxy.

Result
https://panel.example.com + https://api.panel.example.com
How
Dashboard/CLI (automatic) or a manual compose override — built-in Traefik
Needs
2 DNS records, port 80/443, PROXY_ACME_EMAIL

Before & after

Via IP (default)Via domain
Accesshttp://203.0.113.10:3000https://panel.example.com
APIhttp://203.0.113.10:3001https://api.panel.example.com
Session cookieno Secure flagSecure (follows WEB_ORIGIN https)
Git webhookIP:3001 must be publica clean API URL
Port 3000/3001the only way instill open (IP access), or closed with the option below

Two hostnames are needed because the browser calls the API directly (Socket.IO for the terminal and logs) — not through the web app.

Prerequisites

  • aoox is already running over IP (Installation) and you can log in.
  • Ports 80 and 443 are free on the host.
  • Two A DNS records pointing at the server's IP:
DNS
panel.example.com.       A   203.0.113.10
api.panel.example.com.   A   203.0.113.10

Wait until dig +short panel.example.com from outside answers with that IP — a certificate can only be issued once DNS is correct.

From the dashboard or the CLI (no SSH)

The fastest path: set INSTALL_DIR once in .env.dist (the absolute path of the folder containing docker-compose.dist.yml on this host), and the domain can then be changed anytime without SSH. The reverse proxy (Traefik) doesn't need to be provisioned manually first — if it isn't running yet when the domain is saved, the panel starts it automatically (see Proxy & domain for details on Traefik itself).

  1. Set INSTALL_DIR, then restart the stack

    .env.dist
    INSTALL_DIR=/opt/aoox   # absolute path of the docker-compose.dist.yml folder on this host
    docker compose -f docker-compose.dist.yml --env-file .env.dist up -d
  2. Settings → Domain panel (owner)

    Fill in the dashboard domain, the API domain, and an ACME email, then Save & apply. The panel writes docker-compose.override.yml through a helper container and re-runs docker compose up -d — the connection to the dashboard drops for a few seconds while the web/api containers are recreated, which is expected. If the proxy had never been provisioned, a warning appears saying it was just started automatically, plus three things still worth checking manually before the domain is actually reachable — see Common pitfalls.

  3. Or from the CLI

    aoox domain set --web panel.example.com --api api.panel.example.com \
      --acme-email you@example.com

    Calls the same endpoint (including the proxy warning above when it applies), for operators who prefer a terminal.

Compose only auto-includes docker-compose.override.yml when the base file is named exactly docker-compose.yml — since this stack uses -f docker-compose.dist.yml explicitly, the override is not picked up automatically unless it's also named explicitly. The panel (and aoox update panel) already do this internally; it only matters if you run docker compose up/pull by hand over SSH after setting a domain from the dashboard — add -f docker-compose.override.yml too, or the domain you set will disappear.

Only allow access through the domain (optional)

Ports 3000 and 3001 are published on every interface by default, so the dashboard also answers on IP:3000. A client can reach the web port directly with its own X-Forwarded-For header, which sidesteps the per-IP sign-in limit. Turn on Only allow access through the domain in Settings → Domain panel (or run aoox domain set --web … --api … --domain-only) and both ports listen on 127.0.0.1 only, so everything outside goes through Traefik. It is off by default, and it needs both the dashboard and the API domain: the CLI, webhooks and the terminal/log sockets use the API domain once port 3001 is closed.

You can lock yourself out
Turn it on only after https://panel.example.com opens. If DNS, the firewall or the certificate is broken afterwards, the panel cannot be reached at all and IP:3000 no longer works. Recover over SSH: in the install folder delete the two ports: lines in docker-compose.override.yml, then run docker compose -f docker-compose.dist.yml -f docker-compose.override.yml --env-file .env.dist up -d. The override uses the Compose !override tag, which needs Docker Compose 2.24.4 or newer for commands you run by hand (the panel itself applies it with its own, newer Compose). Turning the option off in the dashboard writes the file back without those lines. For the same reason aoox reinstall stops before changing anything when that file uses the tag and the host's Compose is older than 2.24.4: upgrade the Compose plugin, or turn the option off in the dashboard first.

Removing the domain

To go back to plain IP access, use Remove domain in Settings → Domain panel (owner, with a confirmation) or run aoox domain clear (--yes skips the prompt). The panel clears the saved domains, turns off Only allow access through the domain, deletes docker-compose.override.yml, puts WEB_ORIGIN and PUBLIC_API_URL back to http://<PUBLIC_IP>:3000 / :3001 in .env.dist, and recreates the web and api containers without the override. After the restart (a few seconds) the old domain no longer works: reopen the dashboard at http://<server IP>:3000 and sign in again. The reverse proxy, the domains of your applications and the ACME email are left alone. Like saving a domain, it needs INSTALL_DIR (400 without it) and a running Docker daemon (502 otherwise).

Manual, over SSH

An alternative when INSTALL_DIR isn't set (or you'd rather not), or when you want full control over the compose files.

  1. Set the domain variables in .env.dist

    .env.dist
    WEB_DOMAIN=panel.example.com
    API_DOMAIN=api.panel.example.com
    PROXY_ACME_EMAIL=you@example.com
    PROXY_ACME_STAGING=false          # true while testing
    
    # switch both to https + the domains above
    WEB_ORIGIN=https://panel.example.com
    PUBLIC_API_URL=https://api.panel.example.com
    VariableWhy
    WEB_DOMAIN / API_DOMAINThe hosts used for the Traefik labels on the web and api services.
    WEB_ORIGINMust match the browser URL exactly: the session cookie's Secure flag follows it, and the terminal/log gateway rejects any other Origin.
    PUBLIC_API_URLThe API URL the browser and webhook providers call.
    PROXY_ACME_EMAILTurns on the Let's Encrypt resolver; without it there's no HTTPS and no redirect.
  2. Restart with the domain override

    docker compose -f docker-compose.dist.yml -f docker-compose.domain.yml --env-file .env.dist up -d

    docker-compose.domain.yml attaches Traefik labels to the web and api services (routers aoox-web / aoox-api plus a -secure variant — the same shape as application domains) and joins them to the aoox network. Compose refuses to start if WEB_DOMAIN/API_DOMAIN are empty.

    From now on, always include both -f files on every up/pull. Running without the override strips the labels and the domain stops being served.
  3. Provision the reverse proxy (if not already)

    Log in (still over IP:3000 if needed) → Settings → Reverse proxy (Traefik) → provision (owner). A proxy that's already provisioned without PROXY_ACME_EMAIL must be removed and provisioned again for the ACME resolver to activate.

  4. Open https://panel.example.com

    A certificate is requested the first time the host is reached (a few seconds). Log in again — the old session (a cookie without Secure on the IP origin) doesn't carry over to the new origin.

Verification

curl -I https://panel.example.com          # 200, Let's Encrypt issuer
curl -I http://panel.example.com           # 301 → https
curl -s https://api.panel.example.com/auth/setup-status   # {"needsSetup":false}
  • The web terminal opens without an origin not allowed error.
  • Realtime logs on the Deploy tab keep streaming (Socket.IO to PUBLIC_API_URL).
  • The webhook URL on an application's Webhook tab now starts with https://api.panel.example.com/webhooks/….

Variations

Try it with staging first

PROXY_ACME_STAGING=true uses Let's Encrypt's staging CA (no rate limit; the certificate isn't trusted by browsers). Once DNS and ports are confirmed correct, switch it to false, remove the proxy, and provision it again — the ACME volume will then request a production certificate.

Behind Cloudflare

  • SSL mode Full (strict); Flexible causes a redirect loop.
  • When first issuing the certificate, set both records to DNS only.
  • WebSockets (terminal, logs) are supported by Cloudflare; make sure no rule is blocking them.

Your own reverse proxy (nginx/Caddy in front)

No need for docker-compose.domain.yml: point your proxy at :3000 and :3001, enable WebSocket upgrades, and still set WEB_ORIGIN/PUBLIC_API_URL to the public https URLs. If TLS terminates at your proxy and reaches aoox over http, set COOKIE_SECURE=true so the cookie stays Secure.

Common pitfalls

SymptomCause & fix
set WEB_DOMAIN in .env.dist during upThe variable is empty; compose refuses to start.
Domain works, then disappears after an updateup -d without -f docker-compose.domain.yml. Always include both files.
Login succeeds but is immediately logged out / 401WEB_ORIGIN is still http or a different host — the Secure cookie/origin don't match. Fix it, up -d, log in again.
Terminal: origin not allowedSame as above: WEB_ORIGIN ≠ the browser's URL.
Traefik's default certificatePROXY_ACME_EMAIL was empty at provision time, DNS isn't correct yet, or port 80 is closed. Remove the proxy → provision again once fixed.
Logs/terminal don't stream, other pages are finePUBLIC_API_URL isn't reachable from the browser (WebSockets blocked by a proxy in front).
Domain saved, DNS & the OS firewall (ufw) are fine, but the browser says "unable to connect"Check docker ps --filter name=aoox-proxy — if it's missing, the proxy wasn't running (the dashboard/CLI now starts it automatically when a domain is saved, but an older apply from before this feature won't have). If the proxy is already Up but it still fails, ports 80/443 are likely blocked by your VPS provider's firewall/security group — ufw inactive only means the OS firewall isn't blocking, not that the ports are open to the public.
Not available yet
A single hostname for web+API (path-based); wildcard certificates; DNS validation before applying (unlike application domains, which have their own DNS check); automatic rollback if docker compose up fails after the domain is changed.

Next steps

Found a mistake? Edit this page on GitHub ↗