OpenCode в Docker на Ubuntu 24.04: безопасная настройка по шагам

Введение

Это руководство запускает OpenCode с флагом --auto, то есть без единого подтверждения, внутри контейнера Docker на Ubuntu 24.04.

Контейнер видит один git worktree проекта: container/src, докрут сайта на PHP, — на чтение и запись; инструкции, документацию и container/tools — только на чтение; ничего из вашей домашней папки. Что агент сломает, git восстановит. Чего он не должен читать, того здесь нет.

Раскладка репозитория, о которой идёт речь, вместе с файлами настройки; два образа с дополнительным языком необязательны.

Пометки справа говорят, что агент получает в контейнере:

site/ ваш репозиторий ├── AGENTS.md только чтение: правила ├── .claude/skills/ только чтение └── container/ ├── src/ чтение и запись: здесь работает агент ├── docs/ только чтение ├── tests/ только чтение ├── containerfiles/ │ ├── Dockerfile.opencode образ │ ├── Dockerfile.erlang.opencode образ плюс Erlang │ └── Dockerfile.ruby.opencode образ плюс Ruby └── tools/ только чтение └── ai/ ├── opencode-docker.sh лаунчер └── docker/ ├── opencode.jsonc модель ├── CONTAINER.md заметки для агента └── masked пустой файл

Шаг 1: Docker и образ OpenCode

Для настройки нужны Docker на хосте и официальный образ OpenCode той версии, которая у вас запущена: ваш собственный образ из шага 3 (образ) собирается поверх него. Проверьте все три вещи:

docker --version

Docker version 29.1.3, build 29.1.3-0ubuntu3~24.04.2

opencode --version

opencode v2.1.4

docker image inspect --format '{{.RepoTags}}' ghcr.io/anomalyco/opencode:2.1.4

[ghcr.io/anomalyco/opencode:2.1.4]

Тег в последней команде — это версия, которую вывела opencode --version.

Если образа нет, эта команда печатает Error response from daemon: No such image и завершается с кодом 1.

Как установить Docker, запускать его без sudo и скачать образ, рассказано в статье Docker и образ OpenCode.

Шаг 2: worktree для агента

Агент работает не в папке вашего репозитория.

Он получает вторую копию того же репозитория, git worktree, в папке рядом. Из корня репозитория:

git worktree add ../site-agent -b agent/alpine-menu

В вашей папке ничего не меняется: ни файлы, ни ветка, ни файлы настройки из дерева введения. Теперь копий две:

git worktree list

/home/me/github/site 3dc8ba13 [main] /home/me/github/site-agent 3dc8ba13 [agent/alpine-menu]

ПапкаКто там работаетЧто в ней лежит
siteвы; лаунчер запускается отсюдавсё: файлы настройки, игнорируемые данные, ваши незакоммиченные правки
site-agentагент, в контейнереотслеживаемые файлы последнего коммита

Лаунчер собирает образ и читает конфигурацию из вашей папки, а монтирует только работу из site-agent, поэтому файлы настройки работают ещё до коммита.

Проверьте, что в worktree нет ничего игнорируемого. Команда ничего не печатает:

git -C ../site-agent status --short --ignored

Одна ветка на worktree, слитая в main

Worktree живёт на своей ветке: вы коммитите в эту ветку и сливаете её в main.

Git не даёт двум worktree использовать одну ветку, поэтому второй worktree на main падает:

git worktree add ../site-claude main

fatal: 'main' is already used by worktree at '/home/me/github/site'

Внутри worktree можно создавать ветки и переключаться, как в любой копии, кроме ветки, занятой другим worktree:

git -C ../site-agent switch -c agent/alpine-menu-2

Switched to a new branch 'agent/alpine-menu-2'

git -C ../site-agent switch main

fatal: 'main' is already used by worktree at '/home/me/github/site'

О том, как два агента работают одновременно, каждый со своим worktree и веткой, и что ломается, когда оба работают на main, читайте в статье Два агента: по одной ветке каждому.

Шаг 3: образ

Официальный образ запускает OpenCode от root и не содержит программ, которые вызывают навыки: ни PHP, ни bash, ни Python, ни Perl.

То, что он пишет в смонтированную папку, на хосте принадлежит root.

Поэтому вы собираете свой образ поверх него, с этими программами и пользователем с вашим UID.

Его Dockerfile лежит в container/containerfiles:

vi container/containerfiles/Dockerfile.opencode

