Docker
Runtime Base Image
File: docker/runtime-base.Dockerfile
A shared base image for all Rust service containers, built and pushed separately.
FROM debian:trixie-slim
Includes:
ca-certificates,openssl,tini,curl,tzdata- Timezone set to
Europe/Berlin - Non-root user
apollon(UID 1001, GID 1001, no login shell)
This image is built nightly and on changes to the Dockerfile via the build-runtime-base.yml workflow.
Registry: ghcr.io/{owner}/apollon/runtime-base:latest
API Image (Bazel)
The API image is built via Bazel, not a Dockerfile. It layers the compiled binary and config files on top of the runtime base image.
Contents:
/usr/local/bin/apollon-api— API binary/app/config/default.toml— Base configuration/app/config/production.toml— Production overrides/app/config/staging.toml— Staging overrides/app/data/storage/— File storage directory (sounds, uploads)- Entrypoint:
tini -- apollon-api - Non-root user
apollon(UID 1001) - Working directory:
/app
Volume: The /app/data/storage directory must be mounted as a persistent volume. Uploaded files (sounds etc.) are stored here. Without a volume mount, uploads are lost on container restart.
Registry: ghcr.io/{owner}/apollon/api:{tag}
Web Image (Docker)
File: docker/web.Dockerfile
Multi-stage build for the Next.js web app using standalone output.
| Stage | Purpose |
|---|---|
base | Node.js + pnpm |
deps | Install dependencies (cached layer) |
builder | Build Next.js app |
runner | Minimal production image with standalone output |
Configuration:
- Non-root user
apollon - Port:
3000 - Timezone:
Europe/Berlin - Healthcheck on
/api/health
Registry: ghcr.io/{owner}/apollon/web:{tag}
Production Deployment
Example Docker Compose for production deployment:
networks:
apollon:
driver: bridge
services:
postgres:
image: postgres:18-alpine3.23
environment:
- POSTGRES_USER=apollon
- POSTGRES_PASSWORD=your-secure-password
- POSTGRES_DB=apollon
volumes:
- postgres_data:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U apollon"]
interval: 5s
timeout: 5s
retries: 5
timescaledb:
image: timescale/timescaledb-ha:pg18
environment:
- POSTGRES_USER=apollon
- POSTGRES_PASSWORD=your-secure-password
- POSTGRES_DB=apollon_tsdb
- TS_TUNE_MEMORY=2GB
- TS_TUNE_NUM_CPUS=2
volumes:
- timescaledb_data:/home/postgres/pgdata
healthcheck:
test: ["CMD-SHELL", "pg_isready -U apollon -d apollon_tsdb"]
interval: 5s
timeout: 5s
retries: 5
api:
image: ghcr.io/{owner}/apollon/api:latest
restart: unless-stopped
environment:
- APP_ENV=production
- APOLLON__SERVER__HOST=0.0.0.0
- APOLLON__SERVER__PORT=3000
- APOLLON__SERVER__TRUST_PROXY=true
- APOLLON__DATABASE__URL=postgresql://apollon:your-secure-password@postgres:5432/apollon
- APOLLON__TIMESCALE__URL=postgresql://apollon:your-secure-password@timescaledb:5432/apollon_tsdb
- APOLLON__PEPLINK__ROUTER_IP=192.168.50.1
- APOLLON__PEPLINK__USERNAME=admin
- APOLLON__PEPLINK__PASSWORD=your-router-password
- APOLLON__CORS__ALLOWED_ORIGINS=https://your-domain.com
- APOLLON__AUTH__API_KEYS=your-api-key
- APOLLON__WEBSOCKET__TOKENS=your-ws-token
- APOLLON__DOCS__SWAGGER_UI=false
- APOLLON__DOCS__OPENAPI_JSON=false
- APOLLON__DOCS__GRAPHIQL=false
- APOLLON__AIS__ENABLED=true
- APOLLON__AIS__IGNORE_MMSI=211581120
- APOLLON__AIS__AISSTREAM__ENABLED=true
- APOLLON__AIS__AISSTREAM__API_KEY=your-aisstream-api-key
- APOLLON__AIS__AISSTREAM__RADIUS_KM=10
# Add the local receiver (e.g. Quark-elec A027+) — enable alongside aisstream for dual mode
# (local priority in antenna range, aisstream secondary), or on its own:
# - APOLLON__AIS__LOCAL__ENABLED=true
# - APOLLON__AIS__LOCAL__HOST=192.168.1.69
# - APOLLON__AIS__LOCAL__PORT=2000
- APOLLON__PEGELONLINE__ENABLED=true
- APOLLON__PEGELONLINE__RADIUS_KM=30
- APOLLON__STORAGE__PATH=/app/data/storage
- APOLLON__STORAGE__MAX_FILE_SIZE_MB=10
- APOLLON__WATCHLIST__ENABLED=true
- APOLLON__LOGGING__LEVEL=info
- APOLLON__LOGGING__FORMAT=json
volumes:
- storage_data:/app/data/storage
ports:
- "3000:3000"
networks:
- apollon
depends_on:
- postgres
- timescaledb
web:
image: ghcr.io/{owner}/apollon/web:latest
restart: unless-stopped
environment:
- NODE_ENV=production
- PORT=3000
- HOSTNAME=0.0.0.0
- APOLLON_API_URL=http://your-server:3000
- APOLLON_WS_URL=ws://your-server:3000
- APOLLON_WS_TOKEN=your-ws-token
- APOLLON_API_KEY=your-api-key
ports:
- "4000:3000"
networks:
- apollon
depends_on:
- api
volumes:
postgres_data:
timescaledb_data:
storage_data:
Important Notes
- Secrets: Never commit passwords, API keys, or tokens. Use environment variables or a secrets manager.
- TimescaleDB Healthcheck: Use
-d apollon_tsdbto check the correct database, not the default user DB. - API Config: The image includes
default.toml,production.toml, andstaging.toml. SetAPP_ENV=productionto load production overrides. - Web Runtime Config: Use
APOLLON_*env vars (notNEXT_PUBLIC_*). Pages useforce-dynamicso env vars are read at runtime, not embedded at build time. - Networking: All services should be on the same Docker network. The API connects to databases by container name.
- Reverse Proxy: Set
APOLLON__SERVER__TRUST_PROXY=truewhen behind nginx/Caddy. - Storage Volume: The
storage_datavolume persists uploaded files (sounds etc.). The API runs as userapollon(UID 1001) — ensure the volume has correct permissions:chown -R 1001:1001 /path/to/storageor use a named volume (Docker handles permissions automatically). - Watchlist: Requires both
APOLLON__AIS__ENABLED=trueandAPOLLON__WATCHLIST__ENABLED=true. Alerts only trigger when both own ship and tracked vessel are moving.
Dev Stack (Databases Only)
File: dev-stack/db.docker-compose.yml
For local development, only the databases run in Docker. The API and Web app run natively.
# Start databases
just db-up
# Stop
just db-down
Port Mapping (Dev)
| Service | Internal Port | External Port |
|---|---|---|
| PostgreSQL | 5432 | 5432 |
| TimescaleDB | 5432 | 5433 |