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 versionmust 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, in a POSIX shell at the repository root. Both Dockerfiles expect the repository root as their build context, and neither compiles the application: they copy a publish made on the host into .pipeline-publish/ (Dockerfile.back copies .pipeline-publish/backend, Dockerfile.front copies .pipeline-publish/static-server and .pipeline-publish/frontend/wwwroot). On a fresh clone that directory does not exist and a bare docker build fails at that COPY, so publish first, with the script the project's own pipelines use:
DOTNET="$(sh deploy/scripts/ensure-dotnet-sdk.sh)"
DOTNET="$DOTNET" APP_VERSION=1.0.0 \
sh deploy/scripts/publish-application.sh .pipeline-publish
ensure-dotnet-sdk.sh prints the path of the .NET SDK whose version global.json declares, and installs that exact version under ~/.aetheus/dotnet when the host does not already resolve it (it needs curl, bash and sha256sum). publish-application.sh installs the wasm-tools workload when it is missing and refuses a frontend published without the WebAssembly optimizer. Then build the images:
SOURCE_COMMIT="$(git rev-parse HEAD)"
docker build \
--file deploy/docker/Dockerfile.back \
--build-arg APP_VERSION=1.0.0 \
--build-arg SOURCE_COMMIT="$SOURCE_COMMIT" \
--tag aetheus-back:latest .
docker build \
--file deploy/docker/Dockerfile.front \
--tag aetheus-front:latest .
The backend image still builds the EF migrations bundle and the two agent packages inside Docker, and stamps the agent release manifest with SOURCE_COMMIT, which must be a full commit hash: without it that stage fails.
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
| Volume | Holds |
|---|---|
db-data | PostgreSQL. Everything. |
git-repos | Internally hosted Git repositories, including pipeline definitions. |
artifacts | Build artefacts, and therefore the ability to redeploy or roll back. |
dp-keys | Data-protection keys. Losing them invalidates sessions and protected payloads. |
packages | Packages 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
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).sqlPublish and build the new images as in Getting the images, or pull them, and tag them.
Recreate the services. Migrations run in the backend entrypoint as it comes up.
docker compose up -dConfirm 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.