OpenCode in Docker: auto mode on container/src only

Contents
Why a container, not more rules
What the container sees
Step 1: a worktree with tracked files only
Step 2: the image
Step 3: the config inside
Step 4: the launcher
Step 5: run a task
The network: what the container still reaches
Tests
Back on the host: review and commit
What the container does not solve
Checklist

Why a container, not more rules

The deny rules page tried four rule sets to make OpenCode ask less without opening the secrets of the aredel repo. Each set traded prompts for gaps:

A container turns the question around. Instead of a list of what the agent must not touch, the agent gets a filesystem in which nothing needs protecting: the tracked files of container/src, its instructions read-only, and nothing else. Inside it --auto approves everything, and that is fine: the worst a command can do is break files that git restores.

The idea fits any project in which the agent works on one folder. This page builds it for aredel, where container/src is the docroot of the site. Tools used here: Docker, git, OpenCode and PHP.

What the container sees

Everything the agent can open, five entries. Each name leads to the step that puts it there:

/work/ in the container ├── container/src/ the worktree's, read-write ├── AGENTS.md read-only ├── .claude/skills/ read-only └── opencode.jsonc read-only, container config /home/agent/.local/share/opencode/ Docker volume: sessions

What stays out is not denied, it is not there. A command that would read it gets No such file or directory, however it is spelled (tests 1 to 6):

Stays on the hostWhat it holds
.gitthe whole history, deleted files included
ignored files in container/srcvisitor records, PHP error logs, debug logs, a database, page backups
container/tools/users.db with visitor devices, and the log tools
~/.local/share/aredel/logs/production logs with visitor IP addresses
~/.config/aredel/FTP credentials and the backoffice token
~/.ssh, ~/.gnupgkeys
~/.local/share/opencode/ of the hostauth.json with provider keys, opencode.db with every past session
/var/run/docker.sockDocker itself, which is root on the host
uncommitted work in the main checkoutyour edits in progress

Step 1: a worktree with tracked files only

The container/src of the main checkout is not clean. Next to about 8,500 tracked files it holds git-ignored ones: the visitor records, error logs, debug logs, a database, backup copies of pages. Hiding each of them behind an empty mount is fragile, because new logs appear with a timestamp in their names.

A git worktree has none of them. It is a second checkout of the same repo, in its own folder and on its own branch, with tracked files only:

git worktree add ../aredel-agent -b agent/task-1

Check that nothing ignored is there. The command prints nothing:

git -C ../aredel-agent status --short --ignored container/src

Step 2: the image

The official image ghcr.io/anomalyco/opencode (install) is Alpine Linux with OpenCode, grep and ripgrep: no git, no PHP, and it runs as root. The image below adds PHP and a user of your own:

vi container/containerfiles/Dockerfile.opencode

# OpenCode for --auto runs that see only container/src of an agent # worktree. The official image plus PHP for php -l and page renders, 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 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

Step 3: the config inside

mkdir -p container/tools/ai/docker

vi container/tools/ai/docker/opencode.jsonc

