---
title: "Panel Installation"
url: "https://control.actana.ai/docs/install/panel-installation"
description: "Deploy the Panel with Docker or bare Node: the reference Compose file, configuration, TLS in front, backup and upgrade."
updated: 2026-09-01T09:02:59+00:00
---

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`](https://hub.docker.com/r/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.

```mermaid
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

```bash
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:

```bash
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](/docs/panel/first-run), 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](/docs/get-started/docker-compose) uses:

```yaml
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:

```yaml
  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:

```bash
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](/docs/pairing/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](/docs/install/install-core) 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](/docs/install/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](/docs/reference/environment-variables).

### Secrets key

```bash
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](/docs/architecture/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](/docs/architecture/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:

```bash
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:

```bash
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](/docs/core/operating).

## See also

- [Docker Compose quickstart](/docs/get-started/docker-compose)
- [Panel Pairing](/docs/pairing/panel-pairing)
- [Environment Variables](/docs/reference/environment-variables)
- [Panel Link](/docs/architecture/panel-link)
