OpenCode in Docker: problems met on real tasks and what to do instead
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.
The first real tasks with this setup were a new
Perl course and a plan for the errors in
yesterday's access log.
Below is what went wrong, why, and what to do instead.
Tools used here: git,
Docker, awk.
A new run does not know the last one
Every run starts a new session. Asked to repeat
its report in English, the agent of a second run answered that the
conversation had just started.
opencode run takes
--continue for the last session and
--session for a given one, and the launcher
passes both on:
container/tools/ai/opencode-docker.sh ../site-agent run --session <id> "Repeat your report in English."
Name the session, not --continue, when two
agents run.
All their containers keep sessions in the same volume, so
--continue takes the newest session of either.
Started with the worktree of the other task, a resumed agent looked for
the files it had written and for its notes in
/tmp, found neither, and waited:
ls: cannot access '/work/container/src/dev/perl': No such file or directory ls: cannot access '/tmp/agentwork': No such file or directory
The files were in the other worktree, and /tmp
had gone with the container of the earlier run.
The ids are in the shell of step 6
(check what the container sees), newest first, with the time in UTC,
the clock of the container:
opencode session list
ses_e8351f6a7ffehA4yRD0x6NlBtH Untitled session 10/27/2026, 10:09:11 AM ses_e83357c41ffeT3ReWox83y3CJF Untitled session 10/27/2026, 10:07:43 AM
A run started with --title doc-test is listed by its title, which tells the tasks apart:
ses_e8296785dffeIxdSRidoUwdNLJ doc-test 10/27/2026, 12:50:34 PM
The agent sees only the worktree
The log task named a file in an ignored folder of the main checkout.
The container does not have it, so the agent found nothing.
A run that finds nothing can still write a plan, from old notes in the
repo.
Do not mount the log instead: it holds the IP addresses and browsers of
visitors, and a free model may keep what it is sent.
Cut out on the host what the task needs, into a folder that is mounted
and not ignored:
awk '$9 == 404 || $9 >= 500 {print $9, $4, $7}' access.log > ../site-agent/container/docs/errors.txt
cat ../site-agent/container/docs/errors.txt
404 [26/Oct/2026:10:15:11 /old/page.php 500 [26/Oct/2026:10:16:40 /ru/x/
- Status, time and path only. In the combined log format field 9 is the status, 4 the time and 7 the path. Read the extract before the run: a query string can hold an email address or a token.
- Mounted read-only. container/docs is one of the read-only folders, and the launcher accepts a new untracked file; it refuses only ignored ones.
- Delete it after the task. It is not for a commit.
"All checks pass" covers what the agent checked
The report of the course listed php -l, page
renders, tag balance and the sitemap: checks of the
HTML.
None ran the Perl, although Perl is in the image, and one lesson said
that my $last = (1..5); gives 5.
Run such an example in the image yourself, without network and with
nothing else mounted:
vi demo.pl
my $last = (1..5); print "[$last]\n";
docker run --rm --network none --cap-drop ALL -v "$PWD/demo.pl:/tmp/demo.pl:ro" --entrypoint perl opencode-agent -w /tmp/demo.pl
Use of uninitialized value $. in range (or flip) at /tmp/demo.pl line 1. []
In scalar context .. is the flip-flop operator,
not a range, and the value is empty.
A second run fixed the lesson and added real output to 51 pages of the
course.
Of 126 examples with output, 116 printed exactly what the page shows.
The other 10 depend on files the lesson creates first, a module that is
not in the image, the machine, or the order of normal and error output.
The same run broke a page after its own php -l:
a shell command with single quotes inside the single-quoted PHP string
of the page.
The commit hook in the worktree caught it, once the hooks worked there.
Commit hooks in a worktree
A worktree uses the hooks of the repo, but git
runs them differently there: it hands the hook
GIT_DIR, the path of the worktree's git
folder, and no GIT_WORK_TREE.
Then git -C <folder> takes that folder for
the top of the work tree.
A hook script that finds its files that way stops with an error, or
finds no files and passes. In a throwaway repo:
mkdir -p ~/hooktest/main
cd ~/hooktest/main
git init -q
mkdir -p a/b
touch a/b/f
git add .
git commit -qm init
vi .git/hooks/pre-commit
#!/bin/sh echo "GIT_DIR=$GIT_DIR" git -C a/b rev-parse --show-toplevel exit 1
chmod +x .git/hooks/pre-commit
git commit --allow-empty -m test
GIT_DIR= /home/me/hooktest/main
exit 1 stops each test commit, so nothing is committed. The same commit in a worktree:
git worktree add -q ../wt -b side
cd ../wt
git commit --allow-empty -m test
GIT_DIR=/home/me/hooktest/main/.git/worktrees/wt /home/me/hooktest/wt/a/b
With unset GIT_DIR GIT_WORK_TREE in place of the echo line, git finds the worktree from the folder again, and the same commit prints the right top:
/home/me/hooktest/wt
- The fix goes into every hook script that calls git -C, as the first line after set -euo pipefail. In the main checkout git sets no GIT_DIR, so nothing changes there.
- A check inside a service container, such as php -l in the container of the local site, checks the files that container mounts: the main checkout, not the worktree. Run it in a one-off container on the worktree.
- A hook that marks a new include as a page. A check that treated every new .php file as a page failed on a file in .php/, a folder of includes. It now skips folders whose names start with a dot.
main moved on while the agent worked
When the course was ready, its worktree was ten commits behind
main, feed updates among them.
Commit on an old base, and those files conflict at the merge.
Bring the worktree up to date first; staged files stay staged, checked
in a throwaway worktree:
git -C ../site-agent merge --ff-only main
A hook that runs after the commit and stages files, such as a feed of
new pages, leaves them for a second commit.
Make it in the worktree too, so git merge
--ff-only brings both.
Another agent in your folder while you commit
pre-commit puts your unstaged changes aside while the hooks run and
brings them back afterwards.
A commit in the main checkout failed with files were
modified by this hook on a hook that changes nothing.
Another agent, working in the same folder, had edited a file during that
minute.
An edit made in that window can also be lost when pre-commit brings your
changes back.
List the agents and their folders before you commit (user name
replaced):
for p in $(pgrep -x 'claude|opencode'); do echo "$p $(readlink /proc/$p/cwd)"; done
13509 /home/me/github/site 24610 /home/me/github/site 40276 /home/me
The list includes the agent you may be asking to run it.
Commit when no other agent works in that folder, or give each of them a
worktree too, as Claude Code in
two agents: one branch
each.
The free model has a usage limit
After a day of runs, a new one stopped at the first request:
Error: Free usage limit reached. Connect a provider or subscribe to OpenCode Go to keep going, or switch models.
Wait, or continue the same task on an OpenCode Go model or a model
through OpenRouter.
Read the section When the
free model reaches its usage limit of the day to day article to
learn how.
Article author: Klavdiya Kondakova