Home
Documentation

Deploying agent-orchestrator on a VPS

Complete deployment guide, from a bare server to the first real pipeline test.

Requirements

  • A GitLab account (gitlab.com or self-hosted) with a project you want to point the orchestrator at
  • An OpenAI API key
  • A VPS running Ubuntu 24.04 or newer, with root access
  • A domain name you control (for the webhook endpoint)

Prerequisites

DNS — Add two A records pointing to your VPS's IP:

  • your-domain.com (orchestrator webhook)
  • preview.your-domain.com (optional, manual npm run dev preview)

Verify propagation before continuing:

dig +short your-domain.com

Connecting to the server:

ssh-keygen -R <server-ip>   # clears any stale host key locally, useful if this IP served a different host before
ssh root@<server-ip>

Step 1 — Base packages + firewall

sudo apt update
sudo apt install -y git curl gnupg

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw enable
sudo ufw status verbose

⚠️ allow OpenSSH must come before enable, or you lose SSH access.


Step 2 — Install Caddy

Not in Ubuntu's default repositories — it needs its own APT repo:

sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo chmod o+r /usr/share/keyrings/caddy-stable-archive-keyring.gpg
sudo chmod o+r /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install -y caddy
caddy version

Step 3 — Dedicated system user

sudo useradd -r -m -d /home/agent-orchestrator -s /usr/sbin/nologin agent-orchestrator

-r (system account), -m (creates the home directory uv/OpenCode install into), -d (fixes the path the rest of this guide depends on), -s /usr/sbin/nologin (blocks interactive login — sudo -u agent-orchestrator <command> still works, since it executes directly without going through a login shell).

This isolates the blast radius of a compromised token or a misbehaving agent — a real account, usable via sudo -u, but with no exploitable shell session. One side effect worth knowing upfront: this same nologin setting prevents ~/.bashrc from ever being sourced, by systemd or by sudo -u — anything that activates via a .bashrc line (like nvm, below) needs an explicit workaround.


Step 4 — Install uv and OpenCode for this user

sudo -H -u agent-orchestrator bash -c 'curl -LsSf https://astral.sh/uv/install.sh | sh'
sudo -H -u agent-orchestrator bash -c 'curl -fsSL https://opencode.ai/install | bash'

# Symlink OpenCode to /usr/local/bin so systemd can find it.
# The installer places the binary in ~/.opencode/bin, which is only
# on PATH for interactive shell sessions (via .bashrc). systemd never
# reads .bashrc — without this symlink, subprocess calls to "opencode"
# would fail in production even though a manual terminal test works fine.
sudo ln -s /home/agent-orchestrator/.opencode/bin/opencode /usr/local/bin/opencode
opencode --version

Step 5 — Install Node.js for this user

Needed for OpenCode (and REVIEW_COMMANDS — lint/build) to work on a Next.js/TypeScript target project. Same trap as OpenCode above, worse: nvm activates entirely through a line added to ~/.bashrc, never sourced here. Unlike OpenCode's simple symlink, there's no shortcut — you have to explicitly inject the resolved PATH into the systemd service, in Step 11.

1. Install nvm for the agent-orchestrator user:

sudo -H -u agent-orchestrator bash -c 'curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash'

2. Install Node and set it as default:

sudo -H -u agent-orchestrator bash -c 'source ~/.nvm/nvm.sh && nvm install 24 && nvm alias default 24'

3. Get the exact binary path — keep this for Step 11:

sudo -H -u agent-orchestrator bash -c 'source ~/.nvm/nvm.sh && which node'

Expected output, something like:

/home/agent-orchestrator/.nvm/versions/node/v24.19.0/bin/node

Note the directory (.../bin), without the trailing /node.


Step 6 — Install review tools: semgrep and gitleaks

Two tools REVIEW_COMMANDS can invoke as a deterministic gate before any LLM judgment — same category as uv/OpenCode/Node: tooling for the orchestrator itself, never installed into the target project.

Semgrep, via uv tool install:

sudo -H -u agent-orchestrator bash -c '/home/agent-orchestrator/.local/bin/uv tool install semgrep'
sudo ln -s /home/agent-orchestrator/.local/bin/semgrep /usr/local/bin/semgrep
sudo -H -u agent-orchestrator semgrep --version

Gitleaks, official binary with checksum verification:

