macOS / Linux:
curl -fsSL https://raw.githubusercontent.com/EdikSimonian/opencode/dev/scripts/opencode-container/install.sh | sh
# or, from a checkout:
sh scripts/opencode-container/install.shWindows (PowerShell):
irm https://raw.githubusercontent.com/EdikSimonian/opencode/dev/scripts/opencode-container/install.ps1 | iex
# or, from a checkout:
pwsh -File scripts\opencode-container\install.ps1Then open a new terminal (or source your shell rc / . $PROFILE) and run
opencode in a project — the first run prompts for your server + API key.
Run opencode inside a container instead of installing the native binary —
using Podman, a free, open-source, daemonless Docker replacement. It does
not touch any existing Docker install: the wrapper calls podman directly and
Podman keeps its own separate image store (and, on macOS/Windows, its own VM).
The runtime is used only for opencode.
On first launch you're asked for a server (LiteLLM base URL) and an API key. They're stored on the host and passed into the container read-only.
- Installs Podman (with confirmation). No Homebrew or winget required — it
uses them if present, otherwise downloads the official signed Podman
installer (macOS
.pkg, verified by macOS; Windows setup.exe, Authenticode signed). Linux uses the distro package manager. On macOS without Homebrew it also fetches a checksum-verified staticjq. Skips if Podman is already present. - Starts the Podman machine (macOS/Windows VM); on macOS it shares
/Usersand/Volumesso projects outside$HOMEwork. - Pulls
docker.io/edisimon/opencode:latest. - Installs the
opencodecommand (a small wrapper on yourPATH, so it works in bash/zsh/fish/PowerShell) and offers to run first-time setup.
The first time you run opencode (or via opencode-setup) you'll be prompted:
LiteLLM server base URL (e.g. https://ai.simonian.online): https://ai.simonian.online
API key (input hidden): ********
The key is validated against <server>/v1/models (which also auto-discovers the
models your server offers), then written to:
~/.config/opencode-container/opencode.json— provider config (baseURL, model list)~/.config/opencode-container/auth.json— the API key,chmod 600(dirchmod 700)
The key is never echoed to the terminal and never passed as a command-line argument (so it won't land in shell history or the process list).
| Command | What it does |
|---|---|
opencode [...] |
Run opencode in the container, current folder mounted at /work |
opencode-setup |
(Re)enter the server URL + API key |
opencode-reauth |
Forget stored creds and re-run setup |
opencode-update |
podman pull the latest image |
The project is mounted at the git repo root (so running from a subdirectory
still sees .git, AGENTS.md, .opencode/), with your subdir as the working
directory. Sessions/state persist on the host under ~/.local/share/opencode.
Resource cleanup (macOS/Windows): the Podman machine is a VM. If opencode
had to start it and, on exit, no other Podman containers are running, it
stops the machine again to free RAM/CPU. It will not stop a machine that was
already running when you launched (so it never disrupts other Podman work). Set
OPENCODE_KEEP_MACHINE=1 to leave the machine running (faster repeated launches).
- Config + key files live only in
~/.config/opencode-container/. - They are mounted into the container read-only (
:ro), so the container can read them to authenticate but cannot modify/rotate them. - Note: read-only protects against tampering, not against use — code running in the container can read the key in order to call your LiteLLM. This is inherent to running an agent that must authenticate; scope each student key accordingly on the LiteLLM side.
By default the container runs on a dedicated, egress-filtered network so a misbehaving agent (or weak model) can't reach anything it shouldn't:
- Outbound → public internet only. The container cannot initiate
connections to your LAN, your host, link-local or CGNAT addresses
(
10/8,172.16/12,192.168/16,169.254/16,100.64/10are dropped). It can't poke your router, NAS, or host services — only the internet (e.g. your LiteLLM server). - Inbound still works. Replies to inbound connections aren't filtered, so ports you publish remain reachable. See Inbound ports below.
- No privilege escalation. The container drops all Linux capabilities
(
--cap-drop=ALL) and setsno-new-privileges. Combined with Podman's rootless mode — where container UID 0 maps to your non-root host user — code in the container cannot become root on your host.
The filter is an nftables rule installed inside Podman's network namespace on
each launch and verified before the session starts. On macOS/Windows that
namespace lives inside the Podman VM, so nothing on your host firewall is
touched.
Caveat — services on your LAN. Since all private ranges are blocked, a
LiteLLM/registry/proxy hosted on your LAN is blocked too. A public server
(e.g. https://ai.simonian.online) is unaffected. To allow a specific LAN
destination (macOS/Linux):
OPENCODE_ALLOW="192.168.0.50/32" opencode # permit one LAN host through the filteropencode's TUI doesn't listen on anything, but if the agent starts a dev server
(or you run opencode serve), publish the port(s) to reach them from the host:
OPENCODE_PUBLISH="3000" opencode # one port
OPENCODE_PUBLISH="3000 8000-8010" opencode # ports + a range
OPENCODE_PUBLISH_ADDR=0.0.0.0 OPENCODE_PUBLISH="3000" opencode # also reachable from the LANPorts publish to 127.0.0.1 by default (only your machine can reach them).
There is no "publish all 65535": privileged ports (<1024) need root to
bind, and a single already-used host port aborts the whole range — so publish the
specific ports/ranges you need.
| Var | Effect |
|---|---|
OPENCODE_PUBLISH |
Space/comma list of container ports/ranges to expose for inbound |
OPENCODE_PUBLISH_ADDR |
Host address to publish on (default 127.0.0.1; 0.0.0.0 = LAN-reachable) |
OPENCODE_ALLOW |
Extra destination CIDRs to permit through the egress filter (e.g. a LAN LiteLLM) |
OPENCODE_REQUIRE_ISOLATION=1 |
Refuse to start if the egress filter can't be confirmed (fail-closed) |
OPENCODE_NO_ISOLATION=1 |
Run on the default bridge with no egress filter (opt-out) |
If set in your shell, these are forwarded into the container:
ANTHROPIC_API_KEY, OPENAI_API_KEY, OPENROUTER_API_KEY, GEMINI_API_KEY,
GITHUB_TOKEN, GITLAB_TOKEN, AWS/Azure keys, OPENCODE_AUTH_CONTENT,
OPENCODE_CONFIG_CONTENT. For the LiteLLM setup above you don't need any of these.
- Tools run inside the image. opencode executes shell/build/test/git commands
inside the container, a slim Debian image bundling
git,ripgrep, Node.js (current LTS) + npm, and Python 3 + pip/venv (plusca-certificates). Sonpm/nodeandpython/pipwork out of the box; other runtimes (Go, Rust, Java, …) are not present and would need adding to the image. Your~/.gitconfigis mounted read-only so git identity works. - Browser/OAuth logins won't work from inside the container (a
localhostcallback can't reach your host browser). The LiteLLM API-key flow used here avoids that entirely. - macOS: projects must be under a path the Podman machine shares (
/Users,/Volumesby default). For other locations, re-init the machine with an extra-v <path>:<path>. - SELinux (Fedora/RHEL): the installer detects enforcing mode and relabels
bind mounts (
:z). - Windows: Podman uses a WSL2 backend;
install.ps1is authored but should be validated on a real Windows host before classroom use.
opencode (or any podman command) hangs / the TUI won't load. The Podman
machine (VM) is likely wedged — once podman info/podman pull hang, everything
does. Reset it:
podman machine stop; podman machine start
podman info >/dev/null && echo OK # retry once if it says "connection reset" (SSH not ready yet)The installer provisions the machine with 4 GiB to avoid the OOM-wedge the 2 GiB
default hits under load. If you still see wedges, give it more: podman machine stop && podman machine set --memory 6144 && podman machine start.
rm -rf ~/.opencode-container ~/.config/opencode-container
# then remove the block between the "# >>> opencode-container >>>" /
# "# <<< opencode-container <<<" markers from your shell rc (or $PROFILE)Podman itself is left installed (remove with brew uninstall podman /
winget uninstall RedHat.Podman / your package manager if you also want it gone).
Any existing Docker install is unaffected throughout.