OpenCode no Docker

Saudações.

Hoje vou ensinar como enlatei o OpenCode em um container e criei um enxame de programadores automatizados.

Pré-requisitos:

  • Servidor, VPS ou VM com Linux;
  • Docker;

1 – Sobre o OpenCode

O OC é um agente de IA especializado e refinado para planejar (plan) e construir (build) softwares.

Ele é uma “versão gratuita e aberta do Claude Code” e se tornou muito famoso ser simples e poderoso.

Embora essa seja a principal função dele, você pode usá-lo como agente genérico para qualquer tarefa.

É normal rodar vários agentes ao mesmo tempo, basta que para cada projeto você abra uma tela no terminal, vá até a pasta do seu projeto e rode o comando “opencode“.

Ele é executado de duas formas:

  • Terminal do Agente: Ao rodar o comando “opencode” ele inicia um terminal moderno para interação com você, o usuário, nesse terminal você insere o prompt ou comandos específicos que iniciam com “/”;
  • Comando para o Agente: Permite acionar o comando “opencode” com argumentos específicos para tarefas onshot.

Para funcionar ele precisa:

  • Acesso a Internet durante a execução;
  • Chave de API para algum modelo de IA.

Links:

Eu particularmente não quero amarrá-lo ao meu notebook nem acessar um terminal gráfico remotamente. Ao enlatar o OpenCode em container eu ganho a liberdade de ter um container para cada projeto e acessá-lo puramente por SSH de qualquer lugar, alem de automatizar execuções com gatilhos próprios.

2 – OpenCode no Docker

Essa parte foi um pouco difícil. Por ser feito em Javascript/TypeScript e por padrão rodando no framework do nodejs o OpenCode espalha arquivos em vários lugares.

Publiquei a imagem no Docker Hub em tmsoftbrasil/opencode:latest, logo você pode pular esse capítulo.

Leia os tópicos abaixo para entender a engenharia da imagem.

2.1 – Caminhos e volumes

Esses caminhos precisam ser mapeados e direcionado para volumes, assim o container separa:

  • Programas: Os binários e bibliotecas de códigos;
  • Dados: Configurações e informações acumuladas pelo OC;
  • Cache: Arquivos gerados para acelerar a execução;
  • Workspace: Projeto de código onde serão produzidos os artefatos do agente, como códigos, documentações, etc;

Caminhos que configurei:

Caminho dentro do containerNaturezaPersistência recomendada
/data/config/opencodeConfig global, opencode.json, tui.json, agents, commands, plugins, skillsPersistente
/data/share/opencode/auth.jsonCredenciais, chaves de API, OAuthPersistente e sensível
/data/share/opencode/mcp-auth.jsonTokens OAuth de servidores MCPPersistente e sensível
/data/share/opencode/opencode.dbBanco SQLite observado por vocêPersistente
/data/share/opencode/logLogs do OpenCodePersistente por padrão
/data/home/opencodeHOME do usuário, fallback para ferramentas que gravam em ~Persistente
/data/npm-globalPacotes npm globais instalados em runtimePersistente
/cache/npmCache e logs do npmDescartável
/cache/xdg/opencodeCache XDG do OpenCodeDescartável
/cache/tmpTMPDIR e /tmp, inclusive .so temporárioDescartável
/cache/opencode-config-node_modulesnode_modules gerado em config/pluginsDescartável, recriável
/workspaceCódigo do projetoBind mount do host

2.2 – Arquivos para construir a imagem

Dockerfile da imagem:

Dockerfile – ./docker/Dockerfile
# syntax=docker/dockerfile:1.7

# Base Debian Linux
FROM debian:bookworm-slim

ARG APP_UID=1000
ARG APP_GID=1000

# Versao do OpenCode, usar latest ou versao especifica
# docker buikd ...  --build-arg OPENCODE_VERSION=1.x.y
ARG OPENCODE_VERSION=latest

ENV DEBIAN_FRONTEND=noninteractive \
    LANG=C.UTF-8 \
    LC_ALL=C.UTF-8

