The image of OpenCode in Docker: PHP by default, Erlang, Ruby, Go and other languages

Introduction

This article is part of a larger manual, OpenCode in Docker on Ubuntu 24.04.

The manual shows how to run OpenCode with the --auto option inside a Docker container.

With --auto the agent does not ask you before each command, so the container limits what it can reach: the agent works only in one git worktree, a separate copy of the project.

This article is step 3 (the image) of the manual in full.

It explains why the official OpenCode image is not enough, and shows the default image, which adds PHP and other programs.

Then it shows two more images: one with Erlang and one with Ruby.

The launcher uses them when you start a run with --erlang or --ruby.

At the end you learn how to make an image for another language.

All Dockerfiles are in the folder container/containerfiles.

Why your own image

The image pulled in step 1 (Docker and the OpenCode image) runs OpenCode, but not the checks of this project. It runs as root and has none of the programs the skills call:

docker run --rm --entrypoint sh ghcr.io/anomalyco/opencode:2.1.4 -c 'id; php -v; bash --version; python3 --version; perl -v'

uid=0(root) gid=0(root) groups=0(root),0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video) sh: php: not found sh: bash: not found sh: python3: not found sh: perl: not found

And what it writes into a mounted folder belongs to root on the host. From an empty folder:

docker run --rm -v "$PWD:/work" --entrypoint sh ghcr.io/anomalyco/opencode:2.1.4 -c 'touch /work/page.php'

ls -l page.php

-rw-r--r-- 1 root root 0 Oct 26 23:49 page.php

echo '<?php' >> page.php

bash: page.php: Permission denied

The default image

So you build your own image on top of it.

FROM takes the pulled image, and the lines after it add the programs and a user with your UID:

vi container/containerfiles/Dockerfile.opencode

# OpenCode for --auto runs on an agent worktree. The official image, plus # PHP for php -l and page renders, bash, Python, Perl and the GNU tools for # the scripts in container/tools, and a user with your UID, so the files the # agent writes stay yours. FROM ghcr.io/anomalyco/opencode:2.1.4 RUN apk add --no-cache bash python3 perl grep sed findutils coreutils \ php83 php83-ctype php83-dom php83-iconv php83-mbstring php83-session \ php83-simplexml php83-tokenizer php83-xml \ && ln -s /usr/bin/php83 /usr/local/bin/php ARG HOST_UID=1000 ARG HOST_GID=1000 RUN addgroup -g ${HOST_GID} agent \ && adduser -D -u ${HOST_UID} -G agent agent \ && mkdir -p /work && chown agent:agent /work USER agent # Created by the user, so a new named volume mounted here is writable. RUN mkdir -p /home/agent/.local/share/opencode WORKDIR /work

Build and check it

You do not have to build it by hand: the launcher script (page The launcher) runs docker build at every start, so a change to the Dockerfile takes effect at the next run.

The first build took 33 seconds, later ones come from the cache. To build it once yourself and compare:

docker build -t opencode-agent --build-arg HOST_UID=$(id -u) --build-arg HOST_GID=$(id -g) -f container/containerfiles/Dockerfile.opencode container/containerfiles

docker run --rm --entrypoint sh opencode-agent -c 'id; php -v | head -1; bash --version | head -1; python3 --version; perl -v | sed -n 2p'

uid=1000(agent) gid=1000(agent) groups=1000(agent) PHP 8.3.36 (cli) (built: Oct 21 2026 16:38:20) (NTS) GNU bash, version 5.3.9(1)-release (x86_64-alpine-linux-musl) Python 3.14.8 This is perl 5, version 42, subversion 2 (v5.42.2) built for x86_64-linux-thread-multi

The image is 354 MB, 94 MB more than the official one.

The image with Erlang

A task that checks the Erlang pages has to run their examples, and the default image has no erl.

The Erlang image is the default file with one more package at the end of the apk line, erlang, and a first comment that says so:

vi container/containerfiles/Dockerfile.erlang.opencode