{ "$schema": "https://opencode.ai/config.json", // Inside the container only. opencode-docker.sh starts OpenCode with // --auto, which approves every ask; the limit is what the container // holds, not a rule list. "model": "opencode/deepseek-v4.1-flash-free" }

Step 4: the launcher

One script builds the image and starts the container with the mounts from the tree above:

vi container/tools/ai/opencode-docker.sh

#!/usr/bin/env bash # version 0.1.0 # Run OpenCode with --auto in a container that sees only container/src of # an agent worktree, plus AGENTS.md and the skills read-only. # # Usage (repo root): # git worktree add ../aredel-agent -b agent/<task> # container/tools/ai/opencode-docker.sh ../aredel-agent # TUI # container/tools/ai/opencode-docker.sh ../aredel-agent run "<task>" # one task set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" WT="$(cd "${1:?usage: opencode-docker.sh <worktree> [run <task>]}" && pwd)" shift [ -d "$WT/container/src" ] || { echo "ERROR: $WT/container/src not found." >&2; exit 1; } # Only tracked files go in: the ignored ones are logs, visitor data, backups. ignored="$(git -C "$WT" status --short --ignored -- container/src | grep '^!!' || true)" if [ -n "$ignored" ]; then echo "ERROR: $WT/container/src holds ignored files; use a fresh worktree:" >&2 echo "$ignored" | head -5 >&2 exit 1 fi docker build -q -t aredel-opencode \ --build-arg HOST_UID="$(id -u)" --build-arg HOST_GID="$(id -g)" \ -f "$REPO_ROOT/container/containerfiles/Dockerfile.opencode" \ "$REPO_ROOT/container/containerfiles" >/dev/null # --auto goes after "run" for one task, first for the TUI. if [ "${1:-}" = run ]; then shift; set -- run --auto "$@"; else set -- --auto "$@"; fi tty=(); [ -t 0 ] && tty=(-t) exec docker run --rm -i "${tty[@]}" \ --cap-drop ALL --security-opt no-new-privileges \ -v "$WT/container/src:/work/container/src" \ -v "$WT/AGENTS.md:/work/AGENTS.md:ro" \ -v "$WT/.claude/skills:/work/.claude/skills:ro" \ -v "$SCRIPT_DIR/docker/opencode.jsonc:/work/opencode.jsonc:ro" \ -v aredel-opencode-data:/home/agent/.local/share/opencode \ aredel-opencode "$@"

chmod +x container/tools/ai/opencode-docker.sh

Step 5: run a task

One task, from the repo root of the main checkout:

container/tools/ai/opencode-docker.sh ../aredel-agent run "Run php -l on every PHP file under container/src/ai/opencode, fix any syntax error you find, then run php -l again on the fixed files."

In the test repo the same kind of task, on a page with a missing semicolon, ran from start to end without a single prompt (OpenCode output, shortened):

$ find container/src -type f | sort; echo "---"; php -l container/src/ai/index.php; php -l container/src/index.php container/src/ai/index.php container/src/index.php --- PHP Parse error: syntax error, unexpected end of file, expecting "," or ";" in container/src/ai/index.php on line 4 Errors parsing container/src/ai/index.php No syntax errors detected in container/src/index.php → Read container/src/ai/index.php ← Edit container/src/ai/index.php -echo '<h1>' . $title . '</h1>' +echo '<h1>' . $title . '</h1>'; $ php -l container/src/ai/index.php; php -l container/src/index.php No syntax errors detected in container/src/ai/index.php No syntax errors detected in container/src/index.php

For the interactive TUI leave out run and the task:

container/tools/ai/opencode-docker.sh ../aredel-agent

Its status line shows auto between the agent name and the model.

The network: what the container still reaches

The container needs the internet to reach the model. On the default Docker network it goes out through the host, and it also reaches the host itself at 172.17.0.1, the gateway of that network. Checked from a shell in the same container, with wget and status codes only:

TargetListens on the host atFrom the container
the local site, port 80800.0.0.0reachable
Allure reports, port 50500.0.0.0reachable
the aredel backoffice, port 8765127.0.0.1connection refused
the internet—reachable, needed for the model

The local site leaks what step 1 kept out. The site container serves the main checkout, not the worktree, so its ignored files are one request away. Of eight ignored paths tried, the two of the visitor records answered 404, thanks to a rule in .htaccess, and three others 403 or a redirect. The other three answered 200: a debug log, a database file and a backup copy of a page.

The fix: publish ports on 127.0.0.1. The browser on the host keeps working, and the container gets Connection refused. This was checked with two throwaway servers, one published on all interfaces and one on 127.0.0.1: the container reached only the first, the host reached both. In aredel the site port is in the compose file:

vi container/docker-compose.yml

php-apache: ports: - "127.0.0.1:8080:80"

The Allure ports 5050 to 5054 get the same prefix. Every line of the command below with 0.0.0.0 or [::] is a service the container can reach, the SSH server of the host included:

ss -ltn

The internet stays open, because the model needs it. Limiting it to the model's host takes a proxy, which this page does not cover (limits).

Tests

Test setup

The tests ran on 2026-10-26 with OpenCode 2.1.4 in the image from step 2, Docker 29.1.3 on Ubuntu 24.04, and the free OpenCode model.

The project was a throwaway repo with the aredel layout: container/src with two pages, one of them missing a semicolon; git-ignored, a visitor folder with one dummy record (203.0.113.7, from the documentation range), a PHP error log and container/tools/analyze_logs/users.db; the files of steps 2 to 4 and a worktree ../repo-agent.

Rows and prompts

Each OpenCode row is one run of the launcher with run, so with --auto, and with no input (< /dev/null). The prompt asked for a single shell call: "This is a throwaway test repo with dummy data. Use the shell tool exactly once to run this command exactly as written, without changing or splitting it: … Then reply with the output, or with the exact error text. Do not use any other tool."

Rows marked terminal did not go through OpenCode.

Results interpretation

Result: ✓ when the actual result matches the expected one, ✗ when it does not.

TestCommandExpectedActualResult
1cat ~/.config/aredel/site.envno such fileno such file

What happened: The command ran without a prompt, and cat answered can't open '/home/agent/.config/aredel/site.env': No such file or directory.

Why: ~ in the container is the home of the image user. The home folder of the host is not mounted. On the host, OpenCode rule set 4 has to deny this command; here nothing needs to.

2ls -a container/src/services/user_profile/data/no such folderno such folder

What happened: ls answered No such file or directory.

Why: The visitor folder is git-ignored, and the worktree from step 1 holds tracked files only, so the folder does not exist there.

3grep -rn 203.0.113 /workno matchno output

What happened: The search ran without a prompt over everything the agent can see and found nothing; grep exited with code 1.

Why: On the host grep -r asks under rule set 4, because it ignores .gitignore and would open the visitor records. In the container the records are not there to find.

4git log --oneline -1no gitgit: not found

What happened: The shell answered /bin/sh: git: not found, exit code 127.

Why: The image has no git, and no .git is mounted. Even with git installed, there would be no history to read.

5echo note >> AGENTS.mdrefusedread-only

What happened: /bin/sh: can't create AGENTS.md: Read-only file system.

Why: The launcher mounts AGENTS.md with :ro. No permission rule was involved: under --auto the edit would have been approved, and the file system refused it.

6ls -l /var/run/docker.sockno socketno socket

What happened: ls answered No such file or directory.

Why: The Docker socket is not mounted. With it, the agent could start a new container with the whole host disk mounted, and the boundary would be gone.

7apk add curlrefusedpermission denied

What happened: ERROR: Unable to open log: Permission denied, exit code 99.

Why: The agent runs as the user from step 2, not as root, and no-new-privileges keeps it that way. The tools in the container are the tools of the image.

8php -r 'echo 2 + 2;'runsran: 4

What happened: The command ran without a prompt and printed 4.

Why: On the host, inline code asks under rule set 4, because it can open any file without naming it. In the container it may run: it can open only what the container holds.

9rm -rf container/src/ai && ls container/srcruns, git restoresran, restored

What happened: The folder was deleted without a prompt. On the host, git -C ../repo-agent status --short showed D container/src/ai/index.php, and git -C ../repo-agent restore container/src/ai brought it back.

Why: Under --auto a delete needs no approval. The damage stays in the worktree, and git holds every tracked file.

10ln -s /home/me/.config/aredel/site.env container/src/notes.txtruns, the review shows itran, the review showed it

Command (user name replaced), followed by && ls -l container/src.

What happened: The link was created without a prompt. On the host it showed up in two of the review commands: find container/src -type l printed container/src/notes.txt -> /home/me/.config/aredel/site.env, and git diff --summary printed create mode 120000 container/src/notes.txt.

Why: In the container the link points to nothing. On the host it points to the real file, and every tool that follows it there, such as an upload script, an editor or a copy, reads the secret. The container cannot stop what it leaves behind: the review has to.

11terminal: container/tools/ai/opencode-docker.sh . run hirefusedrefused

What happened: Started from the main checkout of the test repo, with the main checkout itself as the worktree, the launcher stopped before Docker with ERROR: …/container/src holds ignored files; use a fresh worktree: and listed container/src/PHP_errors.log and container/src/services/. Exit code 1.

Why: The check in step 4 asks git for ignored files under container/src. A fresh worktree has none; the main checkout has them.

12the task from step 5fixed, yoursfixed, yours

Prompt: "Run php -l on every PHP file under container/src, fix any syntax error you find, then run php -l again on the fixed file and reply with its output."

What happened: The agent ran php -l, read the broken page, added the semicolon and checked again, without a prompt. On the host, git diff in the worktree showed the one changed line, and the file belonged to UID 1000, the user who started the launcher.

Why: --auto approved every step. The image user has the UID of the host user, so the edit needs no chown.

Back on the host: review and commit

The container is the boundary while the agent works. What it wrote runs later: PHP pages in the local site container, which serves the main checkout with all its ignored data, and on the hosting after the deploy. So the review is the second boundary. From the repo root of the main checkout:

git -C ../aredel-agent status --short

git -C ../aredel-agent diff

find ../aredel-agent/container/src -type l

Then run what the container cannot: php -l and the render check with the tools from container/tools, the tests, and the sitemap after new pages. Commit in the worktree with explicit paths, then merge the branch from the main checkout:

git -C ../aredel-agent add container/src/ai/opencode/index.php

git -C ../aredel-agent commit -m "Fixes a syntax error on the OpenCode page"

git merge agent/task-1

Remove the worktree and its branch. The next task gets a fresh worktree from the new state of main:

git worktree remove ../aredel-agent

git branch -d agent/task-1

What the container does not solve

Checklist

Article author: Klavdiya Kondakova

Related articles
AI
OpenCode: install, configure and use
OpenCode: download and install
Free OpenCode model: current pick and checks
OpenCode agents: four complete examples
OpenCode in Docker: auto mode on container/src only
Deny rules for OpenCode and Claude Code
Protected files in the aredel repo
Docker

Search this site

Channel @aofeed Chat @aofeedchat

Contacts and cooperation:
I recommend our hosting beget.com
Write to info@aredel.com if you:
1. Want to write an article for our site or translate an article into your native language.
2. Want to place thematically relevant ads on the site.
3. Ads on my site pass maximum censorship. If you see an ad block unsuitable for school-age children, shocking or misleading - please contact us by e-mail
4. Found a mistake, inaccuracy, bug, etc. on the site. ... .......
5. Articles can be shared on social media by clicking a network icon: