This guide covers installing Sandboxed.sh directly on a dedicated Ubuntu 24.04 server with systemd. This gives you the best performance and native systemd-nspawn container isolation.
Looking for the easier path? See the Docker installation guide — one command gets you running on any OS.
Sandboxed.sh is the orchestrator/UI backend. It does not run model inference; it executes OpenCode, Claude Code, Codex, Gemini, and Grok inside each workspace (host/container), so bash commands and file operations are scoped correctly. A standalone OpenCode server is optional and only required if you want centralized OpenCode services (provider/auth management, health checks, etc.).
For AI Agents: Before starting this installation, ask the user to provide:
- Server IP address (e.g.,
1.2.3.4)- Domain name pointing to that IP (e.g.,
agent.example.com)- SSH access credentials or key path for the server
- Library git repo URL (or confirm using the template)
- Dashboard password to set for authentication (or offer to generate one)
SSH Key Setup: Most servers require SSH key authentication. If the user doesn't have one:
- Generate a key (without passphrase for easier automation):
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N ""- Display the public key for them to copy:
cat ~/.ssh/id_ed25519.pub- They need to add this key to their server. Options:
- Hosting provider dashboard: Most providers (Hetzner, DigitalOcean, Vultr, etc.) have an "SSH Keys" section in their web console
- Existing access: If they can already log in:
ssh-copy-id root@<server-ip>Verify you have SSH access before proceeding:
ssh root@<server-ip> "hostname"If connection fails, common causes:
- The SSH key wasn't added to the server (check hosting provider's dashboard)
- The key has a passphrase (agent tools may not handle passphrase prompts)
- Firewall blocking port 22 (check hosting provider's firewall settings)
- Ubuntu 24.04 LTS, root SSH access
- A dedicated server (not shared hosting)
- You want:
- Sandboxed.sh bound to:
0.0.0.0:3000 - (Optional) OpenCode server bound to localhost:
127.0.0.1:4096
- Sandboxed.sh bound to:
- You have a Git repo for your Library (skills/tools/agents/rules/MCP configs)
Recommendation: Unless you know exactly what you need, install all components in this guide:
- Bun (required for OpenCode plugins and Playwright MCP)
- systemd-container + debootstrap (for isolated container workspaces)
- Desktop automation tools (Xvfb, i3, Chromium, xdotool, etc.)
- Reverse proxy with SSL (Caddy or Nginx + Certbot)
Skipping components may limit functionality. The full installation uses ~2-3 GB of disk space.
Before starting the installation, ensure your domain is configured:
Add an A record in your DNS provider:
agent.yourdomain.com → A → YOUR_SERVER_IP
Example with common providers:
- Cloudflare: DNS → Add Record → Type: A, Name:
agent, IPv4:YOUR_SERVER_IP - Namecheap: Advanced DNS → Add New Record → A Record
- Route53: Create Record → Simple routing → A record
Wait for DNS to propagate (usually 1-15 minutes), then verify:
# From your local machine
dig +short agent.yourdomain.com
# Should return your server IP
# Or use an online checker
curl -s "https://dns.google/resolve?name=agent.yourdomain.com&type=A" | jq .If your Library repo is private, set up an SSH deploy key on the server:
# On the server
ssh-keygen -t ed25519 -C "sandboxed.sh-server" -f /root/.ssh/sandboxed.sh -N ""
cat /root/.ssh/sandboxed.sh.pub
# Copy this public keyAdd the public key as a deploy key in your git provider:
- GitHub: Repository → Settings → Deploy keys → Add deploy key
- GitLab: Repository → Settings → Repository → Deploy keys
Configure SSH to use the key:
cat >> /root/.ssh/config <<'EOF'
Host github.com
IdentityFile /root/.ssh/sandboxed.sh
IdentitiesOnly yes
EOF
# Test the connection
ssh -T git@github.comapt update
apt install -y \
ca-certificates curl git jq unzip tar \
build-essential pkg-config libssl-devContainer workspaces (systemd-nspawn) — recommended for isolated environments:
apt install -y systemd-container debootstrapDesktop automation (Xvfb/i3/Chromium screenshots/OCR) — recommended for browser control:
apt install -y xvfb i3 x11-utils xdotool scrot imagemagick chromium chromium-sandbox tesseract-ocrSee docs/DESKTOP_SETUP.md for i3 config and additional setup after
installation.
OpenCode is distributed as a binary, but:
- OpenCode plugins are installed internally via Bun
- Sandboxed.sh’s default Playwright MCP runner prefers
bunx
Install Bun:
curl -fsSL https://bun.sh/install | bash
# Make bun/bunx available to systemd services
install -m 0755 /root/.bun/bin/bun /usr/local/bin/bun
install -m 0755 /root/.bun/bin/bunx /usr/local/bin/bunx
bun --version
bunx --versionOpenCode server is optional for mission execution. Sandboxed.sh runs OpenCode per-workspace via the CLI. Install the server if you want centralized provider/auth management, health checks, or a shared OpenCode service.
This installs the latest release into ~/.opencode/bin/opencode:
curl -fsSL https://opencode.ai/install | bash -s -- --no-modify-pathOptional: pin a version (recommended for servers):
curl -fsSL https://opencode.ai/install | bash -s -- --version 1.1.8 --no-modify-pathCopy the binary into a stable system location used by systemd:
install -m 0755 /root/.opencode/bin/opencode /usr/local/bin/opencode
opencode --versionSkip this section if you are not running a centralized OpenCode server.
Create /etc/systemd/system/opencode.service:
[Unit]
Description=OpenCode Server
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/opencode serve --port 4096 --hostname 127.0.0.1
WorkingDirectory=/root
Restart=always
RestartSec=10
Environment=HOME=/root
[Install]
WantedBy=multi-user.targetEnable + start:
systemctl daemon-reload
systemctl enable --now opencode.serviceTest:
curl -fsSL http://127.0.0.1:4096/global/health | jq .Note: Sandboxed.sh will also keep OpenCode's global config updated (MCP + tool
allowlist) in: ~/.config/opencode/opencode.json.
OpenCode discovers skills from global locations (e.g. ~/.opencode/skill,
~/.config/opencode/skill) and from the project/mission directory
.opencode/skill. To guarantee per‑workspace skill usage, run OpenCode with
an isolated HOME and keep global skill dirs empty.
- Create an isolated OpenCode home:
mkdir -p /var/lib/opencode- Update
opencode.serviceto use the isolated home:
Environment=HOME=/var/lib/opencode
Environment=XDG_CONFIG_HOME=/var/lib/opencode/.config
Environment=XDG_DATA_HOME=/var/lib/opencode/.local/share
Environment=XDG_CACHE_HOME=/var/lib/opencode/.cache- Point Sandboxed.sh at the same OpenCode config dir (see section 6):
OPENCODE_CONFIG_DIR=/var/lib/opencode/.config/opencode
- Move any old global skills out of the way (optional but recommended):
mv /root/.opencode/skill /root/.opencode/skill.bak-$(date +%F) 2>/dev/null || true
mv /root/.config/opencode/skill /root/.config/opencode/skill.bak-$(date +%F) 2>/dev/null || true- Reload services:
systemctl daemon-reload
systemctl restart opencode.service
systemctl restart sandboxed_sh.serviceValidation (on the server, from the repo root):
scripts/validate_skill_isolation.shIf you want to authenticate with Google accounts (Gemini plans/quotas including free tier) via OAuth instead of API keys:
bunx opencode-gemini-auth installThis enables OAuth-based Google authentication, allowing users to leverage their existing Gemini plan directly within OpenCode. Features include:
- OAuth flow with Google accounts
- Automatic Cloud project provisioning
- Support for thinking capabilities (Gemini 2.5/3)
To authenticate via CLI (useful for testing):
opencode auth login
# Select Google provider, then "OAuth with Google (Gemini CLI)"For dashboard OAuth integration, see the Settings page which handles this flow via the API.
Codex, Gemini, and Grok use their native CLIs plus credentials stored in the provider settings or the CLI's own login cache.
Configure OpenAI API keys or Codex/ChatGPT credentials in Settings →
Providers. Codex missions use raw model ids such as gpt-5.5,
gpt-5.3-codex, or another model visible to the connected account.
Configure Google/Gemini credentials in Settings → Providers or use the
Gemini CLI login flow. Gemini missions use raw model ids such as
gemini-3.1-pro-preview.
Configure xAI credentials in Settings → Providers, set XAI_API_KEY or
GROK_CODE_XAI_API_KEY, or run the Grok CLI login flow. Grok missions use raw
model ids such as grok-4.3.
curl -fsSL https://sh.rustup.rs | sh -s -- -y
source /root/.cargo/env
rustc --version
cargo --versionOn the server we keep the repo under /opt/sandboxed_sh/vaduz-v1. This must be
a git clone (not just copied files) for the dashboard's one-click update
system to work.
mkdir -p /opt/sandboxed_sh
cd /opt/sandboxed_sh
git clone <YOUR_SANDBOXED_SH_REPO_URL> vaduz-v1Important: The update system in Settings relies on git tags to detect new releases. If you deploy via rsync without
.git, the "Update Available" button won't work. Always usegit clonefor production deployments.
For local development, you can rsync to a different path (e.g.,
/root/sandboxed_sh) for rapid iteration, but keep the git clone at
/opt/sandboxed_sh/vaduz-v1 for the update system:
# Fast dev loop (to a separate path)
rsync -az --delete \
--exclude target --exclude .git --exclude dashboard/node_modules \
/path/to/local-dev/ \
root@<server-ip>:/root/sandboxed_sh/
# The git clone at /opt/sandboxed_sh/vaduz-v1 is used by the update systemIf you need to specify a custom SSH key, add -e "ssh -i ~/.ssh/your_key".
cd /opt/sandboxed_sh/vaduz-v1
source /root/.cargo/env
# Debug build (fast) - recommended for rapid iteration
cargo build --bin sandboxed-sh --bin workspace-mcp --bin desktop-mcp
install -m 0755 target/debug/sandboxed-sh /usr/local/bin/sandboxed-sh
install -m 0755 target/debug/workspace-mcp /usr/local/bin/workspace-mcp
install -m 0755 target/debug/desktop-mcp /usr/local/bin/desktop-mcp
# Or: Release build (slower compile, faster runtime)
# cargo build --release --bin sandboxed-sh --bin workspace-mcp --bin desktop-mcp
# install -m 0755 target/release/sandboxed-sh /usr/local/bin/sandboxed-sh
# install -m 0755 target/release/workspace-mcp /usr/local/bin/workspace-mcp
# install -m 0755 target/release/desktop-mcp /usr/local/bin/desktop-mcpNote: The MCP binaries (
workspace-mcp,desktop-mcp) are required for host workspace missions and the Extensions page. They must be in PATH.
Sandboxed.sh expects a git-backed Library repo. At runtime it will:
- clone it into
LIBRARY_PATH(default:{WORKING_DIR}/.sandboxed-sh/library) - ensure the
originremote matchesLIBRARY_REMOTE - pull/sync as needed
Template:
One way to bootstrap:
# On your machine
git clone git@github.com:Th0rgal/sandboxed-library-template.git sandboxed.sh-library
cd sandboxed.sh-library
# Point it at your own repo
git remote set-url origin git@github.com:<your-org>/<your-library-repo>.git
# Push to your remote (choose main/master as you prefer)
git push -u origin HEAD:mainOption A: Via Dashboard Settings (recommended)
After starting Sandboxed.sh, go to Settings in the dashboard and set the Library Remote URL. This is the preferred method as it persists the setting to disk and allows runtime updates without restart.
Option B: Via environment variable (initial default)
Set in /etc/sandboxed_sh/sandboxed_sh.env:
LIBRARY_REMOTE=git@github.com:<your-org>/<your-library-repo>.git(used as initial default if not configured in Settings)- optional:
LIBRARY_PATH=/root/.sandboxed-sh/library
Workspace templates can mark env vars as encrypted. When saved, those values
are encrypted with AES-256-GCM and stored in the git repo as
<encrypted v="1">...</encrypted>. The plaintext is only visible in the
dashboard and when deployed to workspaces.
How the key works:
- A single encryption key protects all encrypted values in the library.
- The key is stored at
{WORKING_DIR}/.sandboxed-sh/private_key(typically/root/.sandboxed-sh/private_key). - On first save of any encrypted value, Sandboxed.sh auto-generates the key if none exists.
- The key can also be set manually via the
PRIVATE_KEYenvironment variable (hex-encoded, 32 bytes / 64 hex chars).
Sharing the key across servers:
All servers that use the same Library repo must share the same encryption key, otherwise they cannot decrypt each other's encrypted values.
The recommended way to share the key is via Backup & Restore in the dashboard (Settings page). The backup archive includes the encryption key along with all other settings and credentials.
To set up a new server with an existing library:
- On the source server: go to Settings → Backup & Restore and click Download Backup.
- On the new server: go to Settings → Backup & Restore and upload the backup file via Restore Backup.
- The encryption key, AI provider credentials, workspace definitions, and all other settings are restored automatically.
Alternatively, copy the key file manually:
# Copy from source to new server
scp root@source-server:/root/.sandboxed-sh/private_key root@new-server:/root/.sandboxed-sh/private_keyOr set it via the environment:
# In /etc/sandboxed_sh/sandboxed_sh.env on the new server
PRIVATE_KEY=<64-hex-char-key-from-source-server>Create /etc/sandboxed_sh/sandboxed_sh.env:
mkdir -p /etc/sandboxed_sh
chmod 700 /etc/sandboxed_shExample (fill in your real values):
cat > /etc/sandboxed_sh/sandboxed_sh.env <<'EOF'
# OpenCode backend (optional; if set, must match opencode.service)
OPENCODE_BASE_URL=http://127.0.0.1:4096
OPENCODE_PERMISSIVE=true
# Optional: keep Sandboxed.sh writing OpenCode global config into the isolated home
# (recommended if you enabled strong workspace skill isolation in section 3.2.1).
# OPENCODE_CONFIG_DIR=/var/lib/opencode/.config/opencode
# Server bind
HOST=0.0.0.0
PORT=3000
# Default filesystem root for Sandboxed.sh (agent still has full system access)
WORKING_DIR=/root
LIBRARY_PATH=/root/.sandboxed-sh/library
# Library remote (optional, can also be set via dashboard Settings page)
LIBRARY_REMOTE=git@github.com:<your-org>/<your-library-repo>.git
# Auth (set DEV_MODE=false on real deployments)
DEV_MODE=false
DASHBOARD_PASSWORD=change-me
JWT_SECRET=change-me-to-a-long-random-string
JWT_TTL_DAYS=30
# Dashboard Console (local shell)
# No SSH configuration required.
# Default model (provider/model). If omitted or not in provider/model format,
# Sandboxed.sh won’t force a model and OpenCode will use its own defaults.
# Desktop tools (optional)
DESKTOP_ENABLED=true
DESKTOP_RESOLUTION=1920x1080
EOFCreate /etc/systemd/system/sandboxed_sh.service:
[Unit]
Description=sandboxed.sh (cloud orchestrator)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=root
Group=root
EnvironmentFile=/etc/sandboxed_sh/sandboxed_sh.env
WorkingDirectory=/root
ExecStart=/usr/local/bin/sandboxed-sh
Restart=on-failure
RestartSec=2
# Agent needs full system access, minimal hardening
NoNewPrivileges=false
PrivateTmp=false
ProtectHome=false
[Install]
WantedBy=multi-user.targetIf you want a workspace to egress via a residential IP, the recommended pattern is:
- Run a Tailscale exit node at home.
- Use a workspace template that installs and starts Tailscale inside the container.
On the home server:
tailscale up --advertise-exit-nodeApprove it in the Tailscale admin console (Machines → your node → “Approve exit node”).
This repo ships a sample template at:
library-template/workspace-template/residential.json
It installs Tailscale and adds helper scripts:
sandboxed.sh-network-up(brings up host0 veth + DHCP + DNS)sandboxed.sh-tailscale-up(starts tailscaled + sets exit node)sandboxed.sh-tailscale-check(prints Tailscale status + public IP)
Set these workspace env vars (not global env):
TS_AUTHKEY(auth key for that workspace)TS_EXIT_NODE(node name likeumbrelor its 100.x IP)- Optional:
TS_ACCEPT_DNS=true|false,TS_EXIT_NODE_ALLOW_LAN=false,TS_STATE_DIR=/var/lib/tailscale
Then inside the workspace:
sandboxed.sh-tailscale-up
sandboxed.sh-tailscale-checkIf the public IP matches your home ISP, the exit node is working.
systemd-nspawn --network-veth needs DHCP + NAT on the host. Without this,
containers won’t reach the internet or Tailscale control plane.
Create an override for ve-* interfaces:
cat >/etc/systemd/network/80-container-ve.network <<'EOF'
[Match]
Name=ve-*
[Network]
Address=10.88.0.1/24
DHCPServer=yes
EOF
systemctl restart systemd-networkdEnable forwarding + NAT (replace <ext_if> with your public interface, e.g.
enp0s31f6):
sysctl -w net.ipv4.ip_forward=1
iptables -t nat -A POSTROUTING -s 10.88.0.0/24 -o <ext_if> -j MASQUERADE
iptables -A FORWARD -s 10.88.0.0/24 -o <ext_if> -j ACCEPT
iptables -A FORWARD -d 10.88.0.0/24 -m state --state ESTABLISHED,RELATED -i <ext_if> -j ACCEPTPersist the iptables rules using iptables-persistent (or migrate to nftables).
Tailscale inside a container requires:
/dev/net/tunbound into the containerCAP_NET_ADMIN- A private network namespace (not host network)
If those aren’t enabled, Tailscale will fail or affect the host instead of the workspace.
Enable + start:
systemctl daemon-reload
systemctl enable --now sandboxed_sh.serviceTest:
curl -fsSL http://127.0.0.1:3000/api/health | jq .If you want browser/desktop automation on Ubuntu, run:
cd /opt/sandboxed_sh/vaduz-v1
bash scripts/install_desktop.shOr follow docs/DESKTOP_SETUP.md.
The Settings page shows available updates for Sandboxed.sh, OpenCode, and Grok. When a new version is available:
- Go to Settings → System Components
- If "Update Available" appears, click the Update button
- The update will:
- Fetch the latest git tags from the repository
- Checkout the newest release tag
- Build the binaries (debug mode for faster compile)
- Install and restart the service
Requirements for one-click updates:
- The repository at
/opt/sandboxed_sh/vaduz-v1must be a git clone (not rsync'd files) - Create GitHub releases with version tags (e.g.,
0.2.1orv0.2.1) to trigger update detection - The server needs SSH access to pull from GitHub (deploy key configured in section 0.5.3)
cd /opt/sandboxed_sh/vaduz-v1
git fetch --tags origin
git checkout <version-tag> # e.g., v0.2.1
source /root/.cargo/env
cargo build --bin sandboxed-sh --bin workspace-mcp --bin desktop-mcp
install -m 0755 target/debug/sandboxed-sh /usr/local/bin/sandboxed-sh
install -m 0755 target/debug/workspace-mcp /usr/local/bin/workspace-mcp
install -m 0755 target/debug/desktop-mcp /usr/local/bin/desktop-mcp
systemctl restart sandboxed_sh.serviceQuick rebuild (main binary only, if MCP binaries haven't changed):
cargo build --bin sandboxed-sh
install -m 0755 target/debug/sandboxed-sh /usr/local/bin/sandboxed-sh
systemctl restart sandboxed_sh.serviceNote: Always rebuild and install all binaries after pulling changes that modify the MCP tools, otherwise the Extensions page will show connection errors.
Or to follow the latest master branch:
cd /opt/sandboxed_sh/vaduz-v1
git pull origin master
# ... build and install as above# Optionally pin a version
curl -fsSL https://opencode.ai/install | bash -s -- --version 1.1.8 --no-modify-path
install -m 0755 /root/.opencode/bin/opencode /usr/local/bin/opencode
systemctl restart opencode.service
curl -fsSL http://127.0.0.1:4096/global/health | jq .For production deployments, always put Sandboxed.sh behind a reverse proxy with TLS. The backend serves HTTP only and should never be exposed directly to the internet.
Caddy automatically obtains and renews Let's Encrypt certificates.
Install Caddy:
apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
apt update && apt install caddyCreate /etc/caddy/Caddyfile:
agent.yourdomain.com {
reverse_proxy localhost:3000
}
Enable and start:
systemctl enable --now caddyCaddy will automatically obtain TLS certificates for your domain.
Install Nginx and Certbot:
apt install -y nginx certbot python3-certbot-nginxCreate /etc/nginx/sites-available/sandboxed.sh:
server {
listen 80;
server_name agent.yourdomain.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;
# SSE support (for mission streaming)
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 86400s;
}
}Enable the site and obtain certificates:
ln -s /etc/nginx/sites-available/sandboxed.sh /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
certbot --nginx -d agent.yourdomain.comBlock direct access to port 3000 from the internet:
# Allow only localhost to reach Sandboxed.sh directly
iptables -A INPUT -p tcp --dport 3000 -s 127.0.0.1 -j ACCEPT
iptables -A INPUT -p tcp --dport 3000 -j DROPSandboxed.sh supports three authentication modes:
| Mode | Environment Variables | Use Case |
|---|---|---|
| Disabled | DEV_MODE=true |
Local development only |
| Single Tenant | DASHBOARD_PASSWORD, JWT_SECRET |
Personal server, one user |
| Multi-User | SANDBOXED_SH_USERS, JWT_SECRET |
Shared server, multiple users |
Set a strong password and JWT secret:
# Generate a random JWT secret
JWT_SECRET=$(openssl rand -base64 32)
# In /etc/sandboxed_sh/sandboxed_sh.env:
DEV_MODE=false
DASHBOARD_PASSWORD=your-strong-password-here
JWT_SECRET=$JWT_SECRET
JWT_TTL_DAYS=30For multiple users with separate credentials:
# In /etc/sandboxed_sh/sandboxed_sh.env:
DEV_MODE=false
SANDBOXED_SH_USERS='[
{"username": "alice", "password": "alice-strong-password"},
{"username": "bob", "password": "bob-strong-password"}
]'
JWT_SECRET=$(openssl rand -base64 32)Note: Multi-user mode provides separate login credentials but does not provide workspace or data isolation between users. All users see the same missions and workspaces.
This guide installs the backend on your server. The dashboard (frontend) is separate and you have several options:
| Option | Best For | Setup |
|---|---|---|
| Vercel | Production, always accessible | Deploy dashboard/ to Vercel |
| Local | Development, quick testing | Run bun dev in dashboard folder |
| iOS App | Mobile access | Enter backend URL in app |
Deploy the dashboard/ folder to Vercel:
- Connect your repo to Vercel
- Set the root directory to
dashboard - Add environment variable:
NEXT_PUBLIC_API_URL=https://agent.yourdomain.com - Deploy
The dashboard will connect to your backend server.
Run the dashboard locally on your machine:
cd dashboard
bun install
NEXT_PUBLIC_API_URL=https://agent.yourdomain.com bun devThen open http://localhost:3000.
On first launch, the iOS app prompts for the server URL. Enter your backend URL
(e.g., https://agent.yourdomain.com).
To change later: Menu (⋮) → Settings
Sandboxed.sh uses OAuth for AI provider authentication. The following providers are pre-configured:
| Provider | OAuth Client | Setup Required |
|---|---|---|
| Anthropic | OpenCode's client | None (works out of the box) |
| OpenAI | Codex CLI client | None (works out of the box) |
| Google/Gemini | Gemini CLI client | Install opencode-gemini-auth plugin |
OAuth flows use copy-paste for the authorization code. The user:
- Clicks "Authorize" in the dashboard
- Completes OAuth in their browser
- Copies the redirect URL back to the dashboard
- Set
DEV_MODE=false - Set strong
DASHBOARD_PASSWORDandJWT_SECRET - Configure reverse proxy (Caddy or Nginx) with TLS
- Firewall port 3000 (only allow localhost)
- Pin OpenCode version for stability
- Set up your Library git repo
- Share encryption key across servers (backup/restore or manual copy)
- Test OAuth flows for AI providers