Раздел Образ по умолчанию статьи про образ разбирает файл строка за строкой и объясняет, зачем нужна каждая.

Скрипт из шага 5 (лаунчер) собирает его при каждом запуске.

Чтобы собрать его один раз вручную и проверить:

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

Образ занимает 354 МБ, на 94 МБ больше официального.

Задача, страницам которой нужен другой язык, получает образ с этим языком: второй Dockerfile, тот же файл плюс один пакет.

В настройке их два, для Erlang и Ruby, и лаунчер выбирает их флагами --erlang и --ruby.

Соберите и проверьте образ с Erlang так же:

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

docker run --rm --entrypoint sh opencode-agent-erlang -c "erl -noshell -eval 'io:format(\"~s~n\", [erlang:system_info(otp_release)]), halt().'"

27

Раздел Другой язык статьи про образ показывает оба файла и их проверки, а также объясняет, как написать Dockerfile для другого языка, на примере Go.

Шаг 4: конфигурация и заметки для контейнера

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", // Read after AGENTS.md: what differs in the container (no git, read-only // folders, what stays on the host). "instructions": ["CONTAINER.md"] }

AGENTS.md написан для хоста. Заметки ниже говорят агенту, что в контейнере иначе:

vi container/tools/ai/docker/CONTAINER.md

# In the container You run in a Docker container with --auto: every command runs without a prompt. The container holds the tracked files of an agent worktree and nothing else, so these replace some rules of AGENTS.md here: - There is no git and no .git. Search with `grep -rn` or `rg` instead of `git grep`: there are no ignored files here for them to read. - container/src is writable, and so is any path the run was started with `--rw`. AGENTS.md, the skills, container/docs, container/tools and container/tests are read-only otherwise. - Files that read as empty and refuse writes (`*.env`, `*creds*`) are masked: say what should change in them, the edit is made on the host. - There are no credentials here. Test the deploy scripts with `--dry-run` and a dummy creds file in /tmp, never against a real host. - The local site, the tests that need it (`run-tests-venv.sh`) and commits are on the host. Do not try to reach them; list what is left to run on the host at the end of your answer. - Do not run generate-sitemap.sh, even after adding pages: it dates every page by its file time, and here every file has the time of the checkout. The sitemap is rebuilt on the host after the merge. - Scratch files go to /tmp.

Без этих заметок первый поиск в демонстрации с бесплатной моделью был git grep, как и написано в AGENTS.md, и он упал с git: command not found.

С ними агент сразу искал через grep -rn и закончил списком того, что нужно запустить на хосте.

Строка про карту сайта появилась позже. После задачи, которая добавила курс, агент пересобрал карту сайта, как велит AGENTS.md после новых страниц, и все 4 333 страницы, которые уже были, получили дату выкладки.

Шаг 5: лаунчер

Вы не запускаете контейнер своим docker run.

Каждая команда этого руководства, которая запускает агента, идёт через один скрипт, лаунчер container/tools/ai/opencode-docker.sh.

Он собирает образ, отказывается от папки с игнорируемыми файлами, монтирует worktree, маскирует файлы секретов, отбирает все права root и затем запускает OpenCode с --auto, оболочку или TUI.

В статье Лаунчер приведён весь скрипт и разобрано, что делает каждая часть, а также как добавить свой флаг.

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

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

Пустой файл, который встаёт на место замаскированных:

touch container/tools/ai/docker/masked

Как его вызывать, из корня репозитория основной копии.

Свои опции идут до worktree, опции opencode run — после run:

Вызов или опцияЧто делает
container/tools/ai/opencode-docker.sh ../site-agentTUI OpenCode
container/tools/ai/opencode-docker.sh ../site-agent run "<task>"одна задача, затем контейнер завершается
container/tools/ai/opencode-docker.sh ../site-agent shellоболочка в том же контейнере, как в шаге 6 (проверка, что видит контейнер)
--rw <path>ещё один путь на запись для этого запуска (когда задача в container/tools: --rw)
--openrouter-key <file>, --opencode-key <file>ключ только для этого запуска: ключ OpenRouter (платная модель через OpenRouter) или ключ OpenCode Zen либо Go (ключи для одного запуска)
--erlang, --rubyобраз с Erlang или Ruby (образ на запуск: --erlang и --ruby)

Шаг 6: проверка, что видит контейнер

Проверка идёт в двух местах. Сначала на хосте, из корня репозитория, запустите лаунчер с shell в конце:

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