VERSION="8.30.1"
BASE="https://github.com/gitleaks/gitleaks/releases/download/v${VERSION}"
curl -fsSLO "${BASE}/gitleaks_${VERSION}_linux_x64.tar.gz"
curl -fsSLO "${BASE}/gitleaks_${VERSION}_checksums.txt"
sha256sum --check --ignore-missing gitleaks_${VERSION}_checksums.txt
tar xzf gitleaks_${VERSION}_linux_x64.tar.gz gitleaks
sudo install -m 0755 gitleaks /usr/local/bin/gitleaks
rm -f gitleaks "gitleaks_${VERSION}_linux_x64.tar.gz" "gitleaks_${VERSION}_checksums.txt"
gitleaks version

Both land in /usr/local/bin — already on systemd's default PATH, no extra config needed.


Step 7 — Clone and install

Generate a GitLab fine-grained access token first, with these permissions:

ResourcePermissions
CodePush, Read
CommitCreate
Merge RequestCreate, Read, Update
Work ItemRead, Update
NoteCreate
sudo -H -u agent-orchestrator git clone https://gitlab.com/thibault-chausson/agent-orchestrator.git /home/agent-orchestrator/agent-orchestrator
cd /home/agent-orchestrator/agent-orchestrator
sudo -H -u agent-orchestrator /home/agent-orchestrator/.local/bin/uv sync

Git identity for automated commits (required, once):

sudo -H -u agent-orchestrator git config --global user.email "agent-orchestrator@your-domain.com"
sudo -H -u agent-orchestrator git config --global user.name "Agent Orchestrator Bot"

Step 8 — Langfuse account

Create a Langfuse account and get your credentials for .env.

Wire Langfuse into OpenCode:

sudo -H -u agent-orchestrator mkdir -p /home/agent-orchestrator/.config/opencode
sudo -H -u agent-orchestrator bash -c 'cat > /home/agent-orchestrator/.config/opencode/opencode.json << "EOF"
{
  "$schema": "https://opencode.ai/config.json",
  "experimental": {
    "openTelemetry": true
  },
  "plugin": ["@langfuse/opencode-observability-plugin@latest"]
}
EOF'

Step 9 — Configure .env

sudo -H -u agent-orchestrator cp .env.example .env
sudo -u agent-orchestrator nano .env
sudo chmod 600 .env

Fill in everything except:

  • GITLAB_URL — leave blank (defaults to gitlab.com)
  • REVIEW_COMMANDS — leave blank for the first test

Step 10 — Authenticate OpenCode

Choose OpenAI as the provider during the interactive login:

sudo -H -u agent-orchestrator opencode auth login

Step 11 — Manual test before systemd

sudo -H -u agent-orchestrator /home/agent-orchestrator/agent-orchestrator/.venv/bin/uvicorn webhook.app:app --app-dir /home/agent-orchestrator/agent-orchestrator --host 127.0.0.1 --port 8000

In another terminal: curl http://127.0.0.1:8000/health → should return {"status":"ok"}. Then Ctrl+C.


Step 12 — Create the systemd service

The PATH below includes the Node directory from Step 5 — replace it with your own value.

/etc/systemd/system/agent-orchestrator.service:

[Unit]
Description=Agent Orchestrator (GitLab issue -> agents -> MR pipeline)
After=network.target

[Service]
Type=simple
User=agent-orchestrator
Environment=PATH=/home/agent-orchestrator/.nvm/versions/node/v24.19.0/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
WorkingDirectory=/home/agent-orchestrator/agent-orchestrator
EnvironmentFile=/home/agent-orchestrator/agent-orchestrator/.env
ExecStart=/home/agent-orchestrator/agent-orchestrator/.venv/bin/uvicorn webhook.app:app --host 127.0.0.1 --port 8000
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now agent-orchestrator
sudo systemctl status agent-orchestrator
journalctl -u agent-orchestrator -f

Step 13 — Expose via Caddy

/etc/caddy/Caddyfile:

your-domain.com {
    reverse_proxy /webhook/* 127.0.0.1:8000
    reverse_proxy /health 127.0.0.1:8000
}

# Optional — manual `npm run dev` preview of the target repo
preview.your-domain.com {
    reverse_proxy 127.0.0.1:5173
}
sudo systemctl reload caddy
curl https://your-domain.com/health

Step 14 — Configure the GitLab webhook

On your target repo → Settings → Webhooks:

  • URL: https://your-domain.com/webhook/gitlab
  • Secret token: same value as GITLAB_WEBHOOK_SECRET in .env
  • Triggers: Work item events + Merge request events + Comments

Step 15 — First real test

Open an issue on your target repo using the user_story.md template with the type::feature label. Watch the logs:

prepare_node → plan_node → develop_node → review_node → supervise_node → git_node

until a merge request appears on GitLab.


Step 16 — Running the tests

sudo -H -u agent-orchestrator /home/agent-orchestrator/.local/bin/uv run pytest tests/ -v