Skip to content

Deploying with Docker

Ion Drive ships a multi-stage production Dockerfile and Compose files for both the app’s PostgreSQL dependency and an optional observability stack.

In development you typically run the app locally (pnpm dev) and only need Postgres in a container:

Terminal window
docker compose -f docker/docker-compose.yml up -d
# Postgres on localhost:5432 (user: ion, password: ion, db: ion_drive)

The Compose file also contains a commented-out ion-drive service — uncomment it to run the whole app in Docker instead of locally.

The docker/Dockerfile is a two-stage build (build → slim runtime), runs as a non-root user, and has a /health healthcheck:

Terminal window
# Build from the repo root
docker build -f docker/Dockerfile -t ion-drive/core:latest .
# Run it
docker run -d --name ion-drive \
-p 3000:3000 \
-e ION_DATABASE_URL='postgresql://ion:ion@your-db-host:5432/ion_drive' \
-e ION_ENCRYPTION_KEY="$(openssl rand -hex 32)" \
-e ION_AUTH_SECRET="$(openssl rand -hex 32)" \
-e ION_REQUIRE_AUTH=true \
ion-drive/core:latest

The image serves the API on port 3000 and the built admin console at /admin (the root / redirects there). Open http://localhost:3000 and sign up — the first user becomes admin.

All configuration is via environment variables (validated at boot). The most important ones:

VariableDefaultPurpose
ION_PORT3000HTTP port.
ION_DATABASE_URLpostgresql://ion:ion@localhost:5432/ion_driveSystem DB connection.
ION_ENCRYPTION_KEY(dev key)32-byte hex; set this in production (encrypts secrets).
ION_AUTH_SECRETfalls back to encryption keySigns auth sessions/tokens.
ION_REQUIRE_AUTHfalseEnforce RBAC on data/schema/admin endpoints.
ION_DISABLE_SIGNUPfalseClose public signup once the first admin exists.
ION_PUBLIC_URLPublic base URL (used as the auth base URL).
ION_CORS_ORIGINStrueAllowed CORS origins.
ION_TRUST_PROXYfalseHonor X-Forwarded-* behind a reverse proxy (true, hop count, or CIDR list).
ION_ADMIN_ENABLEDtrueServe the admin console at /admin.
ION_RATE_LIMIT_ENABLEDtruePer-IP HTTP rate limiting (429 beyond the limits below).
ION_RATE_LIMIT_MAX300Max requests per IP per window (global bucket).
ION_RATE_LIMIT_WINDOW_MS60000Rate-limit window in milliseconds.
ION_RATE_LIMIT_AUTH_MAX20Stricter per-IP limit for /api/auth/* in the same window.
ION_LOG_LEVELinfofataltrace.
ION_METRICS_ENABLEDtruePrometheus endpoint at /metrics.
ION_METRICS_TOKENWhen set, /metrics requires this bearer token.
ION_OTEL_ENABLEDfalseExport traces/logs (+ optionally metrics) over OTLP/HTTP.
ION_OTEL_EXPORTER_OTLP_ENDPOINThttp://localhost:4318OTLP/HTTP collector.
ION_TASKS_ENABLEDtrueScheduled task engine.
ION_BLOCKS_ENABLEDtrueBuilding-block install surface.

Production checklist: always set ION_ENCRYPTION_KEY and ION_AUTH_SECRET to strong random values, enable ION_REQUIRE_AUTH, and put the server behind TLS. The default dev encryption key is insecure by design and logs a warning. The full hardening pass is in the Security Checklist.

An optional overlay brings up Grafana, Loki, Prometheus, and Tempo, pre-configured for Ion Drive (datasources, a Prometheus scrape job, and a starter dashboard are provisioned from docker/{grafana,prometheus,loki,tempo}/):

Terminal window
docker compose \
-f docker/docker-compose.yml \
-f docker/docker-compose.observability.yml up -d

What you get:

ServiceURL (host)Notes
Grafanahttp://localhost:3100Login admin/admin. The Ion Drive Overview dashboard (request rate/latency/errors, schema changes, tasks, events) is in the Ion Drive folder.
Prometheushttp://localhost:9090Scrapes the server’s /metrics every 15s.
Tempohttp://localhost:3200 (API), :4317/:4318 (OTLP)Trace storage; query it through Grafana.
Lokihttp://localhost:3101Log storage; query it through Grafana.

Metrics need no server config — /metrics is on by default (ION_METRICS_ENABLED=true) and Prometheus scrapes it at host.docker.internal:3000, i.e. it assumes the server runs on the host (pnpm dev). If you run Ion Drive inside Compose instead, change the target in docker/prometheus/prometheus.yml to ion-drive:3000.

Traces need the server to export OTLP to Tempo:

Terminal window
ION_OTEL_ENABLED=true
ION_OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318

Logs: the server can export logs over OTLP (ION_OTEL_LOGS_ENABLED=true, default off), but it uses a single OTLP endpoint for traces and logs — and Tempo only accepts traces. To land logs in Loki, put an OTel Collector between the server and the backends to fan the signals out (Loki’s native OTLP ingest is at http://localhost:3101/otlp). The Loki datasource is provisioned and ready for that setup.

See ADR-012 for the telemetry design.

  • GET /health — liveness/readiness (also the container HEALTHCHECK). Returns status, version, schema version, and object count.
  • GET /metrics — Prometheus text exposition (when ION_METRICS_ENABLED).
  • Each server instance serves one database (ION_DATABASE_URL). For tenant isolation today, run one instance per tenant with its own database; built-in tenant provisioning/routing is on the roadmap (Phase 16).
  • Run database backups against Postgres as usual — all state (schema metadata, data, secrets, tasks, blocks ledger) lives there. Recipes and the restore procedure are in Backup & Restore.
  • Running the image on Kubernetes (probes, replica caveats, metrics scraping) is covered in Deploying on Kubernetes.