Он запускает тот же контейнер, что получает агент, с теми же монтированиями, но вместо OpenCode выполняет sh, оболочку образа. Приглашение меняется на:

/work $

Теперь вы внутри контейнера: что бы вы ни набрали, выполняется там, а не на хосте.

/work — папка, в которую смонтирован worktree, а $ вместо # значит, что вы не root.

Второй шаг — по одному вводите в этом приглашении команды из первого столбца, после каждой нажимайте Enter и сравнивайте вывод со вторым столбцом:

КомандаВыводЧто значит
iduid=1000(agent)ваш UID, не root
ls -A ~.ash_history .localникаких ключей и конфигураций хоста; .ash_history хранит набранные команды и исчезает вместе с контейнером
ls container/src/services/user_profile/dataNo such file or directoryигнорируемых данных в worktree нет
wc -c container/tools/deploy/site.env0замаскирован
echo note >> AGENTS.mdRead-only file systemправила остаются как есть
touch container/tools/xRead-only file systemинструменты тоже
git statusgit: not foundистории для чтения нет
ls /var/run/docker.sockNo such file or directoryчерез Docker не выбраться
apk add curlPermission deniedновых пакетов не поставить
grep CapEff /proc/self/status0000000000000000возможностей нет

Когда совпадает каждая строка, выйдите из оболочки контейнера:

exit

Возвращается приглашение хоста, а --rm в лаунчере удаляет контейнер.

Остаётся только том с сессиями (что смонтировано и к чему у OpenCode есть доступ).

Что смонтировано и к чему у OpenCode есть доступ

Список монтирований в том виде, как его отдаёт контейнер (grep ' /work' /proc/mounts).

Первые пять строк идут из worktree, последние три — из папки вашего репозитория и из Docker:

В /workРежим: почему
container/srcчтение и запись: докрут, сама работа
AGENTS.md
.claude/skills
только чтение: правила и навыки
skill.md
_internal_ai_skill_for_files.md
container/docs
только чтение: то, на что указывают навыки
container/toolsтолько чтение: проверки, которые запускают навыки
container/testsтолько чтение: check-reflow.sh читает файл теста
opencode.jsonc
CONTAINER.md
только чтение: шаг 4 (конфигурация и заметки для контейнера)
отслеживаемые *.env, *creds*только чтение и пустые: файл masked
~/.local/share/opencodeчтение и запись: сессии, том Docker

Чего здесь нет совсем, что бы ни делала команда:

До чего он всё же достаёт: до интернета, который нужен модели, и до портов хоста, опубликованных на 0.0.0.0 (что контейнер достаёт по сети).

container/tools, документация и тесты

Навыки велят агенту проверять работу скриптами из container/tools.

Смонтированные только на чтение из worktree, они работают в контейнере и не могут измениться.

Проверено в контейнере с образом из шага 3 (образ):

ИнструментВ контейнере
php -l, рендер в CLI с DOCUMENT_ROOTработает
search/find_article.pyработает
check-sitemap.sh, htaccess_redirects.pyработает
generate-sitemap.shневерные даты: он ставит каждой странице дату по времени файла, а в новом worktree у всех файлов время выкладки; CONTAINER.md его запрещает, а карта сайта пересобирается после слияния
css/build-css.sh --checkработает
linters/check-reflow.shработает с примонтированной папкой тестов
linters/check-php-header.sh, check-line-endings.shработает с Perl в образе
deploy/deploy-ftp.sh --dry-runработает с фиктивным файлом доступов
run-tests-venv.shостанавливается: uv not found; тестам нужен локальный сайт, поэтому они идут на хосте
analyze_logs/download-logs.shостанавливается: нет файла доступов, так и задумано

Когда задача в container/tools: --rw

Задаче, которая исправляет или обновляет скрипт, нужна папка на запись.

--rw делает один путь доступным на запись для одного запуска, остальное остаётся только для чтения:

container/tools/ai/opencode-docker.sh --rw container/tools/deploy ../site-agent run "Add a --quiet option to container/tools/deploy/deploy-ftp.sh and test it with --dry-run."

Какая запись сработает — проверено через touch и echo > в оболочке, пути внутри container/:

Запись вРаботает с
src/xвсегда
tools/deploy/x--rw container/tools/deploy или --rw container/tools
tools/x--rw container/tools
tools/deploy/site.envникогда: замаскирован