# The image of Dockerfile.opencode plus Erlang/OTP (erl, erlc, escript), so # the agent can run the examples of the Erlang pages. OpenCode for --auto # runs on an agent worktree. The official image, plus PHP for php -l and page # renders, bash, Python, Perl and the GNU tools for the scripts in # container/tools, and a user with your UID, so the files the agent writes # stay yours. FROM ghcr.io/anomalyco/opencode:2.1.4 RUN apk add --no-cache bash python3 perl grep sed findutils coreutils \ php83 php83-ctype php83-dom php83-iconv php83-mbstring php83-session \ php83-simplexml php83-tokenizer php83-xml \ erlang \ && ln -s /usr/bin/php83 /usr/local/bin/php ARG HOST_UID=1000 ARG HOST_GID=1000 RUN addgroup -g ${HOST_GID} agent \ && adduser -D -u ${HOST_UID} -G agent agent \ && mkdir -p /work && chown agent:agent /work USER agent # Created by the user, so a new named volume mounted here is writable. RUN mkdir -p /home/agent/.local/share/opencode WORKDIR /work

Compared with the default file, only the comment and that line differ:

diff container/containerfiles/Dockerfile.opencode container/containerfiles/Dockerfile.erlang.opencode

1,4c1,6 < # OpenCode for --auto runs on an agent worktree. The official image, plus < # PHP for php -l and page renders, bash, Python, Perl and the GNU tools for < # the scripts in container/tools, and a user with your UID, so the files the < # agent writes stay yours. --- > # The image of Dockerfile.opencode plus Erlang/OTP (erl, erlc, escript), so > # the agent can run the examples of the Erlang pages. OpenCode for --auto > # runs on an agent worktree. The official image, plus PHP for php -l and page > # renders, bash, Python, Perl and the GNU tools for the scripts in > # container/tools, and a user with your UID, so the files the agent writes > # stay yours. 8a11 > erlang \

The launcher builds it as opencode-agent-erlang when a run starts with --erlang (the image per run: --erlang and --ruby).

A shell in it, on the worktree of step 2 (a worktree for the agent):

container/tools/ai/opencode-docker.sh --erlang ../site-agent shell

At the /work $ prompt, the release:

erl -noshell -eval 'io:format("~s~n", [erlang:system_info(otp_release)]), halt().'

27

A module compiled with erlc and run with erl, in /tmp, where scratch files belong:

cd /tmp

vi hello.erl

-module(hello). -export([hi/0]). hi() -> io:format("hi from a module~n").

erlc hello.erl

erl -noshell -s hello hi -s init stop

hi from a module

And a script for escript:

vi s.escript

#!/usr/bin/env escript main(_) -> io:format("hi from escript~n").

escript s.escript

hi from escript

The image is 435 MB, 81 MB more than the default one.

The package erlang of Alpine 3.24 is Erlang/OTP 27; erlang28 would give OTP 28 (another language).

The image with Ruby

The same for the Ruby pages: the default file plus the package ruby, which brings irb, gem and erb along:

vi container/containerfiles/Dockerfile.ruby.opencode

# The image of Dockerfile.opencode plus Ruby (ruby, irb, gem, erb), so the # agent can run the examples of the Ruby pages. OpenCode for --auto runs on # an agent worktree. The official image, plus PHP for php -l and page # renders, bash, Python, Perl and the GNU tools for the scripts in # container/tools, and a user with your UID, so the files the agent writes # stay yours. FROM ghcr.io/anomalyco/opencode:2.1.4 RUN apk add --no-cache bash python3 perl grep sed findutils coreutils \ php83 php83-ctype php83-dom php83-iconv php83-mbstring php83-session \ php83-simplexml php83-tokenizer php83-xml \ ruby \ && ln -s /usr/bin/php83 /usr/local/bin/php ARG HOST_UID=1000 ARG HOST_GID=1000 RUN addgroup -g ${HOST_GID} agent \ && adduser -D -u ${HOST_UID} -G agent agent \ && mkdir -p /work && chown agent:agent /work USER agent # Created by the user, so a new named volume mounted here is writable. RUN mkdir -p /home/agent/.local/share/opencode WORKDIR /work

A shell with --ruby:

container/tools/ai/opencode-docker.sh --ruby ../site-agent shell

At the /work $ prompt:

ruby -v

ruby 3.4.12 (2026-10-23 revision 366f5fdc00) +PRISM [x86_64-linux-musl]

