OpenCode Web Deployment — Notes
Architecture
- Container:
debian:bookworm-slim, user opencode (uid 1000), SSH + web (opencode web --hostname 0.0.0.0 --port 4096), named volume opencode-workspace -> /home/opencode/workspace.
- Compose prefixes volume names:
opencode-workspace becomes opencode_opencode-workspace (project dir opencode). Pin with name: opencode-workspace in volumes: if you want the plain name.
URL routing (core gotcha)
/ = launcher/home -> cannot create a new session here (no project context).
/{base64(dir)} = project home, New session works.
/{base64(dir)}/session and /session/{id} -> new / existing sessions.
L2hvbWUvb3BlbmNvZGUvd29ya3NwYWNl = base64 of /home/opencode/workspace.
- Re-encode a path with:
printf '/path' | base64 -w0.
Web UI auth
- Env
OPENCODE_SERVER_PASSWORD=... (required for network access; without it the server is unsecured). Optional OPENCODE_SERVER_USERNAME (default opencode).
- Set in compose
environment: (do not bake into the image).
Caddy reverse proxy
@opencode host opencode.domain.com
handle @opencode {
basic_auth {
user $2a$14$HASH # from: caddy hash-password (NOT passwd#hash)
}
redir / /L2hvbWUvb3BlbmNvZGUvd29ya3NwYWNl permanent
reverse_proxy opencode-sandbox:4096 {
flush_interval -1
}
}
redir / matches only the exact root path -> encoded path and /session/* are proxied unchanged (no loop).
flush_interval -1 disables buffering -> smooth SSE streaming. Recommended, not strictly required.
- Generate the hash with
caddy hash-password (user passwd#hash is invalid Caddy).
Volumes / persistence (btrfs host)
- Named volume data:
/var/lib/docker/volumes/opencode_opencode-workspace/_data. It is a plain directory on the root filesystem, so df shows /dev/vda1 — this is normal, not a bug.
- Ownership: files owned by uid 1000 (
opencode). Any container sharing the volume must run as uid 1000, or mount :ro.
- Bind mount alternative: map
/root/workspace:/home/opencode/workspace, then on the host run chown 1000:1000 /root/workspace and git init -q (the image’s git init is shadowed by the bind mount). No Docker integrity damage.
- Share the volume with another container (e.g. hermes): same compose file mounts the same volume; across projects use
external: true + name: opencode_opencode-workspace. Keep uid 1000 or use :ro.
- Flow: opencode builds in the volume -> hermes publishes from the volume to the website.
Sessions / tasks
- Closing the browser does not stop a running task — it runs server-side. Reopen the page to review history / live state.
- A task dies only if the container is stopped or the host reboots mid-task.
- Chat/session history lives in
/home/opencode/.local/share/opencode (the container writable layer, NOT the volume). It survives host reboot and docker restart, but is lost on container recreate (docker compose up -d --build).
- Fix: add a second volume
opencode-data:/home/opencode/.local/share/opencode to preserve history.
Backup to WebDAV via cron (rclone)
# setup (once)
apt install -y rclone
rclone config # create remote "backup:" with type=webdav
# /root/backup-workspace.sh
#!/bin/bash
set -euo pipefail
TMP=/tmp/opencode.tar.gz
DEST="backup:dockervolumes/opencode-$(date +%F_%H%M).tar.gz"
docker run --rm -v opencode_opencode-workspace:/data alpine tar czf - -C /data . > "$TMP"
rclone copyto "$TMP" "$DEST"
rclone delete backup:dockervolumes/ --min-age 14d
rm -f "$TMP"
echo "backup ok -> $DEST" >> /var/log/opencode-backup.log
chmod +x /root/backup-workspace.sh
# cron (as root)
0 2 * * * /root/backup-workspace.sh >> /var/log/opencode-backup.log 2>&1
评论区