Не выдают ли инструменты слишком много?

Смонтированная папка уходит агенту, а через него провайдеру модели, файл за файлом.

Четыре проверки перед тем, как что-то монтировать, из корня репозитория основной копии.

1. Секреты в отслеживаемых файлах.

git ls-files container | grep -E '\.env$|creds'

Здесь два файла: настройки развёртывания и пример файла доступов.

Лаунчер маскирует то, что перечислит эта команда, поэтому новый файл тоже будет замаскирован.

2. Инструменты, которые читают секреты вне репозитория.

git grep -l -E '\.config/|ftp-creds|PASSWORD' -- container/tools

Скрипты развёртывания и скачивания логов. В контейнере нет ~/.config, и они останавливаются до подключения.

3. Инструменты, которые читают игнорируемые данные.

git grep -l -E 'users\.db|access\.log|user_profile/data' -- container/tools

Скрипты разбора логов. Их базы и логи игнорируются, поэтому в worktree их нет.

4. Личные данные в отслеживаемом тексте.

git grep -c -E '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[a-z]{2,}' -- container/docs container/tools

Каждое совпадение уходит модели вместе с файлом.

Здесь: адрес самого сайта в двух скриптах, авторы встроенной библиотеки изображений в её документации и один личный адрес в документе о локальном Git-сервере.

Решите, остаётся ли этот адрес в репозитории.

Ни один инструмент менять не пришлось. Worktree держит данные снаружи, домашняя папка не смонтирована, а маски закрывают отслеживаемые настройки.

Осторожность нужна в другую сторону: код, который написал агент, позже работает на хосте.

Рендер в CLI, локальный сайт и тесты выполняют PHP из container/src, а на хосте этот PHP работает от вашего имени, с вашей домашней папкой.

Пусть агент рендерит в контейнере, как в демонстрации с бесплатной моделью, а на хосте запускайте эти проверки только после ревью и слияния на хосте.

Ревью и слияние на хосте

Из корня репозитория основной копии:

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

git -C ../site-agent diff

find ../site-agent/container -type l

Затем тесты против локального сайта, которые контейнер запустить не может.

Закоммитьте в worktree явными путями, а потом слейте из основной копии:

git -C ../site-agent add container/src/linux/_linux_menu_inc.php

git -C ../site-agent commit -m "Adds Alpine to the distributions of the Linux section"

git merge agent/alpine-menu

Если задача добавила или удалила страницы, пересоберите карту сайта теперь, в основной копии, где у каждого файла настоящее время.

Скрипт сохраняет старую карту как sitemap.xml.bak-<time> рядом с ней; эта копия не для коммита:

container/tools/generate-sitemap.sh

git add container/src/sitemap.xml

git commit -m "Adds the new pages to the sitemap"

Удалите worktree и его ветку:

git worktree remove ../site-agent

git branch -d agent/alpine-menu

Другие страницы этого руководства

Остальное руководство разложено по отдельным страницам:

Ежедневная работа: начать задачу, задача из файла, образ на задачу, отправить обновление во время работы, остановить, продолжить, перечитать отчёт и слить.

Демонстрация: настоящая задача на бесплатной модели: одна задача от команды до изменения в worktree.

Два агента: по одной ветке каждому: два агента сразу, конфликт слияния и оба на main.

Claude Code как супервизор: Claude Code отдаёт задачу OpenCode в Docker, проверяет её на хосте и отправляет правки в ту же сессию.

Платная модель через OpenRouter: отдельный ключ для контейнера, переданный на один запуск.

Сохранённый вход: что это, как узнать, есть ли он, как им пользоваться, создать его или удалить.

Сеть: до чего контейнер всё ещё достаёт и как закрыть локальный сайт.

Может ли сам container/src быть томом?: worktree, основная копия или именованный том.

Проблемы на настоящих задачах: что пошло не так в первых настоящих задачах и что делать вместо этого.

Docker и образ OpenCode: шаг 1 (Docker и образ OpenCode) полностью.

Образ: шаг 3 (образ) полностью, с Erlang, Ruby и другим языком.

Лаунчер: шаг 5 (лаунчер) полностью, со скриптом, его опциями и тем, как добавить флаг.

Выбор модели: откуда берётся модель задачи, бесплатная модель OpenCode и модель OpenCode Go или OpenRouter на один запуск или на каждый.

Чек-лист

Автор статьи: Клавдия Кондакова