ruby -e 'puts [3, 1, 2].sort.inspect'

[1, 2, 3]

gem -v

3.6.9

The image is 373 MB. The first start with --ruby, which built it, took 56 seconds.

Another language

An image for another language takes four steps.

1. Find the package. The packages come from Alpine Linux, the system of the official image.

The agent cannot install anything, so look the names up as root in a throwaway container of the default image.

The container ends with the command, and nothing of it stays:

docker run --rm --user root --entrypoint sh opencode-agent -c 'apk update -q >/dev/null 2>&1; apk search -e go ruby erlang nodejs npm openjdk21-jdk rust cargo elixir gcc musl-dev lua5.4 dotnet9-sdk'

cargo-1.96.1-r0 dotnet9-sdk-9.0.123-r0 elixir-1.19.7-r0 erlang27-27.3.4.19-r0 erlang28-28.5.0.8-r0 gcc-15.2.0-r5 go-1.26.10-r0 lua5.4-5.4.8-r0 musl-dev-1.2.6-r3 nodejs-24.22.1-r1 nodejs-current-26.12.1-r0 npm-11.12.1-r0 openjdk21-jdk-21.0.13.0.3_p1-r0 ruby-3.4.12-r0 rust-1.96.1-r0

-e searches exact names. Each line is a package and its version in Alpine 3.24.

erlang itself is not a line: it is a name that erlang27 provides, and apk add erlang installs that one. What these packages give:

LanguagePackagesChecked here
Erlangerlang (OTP 27) or erlang28built and run: the image with Erlang
Rubyrubybuilt and run: the image with Ruby
Gogobuilt and run: example: Go
Node.jsnodejs npm, or nodejs-current for the newer linein the list
Javaopenjdk21-jdkin the list
Rustrust cargoin the list
Elixirelixirin the list
Cgcc musl-devin the list
Lualua5.4in the list
C#dotnet9-sdkin the list

2. Write the Dockerfile. Copy the default file to Dockerfile.<language>.opencode, add the packages at the end of the apk add line, before && ln, and say in the first comment what the image adds.

3. Give the launcher a flag for it, one line in the script (add a flag for another language).

4. Check it in a shell of the new image, as for the image with Erlang, and note its size.

Example: Go

Go, step by step. The file:

vi container/containerfiles/Dockerfile.go.opencode

# The image of Dockerfile.opencode plus Go (go, gofmt), so the agent can run # the examples of the Go pages. FROM ghcr.io/anomalyco/opencode:2.1.4 RUN apk add --no-cache bash python3 perl grep sed findutils coreutils \ php83 php83-ctype php83-dom php83-iconv php83-mbstring php83-session \ php83-simplexml php83-tokenizer php83-xml \ go \ && ln -s /usr/bin/php83 /usr/local/bin/php ARG HOST_UID=1000 ARG HOST_GID=1000 RUN addgroup -g ${HOST_GID} agent \ && adduser -D -u ${HOST_UID} -G agent agent \ && mkdir -p /work && chown agent:agent /work USER agent RUN mkdir -p /home/agent/.local/share/opencode WORKDIR /work

With the flag --go added to the launcher (add a flag for another language), a shell in it:

container/tools/ai/opencode-docker.sh --go ../site-agent shell

At the /work $ prompt:

go version

go version go1.26.10 linux/amd64

cd /tmp

vi hello.go

package main import "fmt" func main() { fmt.Println("hi from go") }

go run hello.go

hi from go

Tested in a throwaway worktree, with the file and the flag added there only.

The first start built the image in 106 seconds, and it is 737 MB: the Go toolchain alone is 177 MB on disk.

Images on disk

Every image the launcher has built stays until you remove it:

docker image ls 'opencode-agent*'

IMAGE ID DISK USAGE CONTENT SIZE EXTRA opencode-agent-erlang:latest d12ea898f281 435MB 0B U opencode-agent-ruby:latest f11ce3d8efce 373MB 0B opencode-agent:latest 9430341af54f 354MB 0B

U marks an image that a container uses right now.

The disk usage of the images does not simply add up: they share the layers of the official image. To remove one you no longer need:

docker image rm opencode-agent-ruby

The next run with its flag builds it again.

Article author: Klavdiya Kondakova