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
- ../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 начинается с последнего коммита. Незакоммиченные правки и неотслеживаемые файлы, в том числе новая документация, остаются в основной копии.
- Коммитить в нём можно так же, как в основной копии. Он делит с репозиторием историю, ветки и git-хуки, и в нём можно закоммитить любой путь: и container/tools, и container/src. Коммиты уходят в его ветку, а git merge в основной копии переносит их в main (ревью и слияние на хосте).
- Игнорируемых помощников в нём тоже нет: .venv тестов, node_modules сборки CSS, картинок. То, чему они нужны, запускайте в основной копии после слияния.
- Ваши незакоммиченные правки могут помешать слиянию. Если они затрагивают файл, который изменила ветка, git merge остановится с Your local changes to the following files would be overwritten by merge.
- Один worktree на задачу. После слияния он удаляется (ревью и слияние на хосте), а следующая задача начинается с нового main.
- Монтируются только папки внутри него, никогда он сам. Файл .git в нём хранит путь основного репозитория.
Одна ветка на 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"] }
- Списка разрешений нет. Под --auto каждый запрос подтверждения становится разрешением. Граница — список монтирований из скрипта шага 5 (лаунчер).
- Модель — текущая бесплатная модель OpenCode, первый выбор этой настройки: ключ ей не нужен, поэтому в контейнере нет никакого ключа. Бесплатный тариф может хранить то, что ему отправляют: монтируйте только то, что готовы отправить. О том, как проверить, какая модель у задачи, и как перейти на модель OpenCode Go или OpenRouter, читайте в статье Выбор модели.
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-agent | TUI 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 и сравнивайте вывод со вторым
столбцом:
| Команда | Вывод | Что значит |
|---|---|---|
| id | uid=1000(agent) | ваш UID, не root |
| ls -A ~ | .ash_history .local | никаких ключей и конфигураций хоста; .ash_history хранит набранные команды и исчезает вместе с контейнером |
| ls container/src/services/user_profile/data | No such file or directory | игнорируемых данных в worktree нет |
| wc -c container/tools/deploy/site.env | 0 | замаскирован |
| echo note >> AGENTS.md | Read-only file system | правила остаются как есть |
| touch container/tools/x | Read-only file system | инструменты тоже |
| git status | git: not found | истории для чтения нет |
| ls /var/run/docker.sock | No such file or directory | через Docker не выбраться |
| apk add curl | Permission denied | новых пакетов не поставить |
| grep CapEff /proc/self/status | 0000000000000000 | возможностей нет |
Когда совпадает каждая строка, выйдите из оболочки контейнера:
exit
Возвращается приглашение хоста, а --rm в
лаунчере удаляет контейнер.
Остаётся только том с сессиями (что смонтировано и к
чему у OpenCode есть доступ).
Что смонтировано и к чему у OpenCode есть доступ
Список монтирований в том виде, как его отдаёт контейнер
(grep ' /work' /proc/mounts).
Первые пять строк идут из worktree, последние три — из папки вашего
репозитория и из Docker:
| В /work | Режим: почему |
|---|---|
| container/src | чтение и запись: докрут, сама работа |
| AGENTS.md .claude/skills | только чтение: правила и навыки |
| skill.md _internal_ai_skill_ container/docs | только чтение: то, на что указывают навыки |
| container/tools | только чтение: проверки, которые запускают навыки |
| container/tests | только чтение: check-reflow.sh читает файл теста |
| opencode.jsonc CONTAINER.md | только чтение: шаг 4 (конфигурация и заметки для контейнера) |
| отслеживаемые *.env, *creds* | только чтение и пустые: файл masked |
| ~/.local/share/ | чтение и запись: сессии, том Docker |
Чего здесь нет совсем, что бы ни делала команда:
- .git и вся история, включая удалённые файлы.
- Игнорируемые файлы: записи о посетителях, логи, базы данных, резервные копии.
- Неотслеживаемые файлы и ваши незакоммиченные правки.
- Ваша домашняя папка: ~/.ssh, ~/.config, а также auth.json и сессии OpenCode на хосте.
- Сокет Docker и остальной диск.
До чего он всё же достаёт: до интернета, который нужен модели, и до портов хоста, опубликованных на 0.0.0.0 (что контейнер достаёт по сети).
container/tools, документация и тесты
Навыки велят агенту проверять работу скриптами из
container/tools.
Смонтированные только на чтение из worktree, они работают в контейнере и
не могут измениться.
Проверено в контейнере с образом из шага 3
(образ):
| Инструмент | В контейнере |
|---|---|
| php -l, рендер в CLI с DOCUMENT_ROOT | работает |
| search/ | работает |
| check-sitemap.sh, htaccess_redirects.py | работает |
| generate-sitemap.sh | неверные даты: он ставит каждой странице дату по времени файла, а в новом worktree у всех файлов время выкладки; CONTAINER.md его запрещает, а карта сайта пересобирается после слияния |
| css/build-css.sh --check | работает |
| linters/ | работает с примонтированной папкой тестов |
| linters/ | работает с Perl в образе |
| deploy/ | работает с фиктивным файлом доступов |
| run-tests-venv.sh | останавливается: uv not found; тестам нужен локальный сайт, поэтому они идут на хосте |
| analyze_logs/ | останавливается: нет файла доступов, так и задумано |
- Скрипт, который пишет в свою папку, на монтировании только для чтения падает: направьте его вывод в /tmp или в container/src.
- .venv хоста здесь не работает. Он не отслеживается, а Alpine использует musl вместо glibc. Инструменты, которым мало стандартной библиотеки Python, запускайте на хосте, а их пакеты ставьте в образ.
- Ещё одна папка добавляется в ro=( ) лаунчера.
Когда задача в 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/x | --rw container/tools |
| tools/deploy/ | никогда: замаскирован |
- Замаскированные файлы остаются замаскированными. Агент не может ни прочитать их, ни изменить; он говорит, что менять, а правку вы делаете на хосте.
- В контейнере нет доступов. Заметки для контейнера из шага 4 (конфигурация и заметки для контейнера) отправляют агента к --dry-run с фиктивным файлом доступов в /tmp. Проверено: dry-run: no connection, nothing uploaded.
- Ревью строже. Скрипт развёртывания запускается на хосте с вашими настоящими доступами. Прочитайте каждую строку его диффа до первого настоящего запуска: новый хост, URL, curl, wget, nc, base64 или eval — это стоп.
- --rw AGENTS.md или --rw .claude/skills даёт агенту переписать свои же правила: только для задачи о них и только с чтением диффа.
Не выдают ли инструменты слишком много?
Смонтированная папка уходит агенту, а через него провайдеру модели,
файл за файлом.
Четыре проверки перед тем, как что-то монтировать, из корня репозитория
основной копии.
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
- Новые файлы не попадают в git diff, пока их не добавят; status показывает их с ??.
- Симлинки. Последняя команда должна ничего не печатать. Ссылка на ~/.config/… в контейнере указывает в пустоту, а на хосте — на настоящий файл, за которым пойдёт скрипт загрузки или редактор.
- .htaccess строка за строкой: он решает, что сайт отдаёт.
- PHP, который читает файлы или открывает соединения, и каждый скрипт, изменённый под --rw: каждая строка.
- Изменённый sitemap.xml или новый sitemap.xml.bak-… значит, что агент всё-таки пересобрал карту сайта. Восстановите один и удалите другой: git -C ../site-agent restore container/src/sitemap.xml
Затем тесты против локального сайта, которые контейнер запустить не
может.
Закоммитьте в 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 на один запуск или на каждый.
Чек-лист
- docker.io и docker-buildx установлены, вы в группе docker.
- Тег образа совпадает с opencode --version на хосте.
- Сначала сделан коммит; проверка worktree с --ignored ничего не печатает.
- Конфигурация называет текущую бесплатную модель OpenCode или платную модель с ключом, сделанным для неё.
- Оболочка шага 6 (проверка, что видит контейнер) показывает монтирования, маски и отсутствие возможностей.
- --rw только для пути, который меняет задача.
- Локальный сайт и любой другой локальный сервис опубликованы на 127.0.0.1.
- После запуска: status, diff, ни одного симлинка, затем проверки на хосте; коммит, слияние, карта сайта, если страницы добавлялись, удаление worktree.
- Перед коммитом: worktree не отстаёт от main, и никакой другой агент не работает в основной копии (main ушёл вперёд, пока агент работал, другой агент в вашей папке, пока вы коммитите).
Автор статьи: Клавдия Кондакова