Skip to main content

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

ResourceMinimumRecommended (small team, ~50 users)
vCPU24
RAM4 GB8 GB
Disk40 GB SSD100 GB SSD
Network100 Mbps1 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 guideapt-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_HOST to your real hostnames.
  • Set COOKIE_SECURE=true and COOKIE_SAME_SITE=strict.
  • Set ALLOW_LOCALHOST_REDIRECTS=false.
  • Set OPERATOR_COOKIE_DOMAIN and CLIENT_COOKIE_DOMAIN to non-overlapping hosts — see Cookie hardening.
  • (Optional) Set LOG_LEVEL=info and PRETTY_LOGS=false for 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 with ORKESTRA_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_URI and REDIS_URL to your managed-DB connection strings; remove the mongodb and redis services from docker-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.