Step-by-step guide for installing QuadWork on a remote VPS (Hetzner/Ubuntu). Based on real deployment notes. Designed for both humans and AI coding agents.
Provider: Hetzner Cloud — tested and confirmed working.
| Setting | Value |
|---|---|
| Type | Shared Resources > Regular Performance (x86 AMD) |
| Plan | CPX32 — 4 vCPU, 8 GB RAM, 160 GB disk (~$17/mo) |
| Image | Ubuntu 24.04 |
| Networking | IPv4 + IPv6 |
| SSH keys | Add your public key |
Why CPX32: QuadWork runs 4 concurrent AI agents, each spawning its own PTY + subprocess. 4 vCPUs map to the 4-agent model, 8 GB RAM provides headroom. Smaller plans may work for testing.
Why Regular Performance: Newer AMD hardware with consistent CPU. Cost-Optimized uses older generation hardware — not ideal for sustained agent workloads.
After creating the server, add it to your local ~/.ssh/config:
Host quadwork
User root
HostName <server-ip>
Port 22
IdentityFile ~/.ssh/<your-key>
Verify: ssh quadwork
Claude Code blocks --dangerously-skip-permissions when running as root. Agents will crash immediately if QuadWork runs under root.
Run these as root:
useradd -m -s /bin/bash quadwork
mkdir -p /projects
chown quadwork:quadwork /projectsGrant passwordless sudo:
echo "quadwork ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/quadwork
chmod 440 /etc/sudoers.d/quadworkCopy SSH keys:
mkdir -p /home/quadwork/.ssh
cp /root/.ssh/authorized_keys /home/quadwork/.ssh/authorized_keys
chown -R quadwork:quadwork /home/quadwork/.ssh
chmod 700 /home/quadwork/.ssh
chmod 600 /home/quadwork/.ssh/authorized_keysUpdate local ~/.ssh/config — change User root to User quadwork. All subsequent steps run as the quadwork user.
sudo apt-get update
sudo apt-get install -y git apache2-utilsapache2-utils provides htpasswd (used in Step 10 for HTTP basic auth).
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
source ~/.bashrc
nvm install 24
nvm use 24Do NOT use system Node (apt or nodesource). Only use nvm. System Node alongside nvm creates PATH conflicts — pm2 and QuadWork spawn agents with system PATH (missing nvm binaries), causing agents to fail auth or not be found.
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg
echo 'deb [arch=amd64 signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main' | sudo tee /etc/apt/sources.list.d/github-cli.list
sudo apt-get update && sudo apt-get install -y ghnpm install -g @anthropic-ai/claude-code
npm install -g @openai/codexAll binaries (claude, codex, quadwork, pm2) live under ~/.nvm/versions/node/v24.x.x/bin/.
These are interactive steps. The operator must run these via
ssh quadwork:
gh auth login # Follow browser-based auth flow
claude # Follow login prompt
codex # Follow login promptIf migrating from an existing server, copy auth configs instead:
# From your local machine
ssh quadwork-old 'tar czf /tmp/auth-backup.tar.gz .claude .codex .config/gh'
scp quadwork-old:/tmp/auth-backup.tar.gz /tmp/
scp /tmp/auth-backup.tar.gz quadwork:/tmp/
ssh quadwork 'cd ~ && tar xzf /tmp/auth-backup.tar.gz && rm /tmp/auth-backup.tar.gz'npm install -g quadwork@latestOptional — pre-create config at ~/.quadwork/config.json:
{
"port": 3000,
"projects": []
}Run interactive setup:
quadwork initIf upgrading from a version that used AgentChattr (the separate Python/FastAPI chat service), remove its pm2 processes:
pm2 stop agentchattr-*
pm2 delete agentchattr-*
pm2 saveChat is now file-based (JSONL + MCP shim) and no longer requires a separate service. You can also remove agentchattr_url and agentchattr_dir from ~/.quadwork/config.json if present.
npm install -g pm2IMPORTANT: pm2 strips PATH from child processes. Even if nvm is loaded, the QuadWork process won't have nvm binaries in PATH. Do not fix with symlinks — they resolve the binary but not the environment.
Create a wrapper script:
cat > ~/start-quadwork.sh << 'EOF'
#!/bin/bash
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
nvm use 24
exec quadwork start
EOF
chmod +x ~/start-quadwork.shStart with pm2:
pm2 start ~/start-quadwork.sh --name quadwork --interpreter /bin/bash
pm2 saveAuto-start on reboot:
pm2 startup systemd
# This prints a sudo command — copy and run it. Example:
# sudo env PATH=... pm2 startup systemd -u quadwork --hp /home/quadworkImportant: Always run pm2 save while the process is online. If saved while stopped, it resurrects as stopped on every reboot.
pm2 list # View processes
pm2 logs quadwork # Live logs
pm2 logs quadwork --lines 50 --nostream # Last 50 lines
pm2 stop quadwork # Stop
pm2 start quadwork # Start
pm2 restart quadwork # Restart
pm2 save # Save state (always do after start/stop)Create an A record: app.example.com -> server IP.
sudo apt-get install -y nginxCreate /etc/nginx/sites-available/app.example.com:
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400;
}
}proxy_read_timeout 86400 and WebSocket headers are required for live agent terminal connections.
sudo ln -sf /etc/nginx/sites-available/app.example.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginxsudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d app.example.com --non-interactive --agree-tos -m your@email.comThe dashboard is publicly accessible once deployed. Add password protection:
openssl rand -base64 18
# Save the output as your password
sudo htpasswd -cb /etc/nginx/.htpasswd admin 'YOUR_GENERATED_PASSWORD'Mobile browsers (especially Safari) drop the Authorization header aggressively on new connections and WebSocket reconnects, causing repeated sign-in popups every few minutes. The fix: cache successful auth in a cookie so nginx skips the challenge on subsequent requests.
Generate a unique secret for your deployment (this becomes the cookie value):
openssl rand -hex 16
# Example output: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6
# Save this — you'll use it in the nginx config belowAdd a map block outside the server block (at the http level — typically at the top of your site config file or in /etc/nginx/conf.d/auth-cache.conf). Replace YOUR_AUTH_SECRET with the value generated above:
# Cache basic auth in a cookie so mobile browsers don't reprompt
map $cookie_qw_auth $auth_ok {
"YOUR_AUTH_SECRET" "off";
default "QuadWork";
}Then update the server block (inside listen 443 ssl) to use $auth_ok instead of a static string, and set the cookie on every response. Replace YOUR_AUTH_SECRET with the same value:
auth_basic $auth_ok;
auth_basic_user_file /etc/nginx/.htpasswd;
# Set cookie after successful auth (24h expiry)
# Do NOT use "always" — it would set the cookie on 401 responses too
add_header Set-Cookie "qw_auth=YOUR_AUTH_SECRET; Path=/; Max-Age=86400; HttpOnly; Secure";How it works:
- First visit: no
qw_authcookie →$auth_ok="QuadWork"→ browser prompts for credentials (normal basic auth) - After successful auth: cookie is set with 24h expiry (value is your unique secret)
- Subsequent requests: cookie value matches map key →
$auth_ok="off"→ auth challenge skipped - To force re-login: clear the
qw_authcookie in your browser, or wait 24h - Security: the secret is unique to your deployment — knowing the guide's placeholder doesn't help an attacker
Reload nginx to apply the new config (only needed for initial setup — nginx reads .htpasswd on every request, so credential changes don't require a reload):
sudo nginx -t && sudo systemctl reload nginxSave credentials for reference:
cat > ~/.quadwork/.env << 'EOF'
QUADWORK_HTTP_USER=admin
QUADWORK_HTTP_PASS=YOUR_GENERATED_PASSWORD
EOF
chmod 600 ~/.quadwork/.env- Create Hetzner VPS (CPX32, Ubuntu 24.04, Regular Performance)
- SSH in as root, create
quadworkuser with sudo + SSH keys - Update local SSH config to
User quadwork - Install system packages:
git,apache2-utils - Install nvm + Node.js 24
- Install GitHub CLI
- Install Claude Code + Codex CLI
- Authenticate CLIs (gh, claude, codex)
- Install QuadWork + pm2
- Run
quadwork init - Create
~/start-quadwork.shwrapper script (loads nvm before exec) - Start with pm2 wrapper, save, configure startup
- Set up DNS A record
- Configure nginx reverse proxy + SSL
- Add HTTP basic auth
- Verify reboot survival:
sudo reboot, then checkpm2 list