Edgeweir
Deployment

Docker Compose

Deploy the console image with Docker Compose, or without Compose using docker run.

Runtime requirements and other platforms: deployment overview. BaoTa / aaPanel and deploy.sh: baota.en.md.

Compose services

The compose.yml project name is edgeweir: the volume is edgeweir_postgres-data, the network edgeweir_default.

ServiceImageEnabledNotes
consoleghcr.io/marvinli001/edgeweir:${EDGEWEIR_VERSION:-latest}DefaultROLE=all; publishes ${EDGEWEIR_HTTP_PORT:-127.0.0.1:3000}:3000 and ${EDGEWEIR_NODE_API_PORT:-8443}:8443; read-only root file system, /tmp on tmpfs, no-new-privileges
postgrespostgres:18.6-alpine (pinned by digest)DefaultVolume postgres-data mounted at /var/lib/postgresql; no published port; console starts after the pg_isready health check passes
clickhouseclickhouse/clickhouse-server:26.9-alpine (pinned by digest)COMPOSE_PROFILES=analytics in .envVolume clickhouse-data

The image is public; pulling needs no login. Tag rules: versions, upgrades, and rollback.

1. Prepare

  1. Provision a Linux server (amd64 or arm64) with Docker Engine and Docker Compose v2:

    docker compose version
  2. Open 8443/TCP in the cloud security group. Ports Docker publishes skip host firewalls such as ufw and firewalld; to limit sources, use the cloud security group or the DOCKER-USER chain. Before Docker Engine 28, hosts on the same layer-2 network can reach ports published to 127.0.0.1: use 28 or later. Exposure of 3000/TCP: ports and reverse proxy.

2. Fetch the files

The server needs only compose.yml and .env.

mkdir -p /opt/edgeweir && cd /opt/edgeweir
curl -fsSLO https://raw.githubusercontent.com/marvinli001/edgeweir/master/compose.yml
umask 077
curl -fsSL -o .env https://raw.githubusercontent.com/marvinli001/edgeweir/master/.env.example

3. Generate secrets

sed -i "s|^EDGEWEIR_MASTER_KEY=.*|EDGEWEIR_MASTER_KEY=$(openssl rand -base64 32)|" .env
sed -i "s|^POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=$(openssl rand -hex 24)|" .env
VariableCommandConstraint
EDGEWEIR_MASTER_KEYopenssl rand -base64 32Use the output as is, keeping /, +, and =; the console refuses to start on any other character or when it decodes to fewer than 32 bytes.
POSTGRES_PASSWORDopenssl rand -hex 24Embedded in DATABASE_URL; letters and digits only.

The master key encrypts the internal CA key, certificate keys, S3 origin keys, DNS API credentials, and the setup token at rest, and derives the session secret. Back it up offline, apart from database backups; without it that data is unrecoverable.

Master key file

The master key can stay out of .env: put it in a file mounted as a Compose secret and leave EDGEWEIR_MASTER_KEY in .env empty.

umask 077
openssl rand -base64 32 > master.key
chown 1000 master.key   # the image runs as the node user (uid 1000)
sed -i "s|^EDGEWEIR_MASTER_KEY=.*|EDGEWEIR_MASTER_KEY=|" .env

compose.override.yml next to compose.yml (Compose merges it automatically):

services:
  console:
    environment:
      EDGEWEIR_MASTER_KEY_FILE: /run/secrets/edgeweir_master_key
    secrets:
      - edgeweir_master_key
secrets:
  edgeweir_master_key:
    file: ./master.key

The file's trailing newline is ignored; the console refuses to start when EDGEWEIR_MASTER_KEY is also set to a non-empty value or the file is empty or cannot be read.

Other secret files

These variables can be read from files too: set <variable>_FILE, mount the file as above, and leave the variable out of .env. The rules of the master key file apply.

VariableFor
DATABASE_URL_FILEExternal PostgreSQL (compose.baota-host.yml, standalone containers): the file holds the whole connection string. The bundled database of compose.yml is reachable inside the compose network only; POSTGRES_PASSWORD stays in .env
BETTER_AUTH_SECRET_FILEDeployments that set BETTER_AUTH_SECRET
EDGEWEIR_CLICKHOUSE_PASSWORD_FILEExternal ClickHouse; also set EDGEWEIR_CLICKHOUSE_PASSWORD: "" in compose.override.yml, or the template's default conflicts with it

Rotating the master key

