Single VM with Docker Compose
This is the simplest production-shape deployment of Orkestra: one VM (Ubuntu 24.04 or Debian 12), Docker Compose, the docker-compose.prod.yml stack, and a systemd unit that keeps the stack alive across reboots. No Kubernetes, no load balancer, no remote DB — everything on one host.
It's the right shape for:
- Solo operators self-hosting Orkestra for their own use
- Internal-tools deployments where a single VM is enough
- Pre-production environments before a multi-replica K8s deployment
For TLS termination and the per-audience host split (console.* + api.*), add the Caddy reverse-proxy from the next guide.
Sizing
| Resource | Minimum | Recommended (small team, ~50 users) |
|---|---|---|
| vCPU | 2 | 4 |
| RAM | 4 GB | 8 GB |
| Disk | 40 GB SSD | 100 GB SSD |
| Network | 100 Mbps | 1 Gbps |
The base (7 core modules) fits comfortably in the minimum sizing. A fork that adds heavy verticals (AI, graph, document rendering, etc.) should add headroom per workload — budget 4 GB per memory-hungry addon and account for any addon-managed infra containers it runs.
Step 1 — Prepare the VM
# As root or via sudo
apt-get update && apt-get install -y \
ca-certificates curl gnupg lsb-release git \
unattended-upgrades
Install Docker per the official guide — apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin. Add your deploy user to the docker group and re-login.
Configure unattended security upgrades:
dpkg-reconfigure unattended-upgrades # Yes
Step 2 — Clone and init
As your deploy user (NOT root):
cd /opt
sudo mkdir orkestra && sudo chown $USER:$USER orkestra
cd orkestra
git clone https://github.com/<your-org>/orkestra.git .
make init
make init will scaffold docker/.env (with random secrets), generate RS256 JWT keys, and create the orkestra-network Docker bridge. See I just forked Orkestra for what each step does.
Edit docker/.env:
- Set
ENV=production. - Set
BACKEND_URL,FRONTEND_URL,OPERATOR_FRONTEND_URL,CLIENT_FRONTEND_URL,CONSOLE_HOST,CLIENT_API_HOSTto your real hostnames. - Set
COOKIE_SECURE=trueandCOOKIE_SAME_SITE=strict. - Set
ALLOW_LOCALHOST_REDIRECTS=false. - Set
OPERATOR_COOKIE_DOMAINandCLIENT_COOKIE_DOMAINto non-overlapping hosts — see Cookie hardening. - (Optional) Set
LOG_LEVEL=infoandPRETTY_LOGS=falsefor JSON logs that ship to Loki / Datadog / Honeycomb cleanly.
Step 3 — Start the stack
cd /opt/orkestra/docker
docker compose -f docker-compose.infra.yml up -d
docker compose -f docker-compose.prod.yml --env-file .env up -d
Orkestra ships as a single image, so there's only one production compose file to launch.
Verify:
docker compose -f docker-compose.prod.yml --env-file .env ps
curl -s http://localhost:3000/health
Step 4 — systemd unit for auto-restart on reboot
Create /etc/systemd/system/orkestra.service:
[Unit]
Description=Orkestra stack
Requires=docker.service
After=docker.service network-online.target
Wants=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/orkestra/docker
User=deploy
Group=docker
EnvironmentFile=/opt/orkestra/docker/.env
ExecStart=/usr/bin/docker compose -f docker-compose.infra.yml -f docker-compose.prod.yml --env-file .env up -d
ExecStop=/usr/bin/docker compose -f docker-compose.infra.yml -f docker-compose.prod.yml --env-file .env down
TimeoutStartSec=300
[Install]
WantedBy=multi-user.target
Replace deploy with your actual deploy user. Then:
sudo systemctl daemon-reload
sudo systemctl enable orkestra.service
sudo systemctl start orkestra.service
sudo systemctl status orkestra.service
On VM reboot, Docker comes up, then orkestra.service brings the stack up automatically.
Step 5 — First admin login
The core dev-token issuer (POST /dev/token, used by scripts/devtoken.sh) is auto-disabled when ENV=production — so on a real production VM you can't mint an admin token. Create the first administrator through the setup wizard instead:
# The setup endpoints (/v1/setup/*) bootstrap the first admin on a fresh install.
curl -fsS http://localhost:3000/v1/setup/status | jq .
Follow the wizard (or its UI counterpart) to create the first administrator account, then log into /admin/users to manage the rest.
For a non-production VM (
ENV=development/staging), you can instead mint a one-time token withORKESTRA_API_URL=http://localhost:3000 ./scripts/devtoken.sh administrator— but never leave a non-production env exposed publicly.
Step 6 — Log rotation
Docker's default JSON log driver grows unbounded. Configure rotation in /etc/docker/daemon.json:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "100m",
"max-file": "5"
}
}
Restart the daemon (systemctl restart docker) — existing containers keep their old config until next recreate, so re-run docker compose up -d.
For structured JSON logs shipping straight to Loki, use the observability stack:
docker compose -f docker-compose.observability.yml --env-file .env up -d
Promtail tails Docker stdout, JSON-parses, ships to Loki. Grafana is at http://localhost:3010 (admin/admin by default — change immediately).
Step 7 — Backups
See Backup and restore for mongodump cron recipes and restore-test cadence. Redis is ephemeral (caches and sessions) — losing it logs everyone out but doesn't lose data.
Step 8 — Smoke tests
After every deploy and after every restart, run:
# Backend health (process + DB connectivity)
curl -fsS http://localhost:3000/health | jq .
# Admin modules list (should return JSON)
TOKEN=$(./scripts/devtoken.sh administrator --quiet)
curl -fsS -H "Authorization: Bearer $TOKEN" http://localhost:3000/v1/admin/modules | jq '.modules | length'
# OpenAPI spec count
curl -fsS http://localhost:3000/openapi.json | jq '.paths | keys | length'
If any of these fail, check docker compose -f docker-compose.prod.yml --env-file .env logs backend --tail 100.
Upgrade procedure
cd /opt/orkestra
git pull
docker compose -f docker/docker-compose.prod.yml --env-file docker/.env pull
sudo systemctl restart orkestra.service
The infra services (Mongo, Redis) are pinned to specific image versions in the compose file — they don't update on pull. To upgrade them, edit the compose file, then docker compose up -d.
Mongo upgrades across major versions need attention — read the Mongo upgrade docs before bumping. Orkestra ships with Mongo 8.0; the schema is forward-compatible to whatever 8.x lands.
What's missing
This guide is intentionally single-VM. For multi-host setups:
- Multi-replica backend — read the K8s overview; the Go binary is stateless, so horizontal scaling works as long as sticky sessions or shared Redis sessions are configured.
- Managed databases — set
MONGO_URIandREDIS_URLto your managed-DB connection strings; remove themongodbandredisservices fromdocker-compose.infra.yml. - Object storage — the base ships RustFS (in
docker-compose.infra.yml) for local object storage. A fork that needs S3-compatible storage or PDF rendering wires that at the application layer in its own addon.