Skip to main content

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.

StagePurpose
baseNode.js + pnpm
depsInstall dependencies (cached layer)
builderBuild Next.js app
runnerMinimal 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_tsdb to check the correct database, not the default user DB.
  • API Config: The image includes default.toml, production.toml, and staging.toml. Set APP_ENV=production to load production overrides.
  • Web Runtime Config: Use APOLLON_* env vars (not NEXT_PUBLIC_*). Pages use force-dynamic so 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=true when behind nginx/Caddy.
  • Storage Volume: The storage_data volume persists uploaded files (sounds etc.). The API runs as user apollon (UID 1001) — ensure the volume has correct permissions: chown -R 1001:1001 /path/to/storage or use a named volume (Docker handles permissions automatically).
  • Watchlist: Requires both APOLLON__AIS__ENABLED=true and APOLLON__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)

ServiceInternal PortExternal Port
PostgreSQL54325432
TimescaleDB54325433