To replace the master key (for example after a suspected leak) and move the encrypted data to the new one:

  1. In .env, move the old value to EDGEWEIR_MASTER_KEY_PREVIOUS and set EDGEWEIR_MASTER_KEY to the output of openssl rand -base64 32. With files, use EDGEWEIR_MASTER_KEY_PREVIOUS_FILE and EDGEWEIR_MASTER_KEY_FILE.

  2. docker compose up -d.

  3. Wait for the log line no envelope uses EDGEWEIR_MASTER_KEY_PREVIOUS any more:

    docker compose logs console | grep EDGEWEIR_MASTER_KEY_PREVIOUS

    On envelopes still use EDGEWEIR_MASTER_KEY_PREVIOUS: keep it set, keep the old key and look into the cannot re-seal lines.

  4. Remove EDGEWEIR_MASTER_KEY_PREVIOUS and run docker compose up -d again.

ItemNotes
Sessions and two-factor authenticationNot affected: the session secret keeps its value. After a master key leak, also set a new BETTER_AUTH_SECRET: every session ends and two-factor authentication must be enrolled again
NodesNothing to do
Several console instancesRecreate all of them with the same variables
Backups from before the rotationStill encrypted with the old key: keep it offline, and set it as EDGEWEIR_MASTER_KEY_PREVIOUS to restore one
deploy.sh deploymentsEdit .env, then run ./deploy.sh restart; backups leave EDGEWEIR_MASTER_KEY_PREVIOUS out

4. Configure .env

Set the console URL:

sed -i "s|^EDGEWEIR_PUBLIC_URL=.*|EDGEWEIR_PUBLIC_URL=https://cdn-admin.example.com|" .env

Required variables:

VariableDescription
EDGEWEIR_MASTER_KEY or EDGEWEIR_MASTER_KEY_FILEMaster key, or a file holding it, from the previous step. The console refuses to start without it.
POSTGRES_PASSWORD or DATABASE_URLPassword of the bundled PostgreSQL. compose.yml builds DATABASE_URL from it and ignores the DATABASE_URL line in .env. Compose refuses to start without it; deployments created without it used edgeweir, so set that. Without Compose, set DATABASE_URL instead.
EDGEWEIR_PUBLIC_URLURL browsers use for the console, including scheme and port. Must match the browser address bar; otherwise sign-in fails the origin check. Defaults to http://localhost:3000, which also points the generated node install commands to localhost.

Common optional variables (uncomment in .env):

VariableDefaultDescription
EDGEWEIR_VERSIONlatestImage tag to pull. Pin a dated tag in production; see pinning a version.
EDGEWEIR_NODE_API_URLhttps://<host of EDGEWEIR_PUBLIC_URL>:8443URL nodes use for the node channel; a URL saved under "Node channel" in System settings wins; see node channel URL and certificate.
EDGEWEIR_TRUSTED_PROXIESEmptyReverse proxy addresses; see trusted proxies.
EDGEWEIR_HTTP_PORT, EDGEWEIR_NODE_API_PORT127.0.0.1:3000, 8443Published host ports, optionally with a bind address. The web port is local only by default: ports Docker publishes bypass ufw and firewalld, so a reverse proxy serves it; EDGEWEIR_HTTP_PORT=3000 publishes it on every interface.
BETTER_AUTH_SECRETEmpty, derived from the master keyDeployments that set it keep it; the console refuses to start once it is removed.
EDGEWEIR_DOWNLOADS_DIREmptyDirectory of the node package mirror; see downloads mirror.

All variables: environment variables.

After setup, the node release source and the origin allow list are configured in System settings, GeoIP in Protection settings, and SMTP on the Alerts page; saving applies them without a restart. Values saved there take precedence over EDGEWEIR_SMTP_CA_FILE and EDGEWEIR_NODE_RELEASE_BASE_URL in .env, which take precedence over the defaults. EDGEWEIR_OUTBOUND_ALLOW_CIDRS bounds the outbound addresses saved there; the web console cannot widen it.

5. Start

docker compose pull
docker compose up -d
docker compose logs -f console

Startup sequence:

OrderBehaviorLog
1Waits for the database, up to 60 secondsdatabase not reachable yet
2Runs database migrationsdatabase migrated
3On first start, creates the node channel internal CA; its private key is encrypted with the master key—
4While uninitialized, prints the setup tokenfirst-run setup
5Node channel listens on 8443 and logs the CA fingerprintnode channel listening (caSha256)
6Web console and API listen on 3000console listening

6. Run setup

  1. Read the setup token:

    docker compose logs console | grep setupToken

    An uninitialized console prints the same token at every start until it is used. The token is stored encrypted with the master key; a new master key yields a new token.

  2. Open <EDGEWEIR_PUBLIC_URL>/setup and enter the setup token, name, email, and password to create the console account. The console also creates the default cluster default. Setup wizard: quick start.

Setup requests without a token or with a wrong token are rejected and written to the audit log. The token expires after a successful setup.

7. Verify

curl -s http://127.0.0.1:3000/healthz
docker compose ps

