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:

  1. 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.
  2. 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.

See also

Built by Qcentic