Deployment
Linux VPS (Headless) Guide
Synced from github.com/CoWork-OS/CoWork-OS/docs
CoWork OS can run on Linux as a long-running daemon through three supported install paths:
- Packaged server release (recommended for production VPS): a Linux x64 tarball from GitHub Releases with built daemon assets, runtime dependencies, systemd templates, bundled resources, and connectors.
- Node-only daemon from source/npm: run
coworkd-nodeunder Node.js, with no desktop window and no Xvfb. - Headless Electron daemon from source: closer to desktop parity, but requires Electron runtime deps + Xvfb.
All server modes can be driven remotely using:
--headless(no Electron windows)- The WebSocket Control Plane for remote task creation/monitoring (Web UI + CLI)
- Optional channel gateways (Telegram/Discord/Slack/etc) if you’ve configured them in the DB
These modes are designed for VPS/systemd/docker deployments.
Current packaging concept:
- The release tarball target is Linux x64 on glibc-based distributions.
- It starts
bin/coworkd-node.js; it does not launch the desktop UI and does not require Xvfb. - It includes the full
resources/tree, built MCP connector runtimes, and runtimenode_modules. - It may include Electron as a compatibility dependency while shared daemon code still imports Electron-safe helpers; users should not run the desktop app from this package.
- Interaction happens through the Control Plane Web UI,
coworkctl, or configured messaging channels.
If you want an overview (what the interface is, which runtime to pick, what works on Linux), start with:
docs/self-hosting.md
Start Here (First-Time VPS Install)
If you just want to get CoWork OS running and open the web UI, use this path first.
What you need:
- SSH access to your VPS (
user@your-vps) - One LLM API key (
OPENAI_API_KEYorANTHROPIC_API_KEY) - Two terminals (one for server, one for SSH tunnel)
- On your VPS (server terminal):
ssh user@your-vps
- Check Node.js + npm:
node -v
npm -v
cowork-os requires Node >=24.0.0.
If either command is missing, or you see v22/v23, install Node.js 24:
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
sudo apt-get install -y nodejs
node -v
- Install and start CoWork OS (Node-only daemon, no sudo/global npm required):
mkdir -p ~/cowork-run
cd ~/cowork-run
npm init -y >/dev/null
npm install cowork-os@latest --no-audit --no-fund
export COWORK_IMPORT_ENV_SETTINGS=1
export OPENAI_API_KEY=your_key_here # or: export ANTHROPIC_API_KEY=your_key_here
npx coworkd-node --print-control-plane-token
Keep this terminal open. It runs the server and prints the token you need for login.
- On your local machine (second terminal), open SSH tunnel:
ssh -N -L 18789:127.0.0.1:18789 user@your-vps
If local port 18789 is busy:
ssh -N -L 28789:127.0.0.1:18789 user@your-vps
- Open the dashboard in your browser:
http://127.0.0.1:18789/(orhttp://127.0.0.1:28789/if you used 28789)- Paste the Control Plane token printed in step 3
This quick start is great for first run/testing. For always-on production, continue with Option A (Packaged Server Release), Option B (Docker), or Option C (Systemd from Source) below.
Common First-Run Errors
npm WARN EBADENGINE: Node version is too old. Install Node 24 and retry step 3.npm ERR! EACCESonnpm install -g ...: This guide intentionally uses local install (npm install+npx coworkd-node) so you do not need global npm permissions.sh: 1: tsc: not foundafternpx coworkd-node: You are likely on an older broken npm publish that missed daemon build artifacts. Upgrade and retry:npm install cowork-os@latest --no-audit --no-fund
Option A: Packaged Server Release (Recommended)
Use this path when you want a GitHub release artifact that does not require cloning the repo or building TypeScript on the server.
- Install OS deps (Debian/Ubuntu):
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
ca-certificates curl tar \
python3 make g++
- Install Node.js 24 if it is not already installed:
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
sudo apt-get install -y nodejs
node -v
- Download the Linux server tarball and checksum from the GitHub release:
version=0.5.43
curl -LO "https://github.com/CoWork-OS/CoWork-OS/releases/download/v${version}/cowork-os-server-linux-x64-v${version}.tar.gz"
curl -LO "https://github.com/CoWork-OS/CoWork-OS/releases/download/v${version}/cowork-os-server-linux-x64-v${version}.tar.gz.sha256"
sha256sum --check "cowork-os-server-linux-x64-v${version}.tar.gz.sha256"
Replace 0.5.43 with the version you are installing.
- Install under
/opt/cowork-os:
sudo mkdir -p /opt/cowork-os
sudo tar -xzf "cowork-os-server-linux-x64-v${version}.tar.gz" -C /opt/cowork-os --strip-components=1
- Create a dedicated user + data dir:
sudo useradd -r -m -s /usr/sbin/nologin cowork || true
sudo mkdir -p /var/lib/cowork-os /srv/cowork/workspace
sudo chown -R cowork:cowork /var/lib/cowork-os /srv/cowork/workspace /opt/cowork-os
- Install the systemd unit + env file templates:
sudo cp /opt/cowork-os/deploy/systemd/cowork-os.env.example /etc/cowork-os.env
sudo $EDITOR /etc/cowork-os.env
sudo cp /opt/cowork-os/deploy/systemd/cowork-os-node.service /etc/systemd/system/cowork-os-node.service
sudo systemctl daemon-reload
sudo systemctl enable --now cowork-os-node
sudo journalctl -u cowork-os-node -f
The packaged release uses bin/coworkd-node.js by default. It does not launch the desktop UI or require Xvfb. Keep Node.js 24 installed; the package is built and smoke-tested on Linux x64 and includes runtime dependencies, but coworkd-node can still attempt a native dependency rebuild if the local Node ABI is incompatible.
Option B: Docker (Headless Electron)
This repo includes a headless Docker image that runs CoWork OS as a daemon.
How You Use It (After It’s Running)
- Create an SSH tunnel from your laptop:
ssh -N -L 18789:127.0.0.1:18789 user@your-vps
If your local machine already uses port 18789, use a different local port (example: 28789):
ssh -N -L 28789:127.0.0.1:18789 user@your-vps
- Open the minimal Control Plane Web UI locally:
http://127.0.0.1:18789/
- Or use
coworkctl:
export COWORK_CONTROL_PLANE_URL=ws://127.0.0.1:18789
export COWORK_CONTROL_PLANE_TOKEN=... # from logs (first token generation) or via --print-control-plane-token
node bin/coworkctl.js call config.get
- Build and start:
docker compose up --build -d
If you prefer the Node daemon container with no desktop window or Xvfb, use the compose profile:
docker compose --profile node up --build -d cowork-os-node
Defaults in docker-compose.yml:
- Persistent data volume mounted at
/data - A persistent workspace volume mounted at
/workspace(bootstrapped automatically). You can swap this for a host bind mount if you want CoWork OS to operate on files on the VPS. - Control Plane published on host loopback:
127.0.0.1:18789(safe default)
- View the Control Plane token (printed on first startup when it’s generated):
docker compose logs -f cowork-os
If you need to print it again later, restart with:
COWORK_PRINT_CONTROL_PLANE_TOKEN=1(env) or--print-control-plane-token(flag)
Option C: Systemd from Source (Node-Only Daemon)
This is the source-build path when you don’t want Docker or a prebuilt release artifact.
- Install OS deps (Debian/Ubuntu):
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
ca-certificates curl git \
python3 make g++
- Install Node.js 24 and build CoWork OS:
git clone https://github.com/CoWork-OS/CoWork-OS.git /opt/cowork-os
cd /opt/cowork-os
npm run setup:server
npm run build:daemon
npm run build:connectors
On first start, bin/coworkd-node.js may rebuild better-sqlite3 for your Node version (native addon ABI).
- Create a dedicated user + data dir:
sudo useradd -r -m -s /usr/sbin/nologin cowork || true
sudo mkdir -p /var/lib/cowork-os
sudo chown -R cowork:cowork /var/lib/cowork-os
If you cloned/built as root, ensure the service user can read (and rebuild native deps if needed):
sudo chown -R cowork:cowork /opt/cowork-os
- Install the systemd unit + env file templates:
- Unit:
deploy/systemd/cowork-os-node.service - Env example:
deploy/systemd/cowork-os.env.example
sudo cp /opt/cowork-os/deploy/systemd/cowork-os.env.example /etc/cowork-os.env
sudo $EDITOR /etc/cowork-os.env
sudo cp /opt/cowork-os/deploy/systemd/cowork-os-node.service /etc/systemd/system/cowork-os-node.service
sudo systemctl daemon-reload
sudo systemctl enable --now cowork-os-node
sudo journalctl -u cowork-os-node -f
Optional: Browser Automation (Browser V2 Fallback)
CoWork OS desktop uses the visible Browser V2 Workbench by default. Headless Linux/VPS installs do not have that renderer-owned workbench, so browser tools use the Playwright-local fallback when browser automation is explicitly needed. On minimal Linux VPS images (and especially slim Docker images), Chromium may fail to launch until OS dependencies are installed.
If you want fallback browser tools on Debian/Ubuntu, install Playwright’s Chromium + dependencies:
cd /opt/cowork-os
sudo npx playwright install --with-deps chromium
If you don’t need browser automation, you can ignore this and rely on web_fetch + API-based search providers.
If you’re running under Docker and want Playwright inside the container, you’ll want a container image that includes the required libraries. (We can add an optional “Playwright-ready” Docker profile/image next.)
Option D: Systemd from Source (Headless Electron)
This is a good fit when you don’t want Docker.
- Install OS deps (Debian/Ubuntu):
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
ca-certificates curl git \
python3 make g++ \
xvfb xauth \
fonts-liberation \
libgtk-3-0 libnss3 libxss1 libasound2 \
libgbm1 libdrm2 libxshmfence1 \
libatk1.0-0 libatk-bridge2.0-0 \
libcups2 libdbus-1-3 libnspr4 \
libx11-xcb1 libxcomposite1 libxdamage1 libxrandr2 \
libxext6 libxfixes3 libxcb1 libxrender1 \
libpango-1.0-0 libpangocairo-1.0-0 libcairo2 \
libexpat1 libglib2.0-0 libsecret-1-0
- Install Node.js 24 and build CoWork OS:
git clone https://github.com/CoWork-OS/CoWork-OS.git /opt/cowork-os
cd /opt/cowork-os
npm run setup:server
npm run build:electron
npm run build:connectors
- Create a dedicated user + data dir:
sudo useradd -r -m -s /usr/sbin/nologin cowork || true
sudo mkdir -p /var/lib/cowork-os
sudo chown -R cowork:cowork /var/lib/cowork-os
- Install the systemd unit + env file templates:
- Unit:
deploy/systemd/cowork-os.service - Env example:
deploy/systemd/cowork-os.env.example
Example install commands:
sudo cp /opt/cowork-os/deploy/systemd/cowork-os.env.example /etc/cowork-os.env
sudo $EDITOR /etc/cowork-os.env
sudo cp /opt/cowork-os/deploy/systemd/cowork-os.service /etc/systemd/system/cowork-os.service
sudo systemctl daemon-reload
sudo systemctl enable --now cowork-os
sudo journalctl -u cowork-os -f
Recommended: Persistent Data Directory
On VPS you usually want the DB/settings under a known path (for backups and container volumes).
export COWORK_USER_DATA_DIR=/var/lib/cowork-os
node bin/coworkd-node.js
Or via CLI flag:
node bin/coworkd-node.js --user-data-dir /var/lib/cowork-os
Bootstrapping a Workspace (Important)
Headless instances can’t “Select Folder” via UI, so you must either:
- Bootstrap a default workspace at startup:
export COWORK_BOOTSTRAP_WORKSPACE_PATH=/srv/cowork/workspace
export COWORK_BOOTSTRAP_WORKSPACE_NAME=main
- Or create one remotely over the Control Plane using
workspace.create.
coworkctl (Simple Control Plane CLI)
Use the bundled helper to call Control Plane methods:
export COWORK_CONTROL_PLANE_URL=ws://127.0.0.1:18789
export COWORK_CONTROL_PLANE_TOKEN=... # from startup logs
node bin/coworkctl.js call workspace.list
node bin/coworkctl.js call workspace.create '{"name":"main","path":"/srv/cowork/workspace"}'
node bin/coworkctl.js call config.get
node bin/coworkctl.js watch --event task.event
node bin/coworkctl.js tail '<taskId>' --limit 200
Configure Channels (Headless)
If you want to interact with the agent via Telegram/Discord/Slack/etc on a VPS, you can configure and manage channels over the Control Plane (no desktop UI required).
Examples:
node bin/coworkctl.js call channel.list
# Create a Telegram channel (disabled by default; test then enable)
node bin/coworkctl.js call channel.create '{"type":"telegram","name":"telegram","config":{"botToken":"..."},"securityConfig":{"mode":"pairing"}}'
node bin/coworkctl.js call channel.test '{"channelId":"..."}'
node bin/coworkctl.js call channel.enable '{"channelId":"..."}'
Configure LLM/Search Credentials (Headless)
You have two headless-friendly options:
- Control Plane setup (recommended once running)
Use the Web UI LLM Setup panel or callllm.configureviacoworkctl. - Env import on boot
Keep credentials in environment variables and import them at startup.
Option 1: Configure via Control Plane
Examples:
# OpenAI
node bin/coworkctl.js call llm.configure '{"providerType":"openai","apiKey":"sk-...","model":"gpt-4o-mini"}'
# Ollama (remote/local URL)
node bin/coworkctl.js call llm.configure '{"providerType":"ollama","settings":{"baseUrl":"http://127.0.0.1:11434"},"model":"gpt-oss:20b"}'
# Azure OpenAI
node bin/coworkctl.js call llm.configure '{"providerType":"azure","apiKey":"...","settings":{"endpoint":"https://...","deployment":"gpt-4.1-mini"}}'
Then verify:
node bin/coworkctl.js call config.get
API-first account lifecycle (agent signup support)
Use the managed account API to track signup status, store credentials, and let agents orchestrate account setup flows without relying on manual UI state.
# Create or update an account record (secrets stored encrypted)
node bin/coworkctl.js call account.upsert '{
"provider":"openrouter",
"label":"OpenRouter production",
"status":"pending_signup",
"signupUrl":"https://openrouter.ai/signup",
"notes":"Need billing enabled before production traffic"
}'
# Add secrets once available (or rotate later)
node bin/coworkctl.js call account.upsert '{
"id":"<accountId>",
"status":"active",
"secrets":{"api_key":"sk-or-..."},
"lastVerifiedAt":1739923200000
}'
# List records (redacted by default)
node bin/coworkctl.js call account.list
# Fetch a single account
node bin/coworkctl.js call account.get '{"accountId":"<accountId>"}'
# Remove account metadata/credentials
node bin/coworkctl.js call account.remove '{"accountId":"<accountId>"}'
Option 2: Configure via env import
CoWork OS supports an explicit, opt-in import path:
COWORK_IMPORT_ENV_SETTINGS=1(or--import-env-settings)- Optional:
COWORK_IMPORT_ENV_SETTINGS_MODE=merge|overwrite - Optional:
COWORK_LLM_PROVIDER=openai|anthropic|gemini|...
Example (OpenAI):
export COWORK_IMPORT_ENV_SETTINGS=1
export COWORK_LLM_PROVIDER=openai
export OPENAI_API_KEY=...
Example (search):
export COWORK_IMPORT_ENV_SETTINGS=1
export TAVILY_API_KEY=...
If you rotate keys later, restart with:
export COWORK_IMPORT_ENV_SETTINGS_MODE=overwrite
Control Plane Overrides
You can override bind host/port at startup:
export COWORK_CONTROL_PLANE_HOST=127.0.0.1
export COWORK_CONTROL_PLANE_PORT=18789
node bin/coworkd-node.js
Keep host=127.0.0.1 unless you fully understand the security implications of binding to 0.0.0.0.
Remote Access (SSH Tunnel)
From your local machine:
ssh -N -L 18789:127.0.0.1:18789 user@your-vps
Then connect your client to ws://127.0.0.1:18789 using the printed token.
If local port 18789 is busy, either:
- Use another fixed local port (
28789,38789, etc.):
ssh -N -L 28789:127.0.0.1:18789 user@your-vps
Use http://127.0.0.1:28789/ and ws://127.0.0.1:28789.
- Or auto-pick a free local port:
LOCAL_PORT=18789
while lsof -nP -iTCP:${LOCAL_PORT} -sTCP:LISTEN >/dev/null 2>&1; do
LOCAL_PORT=$((LOCAL_PORT + 1))
done
echo "Using local port: ${LOCAL_PORT}"
echo "Open: http://127.0.0.1:${LOCAL_PORT}/"
ssh -N -L ${LOCAL_PORT}:127.0.0.1:18789 user@your-vps
Web Dashboard (Browser UI)
When the Control Plane server is running, it also serves a minimal web UI at:
http://127.0.0.1:18789/(over SSH tunnel)
Open it in your browser, paste the Control Plane token, and you can:
- Configure LLM provider credentials (LLM Setup)
- List/create workspaces
- Create tasks and send messages
- View task events (recent history + live stream)
- Approve/deny approvals
Also see: docs/remote-access.md (SSH + Tailscale Serve/Funnel).
Approvals Over Control Plane
In headless mode, approval prompts (shell commands, deletions, etc.) can be handled remotely over the Control Plane:
- CoWork broadcasts
approval_requestedevents including anapprovalId - Respond via
approval.respondwith{ approvalId, approved }
This enables running a VPS instance without requiring a local UI or messaging channels for approvals.
Uninstall / Remove (VPS)
Choose one option based on whether you want to keep data.
Option 1: Stop and uninstall app services only (keep DB/data)
# If running via docker-compose in this repo
cd /path/to/cowork-vps
docker compose down
# If running via systemd
sudo systemctl stop cowork-os cowork-os-node
sudo systemctl disable cowork-os cowork-os-node
sudo rm -f /etc/systemd/system/cowork-os.service
sudo rm -f /etc/systemd/system/cowork-os-node.service
sudo systemctl daemon-reload
Keep data directories/volumes intact to preserve state:
- Docker: named volume
cowork_data(and workspace volumecowork_workspaceunless bound to host) - systemd example:
/var/lib/cowork-os - Custom env path: value passed in
COWORK_USER_DATA_DIRor--user-data-dir
Option 2: Full uninstall + data deletion (irreversible)
⚠️ WARNING: This deletes the full persistent database and all local state. This will delete everything forever; there is no undo and no recovery if you have no backup.
# Docker (removes anonymous/named volumes used for DB, workspace state, and tasks)
cd /path/to/cowork-vps
docker compose down -v
# systemd (remove user-data store)
sudo rm -rf /var/lib/cowork-os
If you configured a custom user-data directory, also remove it:
rm -rf "$COWORK_USER_DATA_DIR"