Expected: {"status":"ok","version":"<image tag>"}; console is healthy.

Optional components

ProfileComponentEnable
analyticsClickHouse: raw access logs and per-minute statisticsSet COMPOSE_PROFILES=analytics, EDGEWEIR_ANALYTICS=clickhouse, and CLICKHOUSE_PASSWORD in .env
.env
COMPOSE_PROFILES=analytics
EDGEWEIR_ANALYTICS=clickhouse
CLICKHOUSE_PASSWORD=<password>
docker compose up -d

Compose reads COMPOSE_PROFILES from .env, so every later docker compose command (up, pull, logs, down) includes ClickHouse without --profile analytics.

Console charts and alerts use PostgreSQL rollups. Access-log sampling is off by default and is enabled on a site's logs page; raw logs are retained for 7 days. Switching the storage mode does not migrate history. Details: access logs and AccessKeys.

Build from source

git clone https://github.com/marvinli001/edgeweir.git /opt/edgeweir
cd /opt/edgeweir
docker compose up -d --build

The remaining steps are the same. The locally built image takes the same tag and reports version dev; run docker compose pull before returning to a published image.

Without Compose: standalone containers

compose.yml is equivalent to two containers on a dedicated network. With an existing PostgreSQL 18, omit edgeweir-postgres and point DATABASE_URL at that database.

cd /opt/edgeweir
umask 077
POSTGRES_PASSWORD=$(openssl rand -hex 24)
cat > console.env <<ENV
DATABASE_URL=postgres://edgeweir:${POSTGRES_PASSWORD}@edgeweir-postgres:5432/edgeweir
EDGEWEIR_MASTER_KEY=$(openssl rand -base64 32)
EDGEWEIR_PUBLIC_URL=https://cdn-admin.example.com
EDGEWEIR_NODE_API_URL=https://cdn-admin.example.com:8443
ENV

docker network create edgeweir
docker run -d --name edgeweir-postgres --network edgeweir --restart unless-stopped \
  -e POSTGRES_USER=edgeweir -e POSTGRES_DB=edgeweir -e POSTGRES_PASSWORD="$POSTGRES_PASSWORD" \
  -v edgeweir-postgres:/var/lib/postgresql \
  postgres:18.6-alpine@sha256:77f585114c32fbca283dc835b0596f4e52b51b4c6662d7810b2f4084f60a1873
docker run -d --name edgeweir-console --network edgeweir --restart unless-stopped \
  --env-file console.env -e ROLE=all \
  -p 127.0.0.1:3000:3000 -p 8443:8443 \
  --read-only --tmpfs /tmp --security-opt no-new-privileges:true \
  ghcr.io/marvinli001/edgeweir:20260929-a1b2c3d
ItemConstraint
console.envSame variables as the environment of compose.yml; add optional variables as needed. Deployments that set BETTER_AUTH_SECRET keep its value.
EDGEWEIR_VERSIONNot in console.env: inside the image it carries the running version. The tag in the image reference selects the version.
DATABASE_URL127.0.0.1 inside the container is the container itself, not the host.
PostgreSQL volumeMount at /var/lib/postgresql: PostgreSQL 18 images keep data under /var/lib/postgresql/<major>/docker.
Hardening flags--read-only, --tmpfs /tmp, and --security-opt no-new-privileges:true match compose.yml.

Upgrading standalone containers: upgrade.

Troubleshooting

SymptomCauseAction
Log invalid configuration: followed by variable namesVariable missing or malformedFix the listed variables in .env, then run docker compose up -d.
EDGEWEIR_MASTER_KEY: is not valid base64 or must be at least 32 bytesMaster key truncated or edited, for example a panel turned + into a space, or the value is quotedUse the unmodified output of openssl rand -base64 32.
EDGEWEIR_MASTER_KEY does not match this databaseThe master key is not the one this database uses: the key changed, or the database comes from another installationRestore the original master key (the original .env or its offline copy), or set it as EDGEWEIR_MASTER_KEY_PREVIOUS to rotate it; setting BETTER_AUTH_SECRET does not help.
BETTER_AUTH_SECRET is not set, but this database was used with another secretBETTER_AUTH_SECRET removed from an existing deploymentRestore the previous value.
database not reachable yet repeats, exit after 60 secondsDatabase unreachableCheck the database container with docker compose ps postgres; for an external database, check DATABASE_URL.
Sign-in fails, or the origin is reported as untrustedScheme, host, or port of EDGEWEIR_PUBLIC_URL differs from the browser addressCorrect EDGEWEIR_PUBLIC_URL, then run docker compose up -d.
console is unhealthy/healthz does not answerdocker compose logs console.
Node enrollment or connection fails—See adding nodes: troubleshooting.
Edit on GitHub

On this page