# Pacotes base:
RUN set -eux; \
    apt-get update; \
    apt-get install -y --no-install-recommends \
      bash \
      ca-certificates \
      curl \
      wget \
      git \
      openssh-client \
      nodejs \
      npm \
      brotli \
      tar \
      xz-utils \
      zstd \
      unzip \
      tzdata \
      openssl \
      gosu \
      less \
      procps \
      jq \
      sqlite3 \
      ripgrep \
    ; \
    rm -rf /var/lib/apt/lists/*

# Instala OpenCode via npm
RUN set -eux; \
    npm install -g "opencode-ai@${OPENCODE_VERSION}"; \
    opencode --version

# Usuario não-root para rodar o agente.
# A HOME real sera em /data, para persistir tudo que for config/
RUN \
    set -eux; \
    groupadd -g "${APP_GID}" opencode; \
    useradd -u "${APP_UID}" -g "${APP_GID}" \
        -d /data/home/opencode -s /bin/bash opencode; \
    \
    mkdir -p /data /cache /workspace /cache/tmp /root/.local; \
    chmod 0755 /data /cache /workspace; \
    chmod 1777 /cache/tmp

# Redireciona /tmp para /cache/tmp, inclusive para arquivos .so temporarios.
RUN \
    rm -rf /tmp; \
    ln -s /cache/tmp /tmp

# Criar links de /root para os volumes caso rode processos como root dentro do container
RUN \
    rm -rf /root/.config /root/.cache /root/.npm /root/.local/share; \
    ln -s /data/config /root/.config; \
    ln -s /data/share /root/.local/share; \
    ln -s /cache/xdg /root/.cache; \
    ln -s /cache/npm /root/.npm

# Instalar script de entrypoint
COPY rootfs/opt/entrypoint.sh /opt/entrypoint.sh
RUN chmod +x /opt/entrypoint.sh

# Variaveis centrais:
# - XDG_CONFIG_HOME e XDG_DATA_HOME em /data
# - XDG_CACHE_HOME, npm cache e TMPDIR em /cache
# - NO_PROXY para evitar que o servidor local do TUI passe por proxy
ENV HOME=/data/home/opencode \
    XDG_CONFIG_HOME=/data/config \
    XDG_DATA_HOME=/data/share \
    XDG_CACHE_HOME=/cache/xdg \
    OPENCODE_CONFIG_DIR=/data/config/opencode \
    NPM_CONFIG_CACHE=/cache/npm \
    NPM_CONFIG_PREFIX=/data/npm-global \
    TMPDIR=/cache/tmp \
    NO_PROXY=localhost,127.0.0.1 \
    PATH=/data/npm-global/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

# Diretorio padrao inicial
WORKDIR /workspace

# Volumes anonimos (default, requer mapeamento no run)
VOLUME ["/data", "/cache", "/workspace"]

# Entrypoint
ENTRYPOINT ["/opt/entrypoint.sh"]

# Comando padrao
CMD ["bash"]

Script de Entrypoint:

Shell Script rootfs/opt/entrypoint.sh
#!/bin/bash

set -euo pipefail;

# Env defaults
    export HOME="${HOME:-/data/home/opencode}";
    export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-/data/config}";
    export XDG_DATA_HOME="${XDG_DATA_HOME:-/data/share}";
    export XDG_CACHE_HOME="${XDG_CACHE_HOME:-/cache/xdg}";
    export OPENCODE_CONFIG_DIR="${OPENCODE_CONFIG_DIR:-/data/config/opencode}";
    export NPM_CONFIG_CACHE="${NPM_CONFIG_CACHE:-/cache/npm}";
    export NPM_CONFIG_PREFIX="${NPM_CONFIG_PREFIX:-/data/npm-global}";
    export TMPDIR="${TMPDIR:-/cache/tmp}";
    export NO_PROXY="${NO_PROXY:-localhost,127.0.0.1}";


# Criar link simbolico
    safe_link() {
        local src="$1";
        local dst="$2";
        if [ -L "$dst" ] || [ ! -e "$dst" ]; then
            ln -sfnT "$src" "$dst";
        fi;
    };


# Garantir diretorios usados
    mkdir -p \
      "$HOME" \
      "$HOME/.local" \
      "$XDG_CONFIG_HOME/opencode" \
      "$XDG_DATA_HOME/opencode" \
      "$XDG_CACHE_HOME/opencode" \
      "$NPM_CONFIG_CACHE" \
      "$NPM_CONFIG_PREFIX/bin" \
      "$TMPDIR" \
      /cache/opencode-config-node_modules \
      /workspace;


# Ajustes de permissoes
    chmod 700 "$XDG_DATA_HOME/opencode" || true;
    chmod 1777 "$TMPDIR" || true;


# Compatibilidade para softwares que ignoram variáveis XDG.
    safe_link "$XDG_CONFIG_HOME" "$HOME/.config"
    safe_link "$XDG_DATA_HOME" "$HOME/.local/share"
    safe_link "$XDG_CACHE_HOME" "$HOME/.cache"
    safe_link "$NPM_CONFIG_CACHE" "$HOME/.npm"


# Classificacao deliberada:
# ~/.config/opencode e' /data, mas node_modules gerado por
# plugins/config pode ir para /cache.
# Se preferir preservar node_modules também, remova este bloco.
    if [ ! -e "$XDG_CONFIG_HOME/opencode/node_modules" ]; then
        ln -s \
            /cache/opencode-config-node_modules \
            "$XDG_CONFIG_HOME/opencode/node_modules";
    fi;


# Config inicial editavel. Desativa autoupdate dentro do container:
# container deve ser atualizado via rebuild da imagem,
# não mutando binários em runtime.
    if [ "${OPENCODE_INIT_CONFIG:-1}" = "1" ]; then
        if [ ! -e "$XDG_CONFIG_HOME/opencode/opencode.json" ]; then
            (
                echo '{';
                echo '  "$schema": "https://opencode.ai/config.json",';
                echo '  "autoupdate": false';
                echo '}';
            ) > $XDG_CONFIG_HOME/opencode/opencode.json;
        fi;
    fi;


# Se entrou como root, ajusta somente /data e /cache e
# depois cai para usuário não-root.
# Não faz chown de /workspace para não
# alterar dono dos arquivos do projeto no host.
    if [ "$(id -u)" = "0" ]; then
        if [ "${OPENCODE_CHOWN:-1}" = "1" ]; then
            chown -R opencode:opencode /data /cache
        fi
        exec gosu opencode "$@"
    fi


# Rodar CMD do container
    exec "$@";

2.3 – Construção da imagem

Para construir a imagem, crie uma pasta para esse projeto “opencode-builder” crie os dois arquivos (conteúdo acima) nos seguintes caminhos:

  • Dockerfile: ./docker/Dockerfile
  • Entrypoint: ./rootfs/opt/entrypoint.sh

Execute o build para construir a imagem:

Bash
# Constuir imagem opencode:latest
    docker build . -f docker/Dockerfile --no-cache -t opencode:latest;

Caso queira construir uma imagem versionada usando uma versão específica do OpenCode:

Bash
# Construir imagem de versao especifica do opencode:
    OPENCODE_VERSION="1.x.y";
    docker build . \
        -f docker/Dockerfile\
        -t opencode:$OPENCODE_VERSION \
        --build-arg OPENCODE_VERSION=$OPENCODE_VERSION;

3 – Rodando OpenCode no Docker

Vamos rodar o OC no Docker, criando o container opencode-dev.

2.1 – Rede Docker

Criando a rede para containers (network_public):

Bash
# Rede de containers
docker network create network_public \
    -d bridge \
    -o com.docker.network.bridge.name=br-net-public \
    -o com.docker.network.driver.mtu=1500 \
    -o com.docker.network.bridge.gateway_mode_ipv4=nat-unprotected \
    --subnet 10.249.0.0/16 \
    --gateway 10.249.255.254;

2.2 – Volume

A pasta /workspace dentro do container deve ser mapeada na pasta onde estará seu projeto de software no HOST.

Ela é a pasta onde os programas são executados por padrão (workdir do container).

Nesse exemplo vou armazenar tudo em /storage/opencode-dev para você testar e aprender, e depois você personaliza.

Bash
# Diretorio do volume no HOST
    DATADIR=/storage/$NAME;

# Pastas de volumes montados na pasta principal do volume no host
    mkdir -p $DATADIR;
    # - Configs e DB de contexto
    mkdir -p $DATADIR/data;
    
    # - Cache do bibliotecas
    mkdir -p $DATADIR/cache;
    
    # - Pasta com projeto de software (caminho padrao inicial /workspace)
    mkdir -p $DATADIR/workspace;

2.3 – Container do OC

Comando para rodar no “docker run”:

Bash
# Variaveis
    NAME="opencode-dev";

    # Para usar sua imagem local construida no cap. 2
    #IMAGE="opencode:latest";
    # Para usar minha imagem pronta:
    IMAGE="tmsoftbrasil/opencode:latest";

# Diretorio de dados persistentes:
    DATADIR=/storage/$NAME;

    # Pastas de volumes montados
    # na pasta principal do volume no host
    mkdir -p $DATADIR;
    mkdir -p $DATADIR/data;
    mkdir -p $DATADIR/cache;
    mkdir -p $DATADIR/workspace;

# Rodar container
    # Renovar/rodar
    docker rm -f $NAME 2>/dev/null

    #    --read-only
    docker run \
        -d --restart=always \
        --name $NAME --hostname $LOCAL.intranet.br \
        \
        --read-only \
        --tmpfs /run:rw,noexec,nosuid,size=16m \
        --tmpfs /tmp:rw,noexec,nosuid,size=16m \
        \
        --cpus=2 \
        --memory 2g --memory-swap 2g --memory-reservation 256m \
        \
        --network network_public \
        \
        -v $DATADIR/data:/data \
        -v $DATADIR/cache:/cache \
        -v $DATADIR/workspace:/workspace \
        \
        \
        $IMAGE \
            tail -f /dev/null;

2.4 – Stack para Compose

Caso prefira no modelo de Stack para docker compose:

YAML
name: opencode-dev

services:
  opencode-dev:
    image: tmsoftbrasil/opencode:latest
    container_name: opencode-dev
    hostname: opencode-dev
    restart: always
    read_only: true
    command: tail -f /dev/null

    tmpfs:
      - /run:rw,noexec,nosuid,size=16m
      - /tmp:rw,noexec,nosuid,size=16m

    deploy:
      resources:
        limits:
          cpus: "2"
          memory: 2g
        reservations:
          memory: 256m

    mem_swappiness: 0
    memswap_limit: 2g

    networks:
      - network_public

    volumes:
      - /storage/opencode-dev/data:/data
      - /storage/opencode-dev/cache:/cache
      - /storage/opencode-dev/workspace:/workspace

networks:
  network_public:
    external: true

3 – Primeiro acesso

Obtenha shell no container “opencode-dev“:

Bash
# Obter shell no container:
docker exec -it opencode-dev bash;

# Rodar opencode:
opencode;

# Ou, rodar direto o opencode dentro do container:
docker exec -it opencode-dev opencode;

3.1 – Tela inicial

Você verá a seguinte tela:

3.2 – Conectando OC a um modelo de IA

O primeiro passo é fornecer um modelo usando o comando “/connect“.

Gratuitamente: Escolha “OpenCode Zen“, acesse https://opencode.ai/zen para obter uma chave, você pode escolher algum modelo “Free” para usar se precisar pagar nada.

Pagos e baratos: Escolha “OpenRouter“, acesse https://openrouter.ai, cadastre-se, coloque algum crédito (15 dólares), recomendo usar o DeepSeek V4 ou algum modelo focado em código com o preço baixo.

Eu uso OpenRouter com DeepSeek V4 PRO. Custa alguns centavos de dolar por hora e produz software com muita precisão.

Após rodar o /connect escolhe OpenRouter:

Inserindo a chave de API gerada no site:

Escolha o modelo:

E por fim, pronto para usar:

Para fechar o OpenCode (não para o container):

3.3 – Criando alguma coisa com IA

Com o OpenCode aberto, basta pedir alguma coisa, o resultado por padrão será gerado em /workspace (dentro do container) que fica na pasta do HOST onde esse volume foi mapeado.

Prompt:

Agente OC trabalho:

Resultado pronto:

Resultado (abri arquivo aviso.html no navegador):

.

Agora é contigo. Use sua criatividade.

Sábio não é aquele que sabe
de tudo e sim usa tudo que sabe
Provérbio Chinês

Terminamos por hoje!

Patrick Brandão, patrickbrandao@gmail.com