Panel Installation
The Panel is one service: plain HTTP out, all state in one directory. TLS belongs to a reverse proxy in front. Everything that must survive a restart — the Operator, the Core registry, sealed credentials, the secrets key — lives in that directory. Back it up and you have backed up the Panel.
It ships two equivalent ways: a Docker image, actana/panel, and the same build as a plain Node process for machines where containers are unavailable or unwanted. The Panel dials each Core on port 8443 over mutually authenticated TLS; Cores never dial back.
flowchart LR
browser[Browser]
proxy["TLS proxy (optional)"]
panel["Panel :7420"]
core["Core :8443"]
browser --> proxy
proxy --> panel
panel -->|"dials mTLS"| core
On localhost you skip the proxy: localhost is a secure context without TLS, and the reference compose publishes 127.0.0.1:7420.
Run it: Docker
docker run -d --name actana-panel \
-p 127.0.0.1:7420:7420 \
-v actana-panel-data:/data \
actana/panel:latest
Open http://localhost:7420. Binding 127.0.0.1 keeps the plain-HTTP port off the network. Pin the image (actana/panel:x.y.z) if you do not want :latest to move. Anything reaching the Panel from another machine should come through a TLS proxy — below.
The container runs as uid 65532 (the distroless nonroot account). There is no shell in the image — docker exec actana-panel sh will not work; reach for docker exec actana-panel /nodejs/bin/node -e '…', docker cp, or a debugging sidecar. A named volume needs no chown: Docker seeds it with the image's ownership. A host directory mounted at /data must be writable by uid 65532 — sudo chown -R 65532:65532 <dir> before the first start (under rootless Docker or Podman, use podman unshare chown or --userns=keep-id).
Run it: bare Node
The image's entry is an ordinary Node program. The same build runs anywhere Node 24 does:
pnpm install
pnpm build
AC_PANEL_DATA_DIR=/var/lib/actana-panel pnpm start
pnpm start runs packages/panel/bin/panel.mjs — the exact file the container starts. Supervise it with systemd, runit, or a terminal. It logs to stdout/stderr and shuts down cleanly on SIGTERM. The same reverse-proxy rules apply for non-localhost access.
First boot: the Operator
The first time you open the Panel, it asks you to create the Operator — a name and a password. That is the only identity the open-source Panel has: no accounts, roles, or invitations, exactly one Operator per Panel. Presenting the password yields a session cookie; on localhost it works without TLS, anywhere else the proxy must set X-Forwarded-Proto: https or the browser will send the cookie over plain HTTP.
The Operator password is not a fleet credential. Pairing a Core is a separate gesture — a short code minted on the Core, redeemed in Add a Core — and a Panel that knows no Cores opens in the first-run wizard, which walks you through it. Losing the Operator password is a Panel-data problem (restore the volume, or start over and re-pair).
The reference Compose file
One Panel and one Core on one network — the same file the Docker Compose quickstart uses:
services:
panel:
image: actana/panel:${ACTANA_TAG:-latest}
restart: unless-stopped
ports:
- "127.0.0.1:7420:7420"
volumes:
- panel-data:/data
core:
image: actana/core:${ACTANA_TAG:-latest}
restart: unless-stopped
environment:
- ACTANA_PUBLIC_HOST=core
volumes:
- core-home:/home/core
- ./repos:/home/core/repos
volumes:
panel-data:
core-home:
The Panel publishes 127.0.0.1:7420 and holds Operator, registry, and presentation. The Core publishes no port — the Panel dials wss://core:8443 over the compose network, never the reverse.
ACTANA_PUBLIC_HOST is load-bearing
That value names the addresses clients dial: each entry becomes a SAN on the Core's server certificate, and a pairing hands back one of them as the endpoint. It lives next to the service it names — never in .env, never guessed by the image. Rename the service and change this to match.
Since 0.4.2 the value is a comma-separated list — localhost,host.docker.internal,core,192.168.1.50 — and the certificate carries every name in it. Changing the list re-signs the certificate, but clients dialling a name that stays in the list keep working; only names that dropped out break.
Reaching a compose Core from a local CLI
The block above is Panel-only: the Core publishes no port, so nothing outside the compose network — including a host-machine actana — can reach it. For an all-local setup, publish the port and extend the list:
core:
ports:
- "8443:8443"
environment:
- ACTANA_PUBLIC_HOST=localhost,host.docker.internal,core
| Name | Who dials it |
|---|---|
core |
the Panel, over the compose network |
localhost |
your CLI on the same machine, through the published port |
host.docker.internal |
a CLI inside another container (Docker Desktop resolves it to your host; it does not resolve on the host itself) |
docker compose up -d, then mint one code per client — --public-host picks which address that code hands back, selecting from the list and never extending it:
docker compose exec core actana pair new --public-host core
docker compose exec core actana pair new --public-host localhost --label cli
Machines elsewhere on the LAN need a name that routes across it — add your machine's LAN IP to the list, reserved as static in your router (DHCP reservation by MAC address). A lease change silently invalidates dialling by that name, while every name still in the list keeps working. Command reference: Actana CLI Pairing.
Volumes
| Volume | Holds | Destroyed by |
|---|---|---|
panel-data |
Operator, Core registry, sealed credentials, secrets key unless AC_SECRETS_KEY is set |
docker compose down -v |
core-home |
Pairing identity, SQLite, Harness credentials | docker compose down -v |
./repos (bind) |
Checkouts; Add project finds them here | nothing — host directory |
docker compose down leaves all three. Files the Core writes into ./repos are owned by uid 1000; swap that bind for a named volume if that bites.
A second Core
Add a core2 service the same shape as core — its own ACTANA_PUBLIC_HOST, its own core2-home: volume. Service name, the matching ACTANA_PUBLIC_HOST entry, and volumes must agree. Pair with docker compose exec core2 actana pair new and Add a Core address core2:8443. A Core does not have to live in this file at all — install one on a machine that has your code and pair it to this Panel.
Pinning with ACTANA_TAG
Both image: lines read one variable so Panel and Core move together — they are version-locked at the handshake.
| Set | Get |
|---|---|
| nothing | :latest — the newest published release |
ACTANA_TAG=x.y.z |
that release, pinned |
ACTANA_TAG=x.y.z-beta |
that beta cut, pinned |
ACTANA_TAG=beta-x.y.z |
the open x.y train's tip — moves on every train merge |
Put it in .env beside the file to make it stick. Betas and their trains: Installing a Beta.
Configuration
Everything is environment variables. There is no config file. Copy .env.example to .env beside the compose file if you want values to stick; every entry is optional.
| Variable | Default | Meaning |
|---|---|---|
AC_PANEL_PORT / PORT |
7420 |
Port the Panel listens on |
AC_PANEL_HOST / HOST |
0.0.0.0 |
Interface to bind. 127.0.0.1 keeps a shared machine's loopback |
AC_PANEL_DATA_DIR |
/data in the image; platform data dir otherwise |
The one directory all Panel state lives in |
AC_SECRETS_KEY |
generated at <data dir>/secrets.key |
32-byte key (hex or base64) sealing each Core's stored credentials |
ACTANA_UPDATE_CHECK |
on | 0, false, or off stops the daily release check |
ACTANA_TAG |
latest |
Compose image tag for both Panel and Core |
ACTANA_IMAGE_NAMESPACE |
actana |
Docker Hub namespace both images come from — for forks publishing their own |
ACTANA_TAG and ACTANA_IMAGE_NAMESPACE are Compose interpolation for the image: lines, not process env the Panel binary reads. ACTANA_PUBLIC_HOST is not in .env — it lives in docker-compose.yml beside the Core service it names. Core-side variables live with the Core: Environment Variables.
Secrets key
openssl rand -hex 32
Set AC_SECRETS_KEY to keep the key out of the data directory — a copied volume or a backup alone then cannot open the fleet credentials. Left unset, the Panel writes the key to <data dir>/secrets.key and a backup of that directory includes it. Losing whichever key is in use means re-pairing every Core.
Update check
Once a day the Panel reads https://api.github.com/repos/actana/control/releases/latest, caches the answer for 24 hours, and only ever shows a banner. It never downloads or applies an update, and fails silent on any network error. ACTANA_UPDATE_CHECK=0 turns it off.
TLS
The Panel never grows certificate code. It listens on plain HTTP; TLS is your reverse proxy. Point Traefik, Nginx, or Caddy at port 7420. Two requirements:
- Forward WebSocket upgrades. The panel-link is a multiplexed WebSocket from the browser to the Panel. Without upgrades, the UI paints and then goes silent.
- Set
X-Forwarded-Proto: https. Without it the Panel issues a session cookie the browser will happily send over plain HTTP.
Change the compose port mapping from "127.0.0.1:7420:7420" to "7420:7420" only once that proxy is the thing in front of it. The Panel does not speak ACME and does not load PEM files.
Core-link mTLS is not yours to terminate. That path is mutually authenticated TLS 1.3 with material the Core mints itself. You do not supply those certs, renew them, or put a proxy in front of them — a TLS terminator between Panel and Core would break pairing and the live link. See Core Link.
Backup and upgrade
The container is disposable; the data directory is not. Under the reference compose the data directory is the panel-data volume:
docker run --rm -v <project>_panel-data:/data -v "$PWD":/backup debian \
tar czf /backup/panel-data.tar.gz -C /data .
Compose prefixes the volume with the project directory name. That archive is the whole Panel: Operator, Core registry, sealed credentials, and (unless you set AC_SECRETS_KEY) the secrets key. Restore by extracting into a fresh volume and starting the container. If you set AC_SECRETS_KEY, the key is not in the backup — store it with your secrets and provide it to the restored Panel.
This does not back up a Core. For a compose Core, back up core-home (and ./repos if you care about those checkouts); a metal Core's home is ACTANA_HOME.
The Panel has no in-app updater. The image is the release artifact:
docker compose pull && docker compose up -d
Schema migrations run automatically on boot. For a plain docker run Panel: docker pull, docker rm -f, re-run the same command. From source: git pull && pnpm install && pnpm build, then restart. A pinned ACTANA_TAG does not move until you change the pin — that is the point of a pin.
Cores are upgraded on their own machines — actana update on metal, the same compose pull && up -d for a compose Core. The version gate at the core-link handshake renders a drifted pair as "needs update" rather than degrading quietly. See Operating a Core.