Detailed operating notes for the opencode-dev-sanbox. The README covers the quick start; this page goes deeper on sudo, Nix, the CLI proxy, the OpenCode config, TLS and Chrome's sandbox.
- Sudo
- Nix
- CLI Proxy
- OpenCode configuration
- Custom hostname or LAN address in the certificate
- Environment variables
- Chrome sandbox
- Ports
The account user is a member of the sudo group and escalation goes through the real
/usr/bin/sudo:
sudo apt-get updateThat prompts for the password you set on the first-run web form, and standard Debian sudo policy and timestamp caching apply.
- The web password is the Unix password. There is one credential, not two.
- Only the Unix shadow hash is persisted at
/workspace/.devbox/user-password.hash, so the same login and sudo password survives container recreation as long as the workspace volume is kept. - Until first-run setup completes the account is locked, so
sudocannot be used at all. - There is no
NOPASSWDsudoers drop-in and no passwordless escalation. devbox-rootand the fakesudoshim from earlier versions are gone.- The Cloud Run
no_new_privsworkaround is not supported and has no replacement. On a runtime that forbids setuid escalation,sudowill not work.
Nix runs single-user with no nix-daemon, and /nix is owned by uid 1000. Installing packages
needs no sudo:
nix profile install nixpkgs#ripgrep
nix shell nixpkgs#gcc nixpkgs#cmake
nix developnix-command and flakes are enabled in /etc/nix/nix.conf.
Warning
The Nix store does not persist across container recreation. /nix lives in the container
writable layer, not on the /workspace volume, so:
docker restartanddocker stop/startkeep everything you installed.docker rmfollowed by a freshdocker runloses it./workspaceis unaffected.
State and cache are pinned to /nix/var/nix/user-state and /nix/var/nix/user-cache rather than
$HOME, so a recreated container never leaves a profile symlinked to store paths that no longer
exist.
Because of that, commit flake.nix and flake.lock with the project and let nix develop
rebuild the environment. A per-project flake restores itself; a nix profile install does not.
Warning
Single-user Nix does not sandbox derivations (sandbox = false). Builds run with the same uid as
the agent. That is fine for your own projects and not appropriate for untrusted derivations.
OpenCode and OpenChamber are installed as uid 1000 packages under the persistent workspace prefix:
/workspace/.local/bin
/workspace/.local/lib/node_modules
The managed service environment sets NPM_CONFIG_PREFIX=/workspace/.local and puts the prefix's
bin directory first on PATH. This matches the package manager environment used by the web
update controls, so updating OpenCode or OpenChamber does not require root or write access to
/usr.
The image keeps a user-owned seed outside the workspace and copies it only when either application is missing from the volume. Existing user-installed versions are never overwritten. After updating OpenChamber from its web UI, restart the Agent application from the Launcher to load the new server process; the upstream container update flow intentionally keeps the current server online.
CLIProxyAPI brokers provider accounts behind one OpenAI-compatible endpoint, with the upstream Management Center panel baked into the image so it is available offline.
-
Launch CLI Proxy from the dashboard.
-
Open CLI Proxy from the dashboard β the new tab goes through a small bootstrap page that seeds your already-started opencode-dev-sanbox session into the panel, so there is no second login and no prompt for the management key.
-
Add providers and create proxy API keys there. None are seeded for you.
-
Point in-container agents and CLIs at the proxy:
http://127.0.0.1:8317
If you need the management key for anything outside the browser, the entrypoint generates it on first start:
docker exec -u user devbox sh -c 'cat /workspace/.devbox/cliproxy/management.key'Only the panel and its management API are reachable through the gateway. The /v1/ proxy surface
is not published, so it stays loopback-only inside the container.
Provider sign-in supports device-code flows and manual callback-URL submission, plus plain API keys. No OAuth callback ports are published, so a browser on another machine cannot be redirected to a listener inside the container. Use device code or paste the callback URL into the panel.
State lives in /workspace/.devbox/cliproxy (mode 0700): config, management key, provider OAuth
tokens, logs and plugins.
OpenCode starts with a working global config, seeded on first boot to
~/.config/opencode/opencode.jsonc (that is /workspace/.config/opencode/opencode.jsonc, on the
persistent volume). It enables LSP, web/code search, the context7 and chrome-devtools MCP
servers, the background-agents and pty plugins, and registers Local CLIProxyAPI as a provider
pointing at http://127.0.0.1:8317/v1.
Edit it like any other config β the entrypoint writes the file only when neither opencode.jsonc
nor opencode.json exists there, so your changes are never overwritten by a restart or an image
upgrade. To start over, delete the file and restart the container. The seed template ships at
/etc/devbox/opencode.jsonc.
Note
The provider is configured but has no credentials. Add an account in the CLI Proxy Management Center and mint a proxy API key first, otherwise model calls fail with an auth error.
Set extra SANs on first boot:
-e TLS_SAN="DNS:devbox.lan,IP:192.168.1.10"Only read when the certificate does not exist yet. To regenerate, delete
/workspace/.devbox/tls and restart the container. You can also drop your own
devbox.crt / devbox.key in that directory.
| Variable | Default | Effect |
|---|---|---|
TLS_SAN |
β | Extra SANs for the generated certificate |
Chrome's renderer sandbox creates an unprivileged user namespace. Docker's default seccomp
profile blocks that operation, so the quick start uses seccomp=unconfined. The container still
receives no host devices, host namespaces, host socket, or host-level capabilities.
At boot the image probes unshare -Ur. If it is unavailable, Chrome starts with --no-sandbox
rather than failing silently. This reduces browser defense in depth; use the quick-start setting
when the desktop browser is exposed to untrusted web content.
If the probe fails despite the seccomp setting, the host may disallow unprivileged user namespaces:
sudo sysctl -w kernel.unprivileged_userns_clone=1Recent Ubuntu hosts can additionally restrict unprivileged user namespaces through AppArmor:
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0Caution
These are host-wide settings; evaluate them against your threat model before changing them.
The public nginx gateway listens on 80 (plaintext HTTP) and 443 (self-signed TLS). Everything else binds loopback only:
| Service | Internal endpoint | Public route |
|---|---|---|
| Agent (OpenChamber) | 127.0.0.1:9100 |
/ |
| Code (code-server) | 127.0.0.1:9101 |
/code/ |
| Control API (devbox-api) | 127.0.0.1:9102 |
/launcher/api/ |
| Desktop bridge (websockify) | 127.0.0.1:9103 |
/vnc/ |
| Files (FileBrowser) | 127.0.0.1:9104 |
/files/ |
| Web terminal broker | 127.0.0.1:9105 |
/terminal/api/, /terminal/ws/ |
| CLI Proxy (CLIProxyAPI) | 127.0.0.1:8317 |
/management.html only |
Internal services sit at 9100β9105, so dev servers you run inside the box (3000, 5173, 8080, β¦) never collide. CLIProxyAPI is the exception at 8317, outside that range: it keeps the upstream default port so provider documentation and agent configs work unchanged.
The Terminal application stores each tab as a private tmux session. A browser WebSocket is only an attachment to that session: closing the tab, closing the browser or losing the network does not terminate the shell or commands running inside it. Reopen the Terminal application to enumerate existing sessions and reconnect automatically.
The Kill action is separate from detaching a tab and terminates the tmux session. Sessions and
their processes cannot survive docker restart or container recreation because they are runtime
processes, not files on /workspace.