The opencode AI coding agent from the
anomalyco/opencode releases
(formerly sst/opencode), as /usr/local/bin/opencode. The feature:
- downloads
https://github.com/anomalyco/opencode/releases/download/v<version>/opencode-linux-{x64,arm64}.tar.gz, checks it against your sha256 and installs theopencodebinary from it; - creates
~/.config/opencodeand~/.local/share/opencodefor the remote user, mode0700(see Keeping the login across rebuilds); - sets
OPENCODE_DISABLE_AUTOUPDATE=1in the container environment, so opencode doesn't check for or offer updates away from your pin (see Bumping the version).
It declares no mounts and sets no state-path variable such as
OPENCODE_CONFIG_DIR: where opencode's state and credentials live is your
choice, not the feature's.
The x64 build is the one opencode's own installer
(https://opencode.ai/install) picks for a CPU with AVX2. The releases
also carry opencode-linux-x64-baseline.tar.gz for x64 CPUs without
AVX2, and *-musl builds for Alpine; this feature installs neither.
This feature has no default version. You choose the opencode release and supply the sha256 of its tarball for each arch you build on, and the install fails unless it finds both. The download is always checked against your sha256.
opencode doesn't publish a checksums file, but GitHub shows each release
asset's sha256 (its digest), and you can compute it from the download
yourself:
VERSION=1.19.5
gh release view "v${VERSION}" --repo anomalyco/opencode --json assets \
--jq '.assets[] | select(.name == "opencode-linux-x64.tar.gz" or .name == "opencode-linux-arm64.tar.gz") | "\(.name) \(.digest | ltrimstr("sha256:"))"'
# or
for a in x64 arm64; do
printf '%s ' "opencode-linux-${a}.tar.gz"
curl -fsSL "https://github.com/anomalyco/opencode/releases/download/v${VERSION}/opencode-linux-${a}.tar.gz" | sha256sum
done| Arch | Asset |
|---|---|
| amd64 | opencode-linux-x64.tar.gz |
| arm64 | opencode-linux-arm64.tar.gz |
The version is the release tag without its v (1.19.5 for tag
v1.19.5); a leading v is rejected. You only need the sha for the
arches you actually build on.
The pin can come from three places. The feature uses the first one that sets either the version or a sha, and that place must set both: a version in one place is never paired with a sha from another.
To keep the pin out of devcontainer.json, feed the options from the
host's environment:
"ghcr.io/craftainer/devcontainer-features/cli-opencode:1": {
"version": "${localEnv:CLI_OPENCODE_VERSION}",
"sha256_amd64": "${localEnv:CLI_OPENCODE_SHA256_AMD64}",
"sha256_arm64": "${localEnv:CLI_OPENCODE_SHA256_ARM64}"
}Set CLI_OPENCODE_VERSION, CLI_OPENCODE_SHA256_AMD64 and
CLI_OPENCODE_SHA256_ARM64 with ENV in the Dockerfile your devcontainer
builds from, and leave the feature's options empty:
ENV CLI_OPENCODE_VERSION=1.19.5
ENV CLI_OPENCODE_SHA256_AMD64=588310a336fa3b9df36b3f0225b26f679d453efaad0cf3d66c3651ef3ec1647a
ENV CLI_OPENCODE_SHA256_ARM64=fae5be262d693b7c5e562856cc473e59e741772936db0c0600ac0d5ed5bd088b"build": { "dockerfile": "Dockerfile" },
"features": {
"ghcr.io/craftainer/devcontainer-features/cli-opencode:1": {}
}Put the same KEY=value lines in a file and COPY it into the image at
/etc/devcontainer-features/pins.env. One file can hold the pins of every
feature in this collection; each feature reads only its own keys.
# pins.env
CLI_OPENCODE_VERSION=1.19.5
CLI_OPENCODE_SHA256_AMD64=588310a336fa3b9df36b3f0225b26f679d453efaad0cf3d66c3651ef3ec1647a
CLI_OPENCODE_SHA256_ARM64=fae5be262d693b7c5e562856cc473e59e741772936db0c0600ac0d5ed5bd088bCOPY pins.env /etc/devcontainer-features/pins.envThe file is parsed, never sourced: KEY=value lines (optionally after
export), # comments and quoted values are understood, and nothing is
expanded. To keep it somewhere else, set the pins_file option to its
path in the image:
"ghcr.io/craftainer/devcontainer-features/cli-opencode:1": {
"pins_file": "/opt/pins/devcontainer.env"
}opencode checks for new versions in the background and shows a notice
when one is available; it doesn't install updates by itself, but /update
in the TUI and opencode upgrade do
(config: Autoupdate). This
feature sets OPENCODE_DISABLE_AUTOUPDATE=1 in containerEnv, the
documented switch that "disable[s] update checks and notices"
(CLI: Environment variables).
It has the same effect as "autoupdate": false in opencode.json, without
the feature writing your config.
To move to a newer release, change the pin: put the new version and its
two shas wherever you set the pin, and rebuild the container. Don't use
opencode upgrade or /update in this container. They don't replace
/usr/local/bin/opencode; depending on what they detect they fail, or
(with --method curl, or an opencode-ai npm package installed globally)
install a second, unpinned copy elsewhere, such as ~/.opencode/bin, that
can shadow the pinned one on PATH.
opencode keeps its global config (opencode.json, agents, plugins) in
~/.config/opencode, and its data in ~/.local/share/opencode: the
provider credentials you add with /connect in auth.json, session and
message history, and logs
(Troubleshooting: Storage,
Providers: Credentials).
It also uses ~/.cache/opencode (downloaded plugins and tools, which
opencode rebuilds when the directory is deleted) and
~/.local/state/opencode (run-time state); the docs put no credentials
in either, and this feature doesn't create them. A
rebuild discards all of them, so you sign in again each time unless you
choose one of the options below.
The feature declares no mount: a feature's mounts can't be switched off,
and whether credentials sit in a volume, and whether projects share one,
is a security choice that belongs to you. What it does is create
~/.config/opencode and ~/.local/share/opencode in the image, owned by
the remote user with mode 0700. Docker seeds an empty named volume with
the ownership of the directory it is mounted on, so volumes you mount
there start out writable by the remote user instead of owned by root.
In the snippets below, replace /home/vscode with your remoteUser's
home directory (/root if the container runs as root). The config volume
is optional: if you keep opencode.json in your repository or dotfiles
instead, mount only the data directory.
Each devcontainer gets its own volumes, named after its
${devcontainerId}. A login in one project isn't visible to another.
"mounts": [
"source=opencode-data-${devcontainerId},target=/home/vscode/.local/share/opencode,type=volume",
"source=opencode-config-${devcontainerId},target=/home/vscode/.config/opencode,type=volume"
]Sign in once and every devcontainer that mounts the same volume is signed in. The trade-off: every container that mounts it can read the same credentials, including a container for a repository you trust less.
"mounts": [
"source=opencode-data,target=/home/vscode/.local/share/opencode,type=volume",
"source=opencode-config,target=/home/vscode/.config/opencode,type=volume"
]Keep no credentials in the container at all and pass the provider's API
key in from the host's environment on every start. opencode loads a
provider whose API-key variable is set, without /connect; the variable
names come from models.dev, which opencode uses for
its provider list. Common ones:
| Provider | Variable |
|---|---|
| Anthropic | ANTHROPIC_API_KEY |
| OpenAI | OPENAI_API_KEY |
| Google (Gemini) | GOOGLE_GENERATIVE_AI_API_KEY or GEMINI_API_KEY |
| OpenRouter | OPENROUTER_API_KEY |
| Amazon Bedrock | AWS_BEARER_TOKEN_BEDROCK, or the usual AWS_* credentials |
The providers page lists the rest. History doesn't survive a rebuild, but nothing secret is stored in a volume, and revoking the key on the host side is enough. This is the recommended option for enterprise use.
"containerEnv": {
"ANTHROPIC_API_KEY": "${localEnv:ANTHROPIC_API_KEY}",
"OPENAI_API_KEY": "${localEnv:OPENAI_API_KEY}"
}This feature deliberately declares no installsAfter: the Dev Container
CLI fetches every installsAfter reference from ghcr.io, so declaring one
would make every build reach ghcr.io for features you may not use. Set
the order yourself with overrideFeatureInstallOrder in these two cases.
ssl-ca. If you use this collection'sssl-cafeature for a corporate root CA, the CA has to be trusted before opencode is downloaded, so listssl-cafirst.- A feature that creates the remote user, such as
ghcr.io/devcontainers/features/common-utilson an image without that user. This feature creates its state directories in the remote user's home, and the install fails if that home doesn't exist yet, so list the user-creating feature before this one.
"overrideFeatureInstallOrder": [
"ghcr.io/devcontainers/features/common-utils",
"ghcr.io/craftainer/devcontainer-features/ssl-ca",
"ghcr.io/craftainer/devcontainer-features/cli-opencode"
]opencode is a single Bun executable, with its own
bundled root CAs. Its network
docs name NODE_EXTRA_CA_CERTS for trusting a custom CA ("This works for
both proxy connections and direct API access"). Bun also honours
NODE_USE_SYSTEM_CA=1, which adds the operating system's trust store, and
SSL_CERT_FILE / SSL_CERT_DIR, which add CA certificates on top of the
bundled ones
(Bun environment variables). ssl-ca
sets all four in containerEnv: NODE_EXTRA_CA_CERTS and SSL_CERT_FILE
to the system bundle /etc/ssl/certs/ca-certificates.crt it updates,
SSL_CERT_DIR to /etc/ssl/certs, and NODE_USE_SYSTEM_CA to 1 (the
full list of twelve is in its NOTES.md). So a root CA it installs is
trusted with no further setting. Without ssl-ca, set
NODE_EXTRA_CA_CERTS to your CA's PEM file in containerEnv.
opencode's TUI talks to a local HTTP server, so if you set HTTPS_PROXY
or HTTP_PROXY, also set NO_PROXY=localhost,127.0.0.1 (same page).
Debian and Ubuntu (glibc), on amd64 and arm64. The x64 build needs a CPU
with AVX2. The install adds curl and ca-certificates if either is
missing.
opencode is MIT-licensed (LICENSE). Its release tarball holds only the binary, so the feature installs no license file.