# Deployment Architecture
## Docker Build
```mermaid
flowchart LR
subgraph Build[Multi-Stage Docker Build]
BASE["base
node:22-alpine
npm ci --production"]
BUILD["build
node:22-alpine
npm ci + npm run build"]
RUNTIME["runtime
node:22-alpine (slim)
dist/ + node_modules"]
end
subgraph Artifacts[Build Artifacts]
DIST["dist/
compiled JS + JSON assets"]
WEB["dist/web/
built SPA (dashboard)"]
PROD_DEPS["node_modules/
production only"]
end
subgraph Config[Runtime Configuration]
ENV[".env file
or environment variables"]
DATA["data/
keywords.json
comps.csv (optional)"]
end
subgraph Storage[Data Storage]
SQLITE[("/data/dominus.db
SQLite file")]
end
BASE --> BUILD
BUILD --> RUNTIME
RUNTIME --> DIST
RUNTIME --> WEB
RUNTIME --> PROD_DEPS
RUNTIME --> ENV
RUNTIME --> DATA
RUNTIME --> SQLITE
style Build fill:#16213e,stroke:#0f3460,color:#eee
style Artifacts fill:#0f3460,stroke:#533483,color:#eee
style Config fill:#1a1a2e,stroke:#16213e,color:#eee
style Storage fill:#2d1b69,stroke:#533483,color:#eee
```
`scripts/finalize-dist.mjs` runs after `tsc` to make the build output
complete across every distribution channel: it copies runtime JSON assets
(`tsc` drops non-`.ts` files), stages the compiled SPA at `dist/web/`, and
prunes the compiled test suites.
## Docker Compose (Community Edition)
```mermaid
flowchart LR
INTERNET["Internet / LAN"]
UNBOUND["unbound sidecar
:5300 DNS (DNSSEC)"]
DOMINUS["dominus-api
:3000 (API + dashboard)"]
WORKER["dominus-worker
job queue consumer"]
SCHED["dominus-scheduler
cron jobs"]
VOLUME[("dominus-data
Docker volume
/data/*.db")]
INTERNET --> DOMINUS
DOMINUS --> UNBOUND
WORKER --> UNBOUND
WORKER --> VOLUME
SCHED --> VOLUME
DOMINUS --> VOLUME
style INTERNET fill:#1a1a2e,stroke:#16213e,color:#eee
style DOMINUS fill:#0f3460,stroke:#533483,color:#eee
style UNBOUND fill:#16213e,stroke:#0f3460,color:#eee
style WORKER fill:#2d1b69,stroke:#533483,color:#eee
style SCHED fill:#2d1b69,stroke:#533483,color:#eee
style VOLUME fill:#2d1b69,stroke:#533483,color:#eee
```
```bash
# Community edition: SQLite + Unbound validating resolver
docker compose -f docker-compose.yml -f docker-compose.unbound.yml up -d
```
The Unbound overlay is not optional if you want "available" verdicts:
without a validating resolver the DNS stage degrades and never reports a
domain as available (ADR-0072, ADR-0076).
## Docker Compose (DOMINUS Cloud)
```mermaid
flowchart TB
subgraph External[External]
USERS["Users
Browser / CLI / API"]
CADDY["Caddy reverse proxy
TLS termination"]
end
subgraph App[Application Node]
API["dominus-api
Express API + worker"]
SCHED2["dominus-scheduler"]
end
subgraph Data[Managed Data Services]
PG[("PostgreSQL
HA / automated backups")]
REDIS[("Redis
rate-limit + lock store")]
BLOB[("Object Storage
off-host backups")]
end
subgraph Monitoring[Observability]
PROM[Prometheus
metrics scrape]
AM[Alertmanager
alert routing]
GRAF[Grafana
dashboards]
end
CADDY --> API
USERS --> CADDY
API --> PG
API -.-> REDIS
API --> BLOB
API --> PROM
PROM --> AM
PROM --> GRAF
style External fill:#1a1a2e,stroke:#16213e,color:#eee
style App fill:#16213e,stroke:#0f3460,color:#eee
style Data fill:#2d1b69,stroke:#533483,color:#eee
style Monitoring fill:#0f3460,stroke:#16213e,color:#eee
```
```bash
# Cloud stack: PostgreSQL + Redis + Prometheus/Grafana + Unbound
docker compose \
-f docker-compose.yml \
-f docker-compose.unbound.yml \
-f docker-compose.prod.yml up -d
```
Infrastructure-as-code for a dedicated node lives in
[`deploy/terraform/`](../../deploy/terraform/) (Hetzner app node, PITR
database node, and Caddy).
## Resource Requirements
| Edition | CPU | RAM | Storage | Cost |
|---------|-----|-----|---------|------|
| Community (Docker, SQLite) | 0.5 core | 256 MB | 1 GB (SQLite + data files) | ~€0 (existing hardware) |
| Cloud (Compose + PGSQL) | 1 core | 1 GB | 10 GB (PostgreSQL + backups) | ~€25-50/mo |
| Enterprise (Terraform) | 2+ cores | 2+ GB | 40 GB+ NVMe + object storage | on contract |
## Port Mapping
One HTTP port per deployment; there is no dedicated metrics port.
| Port | Protocol | Purpose | Notes |
|------|----------|---------|-------|
| `PORT` (default 3000) | HTTP | REST API + dashboard | bound to `127.0.0.1` by default in compose; expose deliberately |
| `PORT` (same) | HTTP | Prometheus metrics at `/api/v1/metrics/*` | optionally bearer-protected with `METRICS_TOKEN` |
| 5300 | TCP/UDP | Unbound resolver | internal compose network only |
| 9090 | HTTP | worker/scheduler healthcheck | loopback only, never exposed |