Docker

The shortest path to a running instance. One Compose project holds PostgreSQL, the backend and the frontend, with named volumes for everything that must outlive a container.

Prerequisites

  • Docker Engine with the Compose plugin (docker compose version must answer).
  • Two host names pointing at the machine, and a reverse proxy holding the certificate.
  • The four secrets from the shared prerequisites.

The Compose file

This is the shape the project's own deploy/compose/remote.compose.yml uses, reduced to what a first installation needs. Save it as compose.yml.

services:

  front:
    container_name: aetheus-front
    image: aetheus-front:latest
    # Loopback only: the reverse proxy is the sole entry point. Publishing on 0.0.0.0 would
    # expose the app in cleartext, and Docker's iptables rules bypass a host firewall.
    ports:
      - "127.0.0.1:10025:8080"
    environment:
      - API_BASE_URL=https://api.example.com
      - APP_VERSION=1.0.0
    depends_on:
      back:
        condition: service_healthy
    mem_limit: 256m
    restart: unless-stopped

  back:
    container_name: aetheus-back
    image: aetheus-back:latest
    ports:
      - "127.0.0.1:10026:8080"
    environment:
      - ASPNETCORE_ENVIRONMENT=Production
      # The entrypoint owns migration execution, so the application must not run them again.
      - Database__SkipMigrations=true
      - AETHEUS_RUN_MIGRATIONS=true
      - DataProtection__KeyPath=/app/data/dp-keys
      - ConnectionStrings__Default=Host=aetheus-database;Port=5432;Database=aetheus;Username=aetheus;Password=${DB_PASSWORD}
      - Auth__JwtKey=${JWT_KEY}
      - Auth__AdminPassword=${ADMIN_PASSWORD}
      - Auth__EncryptionKey=${ENCRYPTION_KEY}
      - Auth__EncryptionSalt=${ENCRYPTION_SALT}
      - Aetheus__PublicApiBaseUrl=https://api.example.com
      - Cors__Origins__0=https://app.example.com
    volumes:
      - git-repos:/app/data/git-repos
      - dp-keys:/app/data/dp-keys
      - artifacts:/app/data/artifacts
      - packages:/app/data/packages
    depends_on:
      database:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-sf", "http://127.0.0.1:8080/health/live"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 120s
    mem_limit: 1g
    networks:
      - default
      - backend
    restart: unless-stopped

  database:
    container_name: aetheus-database
    image: postgres:18-alpine
    environment:
      - POSTGRES_DB=aetheus
      - POSTGRES_USER=aetheus
      - POSTGRES_PASSWORD=${DB_PASSWORD}
      - PGDATA=/var/lib/postgresql/data
    volumes:
      - db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U aetheus -d aetheus"]
      interval: 5s
      timeout: 3s
      retries: 10
    # Not on the default network: the database is reachable by the backend and by nothing else.
    networks:
      - backend
    restart: unless-stopped

networks:
  backend:
    driver: bridge

volumes:
  db-data:
  git-repos:
  dp-keys:
  artifacts:
  packages:

The .env file

Compose reads .env from the same directory. Keep it out of version control and readable only by the account that runs Docker.

DB_PASSWORD=...
JWT_KEY=...
ADMIN_PASSWORD=...
ENCRYPTION_KEY=...
ENCRYPTION_SALT=...
chmod 600 .env

Getting the images

Build them from a checkout of the repository. Both Dockerfiles expect the repository root as their build context.

docker build \
    --file deploy/docker/Dockerfile.back \
    --build-arg APP_VERSION=1.0.0 \
    --tag aetheus-back:latest .

docker build \
    --file deploy/docker/Dockerfile.front \
    --tag aetheus-front:latest .

The frontend image is a small static server that rewrites wwwroot/appsettings.json at startup from API_BASE_URL. That is why the frontend's API address is an environment variable here, while the binaries route has to edit the file by hand.

First start

docker compose up -d
docker compose logs -f back

On a fresh database the backend entrypoint applies the migrations before the application starts, so the first start takes longer than later ones. That is what the 120-second start_period on the healthcheck accounts for.

When the backend is healthy:

curl -sf http://127.0.0.1:10026/health/ready

Then point your reverse proxy at 127.0.0.1:10025 for the app host and 127.0.0.1:10026 for the API host, and sign in as admin.

The volumes, and why they matter

VolumeHolds
db-dataPostgreSQL. Everything.
git-reposInternally hosted Git repositories, including pipeline definitions.
artifactsBuild artefacts, and therefore the ability to redeploy or roll back.
dp-keysData-protection keys. Losing them invalidates sessions and protected payloads.
packagesPackages served by the internal package registry.

A docker compose down keeps named volumes. A docker compose down -v deletes them, and there is no undo.

Upgrading

  1. Back up the database and the volumes.

    docker compose exec -T database \
        pg_dump -U aetheus aetheus > aetheus-$(date +%Y%m%dT%H%M%SZ).sql
  2. Build or pull the new images and tag them.

  3. Recreate the services. Migrations run in the backend entrypoint as it comes up.

    docker compose up -d
  4. Confirm readiness before sending traffic back.

    curl -sf http://127.0.0.1:10026/health/ready

Zero-downtime upgrades

The layout above restarts the containers in place, so there is a short outage. The repository also carries a blue-green Compose file, deploy/compose/remote-bluegreen.compose.yml, where two coloured stacks share one database and the reverse proxy switches between them. That is how Aetheus deploys itself, and it is what the pipeline route drives.