Saudações.
Hoje vou ensinar como enlatei o Coder dentro de um container Docker com vários agentes de IA.
Pré-requisitos:
- Servidor, VPS ou VM com Linux;
- Docker;
1 – Sobre o Coder
O Coder é uma IDE de desenvolvimento que funciona no navegador.
Ele é inspirado e compatível com o VSCode.
Depois de um tempo tendo que lidar com gestão de projetos usando o método tradicional de instalar tudo no notebook e as vezes sincronizar com o Git, me veio uma ideia: E se a IDE, o projeto, a compilação, testes, agentes de IA pudessem ficar enlatados no Docker?
Com isso eu poderia programar em qualquer computador, smartphone, tablet, delegar tarefas aos agentes, sem precisar manter nenhum deles ligados.
Links:
- Projeto no GitHub: https://github.com/coder/code-server;
- Site: https://coder.com/
- Documentação: https://coder.com/docs
- Releases: https://github.com/coder/code-server/releases
2 – Guia rápido
Para quem tem pressa!
Crie a rede Docker:
# 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;
Criar volume e configuração inicial com senha tulipa_coder (mude para a sua):
# Diretorio do volume
# Pasta no HOST
mkdir -p /storage/coder-ide;
# Pasta do projeto para mapear em /project do container
mkdir -p /storage/coder-ide/project;
# Raiz do sistema
mkdir -p /storage/coder-ide/.config/code-server;
# Config inicial
#(
# echo;
# echo 'bind-addr: 127.0.0.1:8080';
# echo 'auth: password';
# echo 'password: tulipa_coder';
# echo 'cert: false';
# echo;
#) > /storage/coder-ide/.config/code-server/config.yaml;
# Ajustar permissoes, login 'coder' uid 1000
chown -R 1000:1000 /storage/coder-ide;
Container do Coder:
# Senha de acesso
PASSWORD="tulipa_coder";
# Atualizar imagem
docker pull codercom/code-server:latest;
# Renovar/rodar:
# - Parar atual
docker container stop coder-ide 2>/dev/null;
docker container rm coder-ide 2>/dev/null;
# - Rodar com imagem renovada
docker container run \
-d \
--user root \
--restart always \
--name coder-ide \
--hostname coder-ide.intranet.br \
\
--cpus 2.0 \
--cpu-shares 1024 \
--memory 2g \
--memory-swap 2g \
--memory-reservation 1g \
--shm-size 1g \
\
--network network_public \
--ip 10.249.111.201 \
\
-p 7280:8080 \
\
-v /storage/coder-ide:/root \
-v /storage/coder-ide:/home/coder \
-v /storage/coder-ide/project:/project \
\
-v /var/run/docker.sock:/var/run/docker.sock \
\
-e DEFAULT_WORKSPACE=/project \
-e DOCKER_USER=root \
-e PASSWORD=$PASSWORD \
\
codercom/code-server:latest \
--auth password \
/project;
# entrypoint: /usr/bin/entrypoint.sh
# cmd.......: --bind-addr 0.0.0.0:8080
Versão Docker Compose:
services:
coder-ide:
image: codercom/code-server:latest
container_name: coder-ide
hostname: coder-ide.intranet.br
user: root
restart: always
networks:
network_public:
ipv4_address: 10.249.111.201
ports:
- "7280:8080"
volumes:
- /storage/coder-ide:/root
- /storage/coder-ide/log:/var/log
- /storage/coder-ide:/home/coder
- /storage/coder-ide/project:/project
- /var/run/docker.sock:/var/run/docker.sock
environment:
DEFAULT_WORKSPACE: /project
DOCKER_USER: root
PASSWORD: tulipa_coder
command:
- --auth
- password
- /project
cpus: 2.0
cpu_shares: 1024
mem_limit: 2g
memswap_limit: 2g
mem_reservation: 1g
shm_size: 1g
networks:
network_public:
name: network_public
driver: bridge
driver_opts:
com.docker.network.bridge.name: br-net-public
com.docker.network.driver.mtu: 1500
com.docker.network.bridge.gateway_mode_ipv4: nat-unprotected
ipam:
config:
- subnet: 10.249.0.0/16
gateway: 10.249.255.254
Primeiro acesso: Abra o navegador no endereço do servidor porta HTTP 7280.
3 – Imagem container do pacotão
Agora vou criar uma única imagem juntando tudo que foi feito no capítulo 3.
Crie e entre na pasta coder-bundle:
mkdir -p /root/coder-bundle;
cd /root/coder-bundle;
Nessa pasta coloque todos os arquivos abaixo:
- supervisord.conf
- sshd_config
- crontab
- entrypoint.sh
- Dockerfile
3.1 – Supervisor
Usamos o supervidord para rodar vários softwares no container. Ele será o PID 1 que manterá e reiniciará todos os softwares caso falhem.
Arquivo supervisord.conf
[supervisord]
user = root
nodaemon = true
pidfile = /run/supervisord.pid
childlogdir = /root/log
logfile = /dev/null
logfile_maxbytes = 0
[unix_http_server]
file = /var/run/supervisor.sock
chmod = 0700
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
[supervisorctl]
serverurl = unix:///run/supervisor.sock
[program:sshd]
user = root
command = /usr/sbin/sshd -D -e
priority = 1
directory = /run
autostart = true
autorestart = true
startsecs = 0
stopwaitsecs = 2
stdout_logfile = /proc/1/fd/1
stderr_logfile = /proc/1/fd/2
stdout_logfile_maxbytes = 0
stderr_logfile_maxbytes = 0
[program:coder-server]
user = root
command = /usr/bin/entrypoint.sh --bind-addr 0.0.0.0:8080 --auth password /project
environment = HOME="/root",DEFAULT_WORKSPACE="/project"
priority = 2
directory = /run
autostart = true
autorestart = true
startsecs = 0
stopwaitsecs = 2
stdout_logfile = /proc/1/fd/1
stderr_logfile = /proc/1/fd/2
stdout_logfile_maxbytes = 0
stderr_logfile_maxbytes = 0
[program:cron]
user = root
command = /usr/sbin/cron -f -L 8
priority = 3
directory = /run
autostart = true
autorestart = true
startsecs = 0
stopwaitsecs = 2
stdout_logfile = /proc/1/fd/1
stderr_logfile = /proc/1/fd/2
stdout_logfile_maxbytes = 0
stderr_logfile_maxbytes = 0
3.2 – Configuração SSH
Para permitir acesso direto no container por SSH temos que salvar as chaves do servidor em volume e autorizar a entrada como root direto.
O container não possui senha no usuário root, você deverá cadastrar sua chave pública no arquivo trusted_keys:
- Dentro do container: /root/ssh/trusted_keys
- No volume: /storage/coder-ide/ssh/trusted_keys
Arquivo sshd_config
VersionAddendum OpenSSH_12
Protocol 2
Port 22
AddressFamily any
ListenAddress 0.0.0.0
ListenAddress ::
IPQoS lowdelay throughput
Compression yes
UseDNS no
ClientAliveInterval 3
ClientAliveCountMax 15
TCPKeepAlive yes
PubkeyAuthentication yes
AuthorizedKeysFile /root/ssh/trusted_keys .ssh/authorized_keys
HostKey /root/ssh/ssh_host_rsa_key
HostKey /root/ssh/ssh_host_ecdsa_key
HostKey /root/ssh/ssh_host_ed25519_key
PasswordAuthentication yes
PermitEmptyPasswords no
PermitListen any
PermitRootLogin yes
KbdInteractiveAuthentication no
UsePAM yes
SyslogFacility AUTH
LogLevel INFO
PubkeyAuthentication yes
RekeyLimit 1G 1h
RequiredRSASize 2048
PrintLastLog no
PrintMotd no
AcceptEnv LANG LC_* COLORTERM NO_COLOR
Subsystem sftp /usr/lib/openssh/sftp-server
AllowAgentForwarding yes
AllowTcpForwarding yes
X11Forwarding yes
X11DisplayOffset 10
GatewayPorts clientspecified
PermitTTY yes
PermitTunnel no
PermitUserEnvironment no
Include /etc/ssh/sshd_config.d/*.conf
3.3 – Crontab
O plano de crontab com intervalos pre-configurados nos permitirá criar e acionar scripts de automação dentro do nosso projeto.
Vou criar em /root/cron/ pois a pasta /root ficará em volume, preservando assim o projeto e os scripts de automação juntos.
Arquivo crontab
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# min hour day month week-day user program args...
0 * * * * root run-parts --regex '.*' /root/cron/hourly
0 2 * * * root run-parts --regex '.*' /root/cron/daily
0 3 * * 6 root run-parts --regex '.*' /root/cron/weekly
0 5 1 * * root run-parts --regex '.*' /root/cron/monthly
*/1 * * * * root run-parts --regex '.*' /root/cron/1min
*/5 * * * * root run-parts --regex '.*' /root/cron/5min
*/10 * * * * root run-parts --regex '.*' /root/cron/10min
*/15 * * * * root run-parts --regex '.*' /root/cron/15min
*/30 * * * * root run-parts --regex '.*' /root/cron/30min
0 0 * * 0 root run-parts --regex '.*' /root/cron/sunday
0 0 * * 1 root run-parts --regex '.*' /root/cron/monday
0 0 * * 2 root run-parts --regex '.*' /root/cron/tuesday
0 0 * * 3 root run-parts --regex '.*' /root/cron/wednesday
0 0 * * 4 root run-parts --regex '.*' /root/cron/thursday
0 0 * * 5 root run-parts --regex '.*' /root/cron/friday
0 0 * * 6 root run-parts --regex '.*' /root/cron.saturday
3.4 – Entrypoint
Vou criar um novo entrypoint com supervidord para rodar o Coder Server e o OpenSSH Server, dessa forma poderemos entrar via SSH direto no container.
Arquivo entrypoint.sh
#!/bin/sh
# Funcao de log na stdout
_log(){
echo "$(date '+%Y-%m-%d %H:%M:%S'),$(date +%3N) INFO entrypoint $@";
};
# Codigo principal
_log "Started";
# Garantir existencia dos diretorios
mkdir -p /root/log;
mkdir -p /run;
mkdir -p /project;
# Logs
touch /root/log/supervisord.log;
touch /var/log/lastlog;
# Preparativos do servidor SSH
# Pasta de configs e chaves do servidor
mkdir -p /root/ssh;
# Pasta de configs e chaves do usuario
mkdir -p /root/.ssh;
# Pasta de trabalho do sshd
mkdir -p /run/sshd;
chown root:root /run/sshd;
chmod 0755 /run/sshd;
# Arquivo de cadastro de cahves conviaveis dos clientes
touch /root/ssh/trusted_keys;
touch /root/.ssh/authorized_keys;
# Chaves do servidor SSH
[ -s /root/ssh/ssh_host_rsa_key ] || {
_log "SSHD creating key ssh_host_rsa_key";
ssh-keygen -t rsa -b 4096 -f /root/ssh/ssh_host_rsa_key -N "";
chmod 600 /root/ssh/ssh_host_rsa_key;
};
[ -s /root/ssh/ssh_host_ecdsa_key ] || {
_log "SSHD creating key ssh_host_ecdsa_key";
ssh-keygen -t ecdsa -b 521 -f /root/ssh/ssh_host_ecdsa_key -N "";
chmod 600 /root/ssh/ssh_host_ecdsa_key;
};
[ -s /root/ssh/ssh_host_ed25519_key ] || {
_log "SSHD creating key ssh_host_ed25519_key";
ssh-keygen -t ed25519 -f /root/ssh/ssh_host_ed25519_key -N "";
chmod 600 /root/ssh/ssh_host_ed25519_key;
};
# Chave de usuario local pre-provisionada
[ -s /root/.ssh/id_ed25519 ] || {
_log "SSHD creating local key id_ed25519";
ssh-keygen -t ed25519 -f /root/.ssh/id_ed25519 -N "";
chmod 600 /root/.ssh/id_ed25519;
};
# Servidor SSH padrao StrictModes=yes requer acesso seguro
chown -R root:root /root;
chmod 700 /root/ssh;
chmod 600 /root/ssh/trusted_keys;
chmod 600 /root/.ssh/authorized_keys;
chmod 755 /root;
# Preparativos do crontab
crondirs="
1min 10min 15min 30min
hourly daily weekly monthly
sunday monday tuesday wednesday
thursday friday saturday
";
for cdir in $crondirs; do
mkdir -p /root/cron/$cdir;
done;
# Comando
EXEC_CMD="$@"
_log "CMD arg: $EXEC_CMD";
# Rodar CMD
if [ "x$EXEC_CMD" = "x" ]; then
FULLCMD="exec sleep 252288000";
else
FULLCMD="exec $EXEC_CMD";
fi;
_log "CMD exe: $FULLCMD";
eval $FULLCMD;
3.5 – Dockerfile
Construtor da imagem. Aditivos:
- Debian com ferramentas básicas;
- NodeJS e NPM – versão oficial do Debian (node 22);
- Agente Claude;
- Agente OpenCode;
- Extensões VS Code.
Arquivo Dockerfile
# syntax=docker/dockerfile:1.7
# Preparativos dos arquivos externos
#--------------------------------------------------- stage:files
FROM alpine:latest as STAGE_FILES
RUN set -eux; \
apk update; \
apk upgrade; \
apk add tar gzip bzip2 xz zstd
RUN set -eux; \
mkdir -p /rootfs/opt; \
mkdir -p /rootfs/etc; \
mkdir -p /rootfs/etc/ssh
COPY supervisord.conf /rootfs/etc/supervisord.conf
COPY sshd_config /rootfs/etc/ssh/sshd_config
COPY crontab /rootfs/etc/crontab
COPY entrypoint.sh /rootfs/opt/entrypoint.sh
RUN set -eux; \
chmod +x /rootfs/opt/entrypoint.sh; \
chown -R root:root /rootfs; \
cd /rootfs; \
tar -cpzvf /files.tgz .
# Coder Server pronto, continuando
#--------------------------------------------------- stage:main
FROM codercom/code-server:latest
USER root
ENV LANG=C.UTF-8 \
LC_ALL=C.UTF-8
# Instalar pacotes basicos
RUN \
apt -y update; \
apt -y upgrade; \
apt -y dist-upgrade; \
apt -y full-upgrade; \
\
apt -y install --no-install-recommends \
bash locales tzdata sudo strace \
openssl wget curl ca-certificates \
iproute2 htop iputils-ping \
bzip2 tar gzip xz-utils zstd zip unzip \
util-linux coreutils procps psmisc \
brotli gosu sqlite3 ripgrep \
less logrotate lsof \
sed grep mawk \
mc nano jq git \
file findutils \
iputils-ping traceroute \
supervisor
# Instalar aditivos do ambiente de programacao
RUN apt -y install docker-cli \
docker-compose \
docker-buildx \
rsync \
openssh-server \
openssh-sftp-server \
openssh-client \
nodejs npm
# Instalar opencode
ENV OPENCODE_CONFIG_DIR=/root/opencode
ENV TMPDIR=/tmp
RUN npm install -g opencode-ai@latest; \
opencode --version
# Instalar claude-code
RUN npm install -g @anthropic-ai/claude-code; \
claude --version
# Arquivos personalizados
COPY --from=STAGE_FILES /files.tgz /tmp/files.tgz
# Ajustes de ambiente
RUN set -eux; \
tar -xvf /tmp/files.tgz -C /; \
rm -f /tmp/files.tgz; \
locale-gen en_US.UTF-8; \
locale-gen pt_BR.UTF-8; \
update-locale LANG=en_US.UTF-8; \
echo 'LANG=en_US.UTF-8' > /etc/default/locale; \
sed -i '/LANG\|LC_\|LANGUAGE/d' /etc/environment; \
( \
echo; \
echo; \
echo " Coder IDE Container"; \
echo; \
echo; \
) > /etc/motd; \
true
# Diretorio padrao inicial
WORKDIR /project
# Entrypoint para supervidord
# /opt/entrypoint.sh /usr/bin/supervisord -n -c /etc/supervisord.conf
ENTRYPOINT ["/opt/entrypoint.sh"]
CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisord.conf"]
3.6 – Construção da imagem
Execute o build para construir a imagem:
# Constuir imagem:
docker build . \
-f Dockerfile \
-t coder-bundle:latest;
3.7 – Rede
Criando a rede para containers (network_public):
# 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;
3.8 – Rodando
Agora basta usar a imagem coder-bundle:latest para ter um super ambiente de desenvolvimento isolado, na nuvem, cheio de bots trabalhadores.
#!/bin/sh
# Nome do container
NAME="coder-ide";
# Senha de acesso
PASSWORD="tulipa_coder";
# Nome de DNS
FQDN="coder.$(hostname -f)";
#FQDN="coder.seudominio.com";
# Diretorio do volume
DATADIR="/storage/$NAME";
# Pasta no HOST
mkdir -p $DATADIR;
# Pasta do projeto para mapear em /project do container
mkdir -p $DATADIR/project;
# Raiz do sistema
mkdir -p $DATADIR/.config/code-server;
# Logs do sistema no volume
mkdir -p $DATADIR/log;
# Ajustar permissoes, login 'coder' uid 1000
chown -R 1000:1000 $DATADIR/project;
chown -R 1000:1000 $DATADIR/.config;
# Renovar/rodar:
# - Parar atual
docker container stop $NAME 2>/dev/null;
docker container rm $NAME 2>/dev/null;
# - Rodar com imagem renovada
docker container run \
-d \
--user root \
--restart always \
--name $NAME \
--hostname $NAME.intranet.br \
\
--cpus 2.0 \
--cpu-shares 1024 \
--memory 2g \
--memory-swap 2g \
--memory-reservation 1g \
--shm-size 1g \
\
--network network_public \
--ip 10.249.111.201 \
\
-p 7422:22 \
-p 7480:8080 \
\
--tmpfs /run:rw,noexec,nosuid,size=512m \
--tmpfs /tmp:rw,exec,suid,mode=1777,dev,size=512m \
\
-v $DATADIR:/root \
-v $DATADIR/log:/var/log \
-v $DATADIR:/home/coder \
-v $DATADIR/project:/project \
\
-v /var/run/docker.sock:/var/run/docker.sock \
\
-e DEFAULT_WORKSPACE=/project \
-e DOCKER_USER=root \
-e PASSWORD=$PASSWORD \
\
--label "traefik.enable=true" \
--label "traefik.http.routers.$NAME.rule=Host(\`$FQDN\`)" \
--label "traefik.http.routers.$NAME.entrypoints=web,websecure" \
--label "traefik.http.routers.$NAME.tls=true" \
--label "traefik.http.routers.$NAME.tls.certresolver=letsencrypt" \
--label "traefik.http.services.$NAME.loadbalancer.server.port=8080" \
\
coder-bundle:latest;
O acesso do container acima está na porta HTTP 7480
Nota: Recomendo configurar o Traefik e escolher um nome real para a variável FQDN, ao funcionar, retire a porta 7480 (HTTP sem criptografia) do container.
O acesso pelo termina web do Coder não é grandes coisas… acesse via SSH para rodar os agentes e comandos com mais liberdade, o acesso pode ser direto na porta SSH 7422.
#
# Coder IDE Container
#
# root@coder-ide:~#
ls -1 /usr/local/bin/
# claude
# opencode
# Pasta padrao para projetos
cd /project;
3.9 – Extensões
Para instalar extensões via linha de comando, siga os exemplos abaixo:
#
# Coder IDE Container
#
# Instalar extensoes do VS Code
# - Python Plugin
code-server --force --install-extension ms-python.python;
# - GoLang Plugin
code-server --force --install-extension golang.go;
# - Claude Plugin
code-server --force --install-extension anthropic.claude-code;
# - Docker Plugin
code-server --force --install-extension ms-azuretools.vscode-docker;
# - OpenCode Plugin
code-server --force --install-extension sst-dev.opencode;
4 – Stack Coder e Agentes
Se você não gosta do bundle com muitos programas no mesmo container (entendo, isso deixa a imagem grande e dificulta a escala), podemos rodar o Coder e os agentes separados, cada um no seu container e com suas particularidades.
Eu publiquei as seguintes imagens;
- tmsoftbrasil/coder-bundle – Coder construído no capítulo 3;
- tmsoftbrasil/coder-simple – Coder com SSH e crontab, sem agentes;
- tmsoftbrasil/agent-pi – Agente Pi enlatado;
- tmsoftbrasil/opencode – Agente OpenCode enlatado;
Vamos rodar os 3 separados mas num mesmo “pod” para criar um ambiente mais adequado para VibeCoding.
Rede:
# 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;
Container coder-simple:
#!/bin/sh
# Variaveis
NAME="coder";
PASSWORD="tulipa_coder";
FQDN="coder.$(hostname -f)";
#FQDN="coder.seudominio.com";
# Diretorio do volume
DATADIR="/storage/$NAME";
mkdir -p $DATADIR;
mkdir -p $DATADIR/project;
mkdir -p $DATADIR/.config/code-server;
mkdir -p $DATADIR/log;
chown -R 1000:1000 $DATADIR/project;
chown -R 1000:1000 $DATADIR/.config;
# Renovar/rodar:
docker container stop $NAME 2>/dev/null;
docker container rm $NAME 2>/dev/null;
docker container run \
-d \
--user root \
--restart always \
--name $NAME \
--hostname $NAME.intranet.br \
\
--cpus 2.0 \
--cpu-shares 1024 \
--memory 2g \
--memory-swap 2g \
--memory-reservation 1g \
--shm-size 1g \
\
--network network_public \
--ip 10.249.111.123 \
\
-p 7422:22 \
\
--tmpfs /run:rw,noexec,nosuid,size=512m \
--tmpfs /tmp:rw,exec,suid,mode=1777,dev,size=512m \
\
-v $DATADIR:/root \
-v $DATADIR/log:/var/log \
-v $DATADIR:/home/coder \
-v $DATADIR/project:/project \
\
-e DEFAULT_WORKSPACE=/project \
-e DOCKER_USER=root \
-e PASSWORD=$PASSWORD \
\
--label "traefik.enable=true" \
--label "traefik.http.routers.$NAME.rule=Host(\`$FQDN\`)" \
--label "traefik.http.routers.$NAME.entrypoints=web,websecure" \
--label "traefik.http.routers.$NAME.tls=true" \
--label "traefik.http.routers.$NAME.tls.certresolver=letsencrypt" \
--label "traefik.http.services.$NAME.loadbalancer.server.port=8080" \
\
coder-simple:latest;
Container opencode:
#!/bin/sh
# Variaveis
NAME="opencode-senior";
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;
# Renovar/rodar
docker container rm -f $NAME 2>/dev/null
# --read-only
docker container run \
-d \
--user root \
--restart always \
--name $NAME \
--hostname $NAME.intranet.br \
\
--cpus 2.0 \
--cpu-shares 1024 \
--memory 2g \
--memory-swap 2g \
--memory-reservation 1g \
--shm-size 1g \
\
--network network_public \
--ip 10.249.111.134 \
-p 9422:22 \
\
-v $DATADIR/data:/data \
-v $DATADIR/cache:/cache \
-v $DATADIR/workspace:/workspace \
\
-v /storage/coder/project:/project \
-w /project \
\
$IMAGE;
x
.
Terminamos por hoje!
Patrick Brandão, patrickbrandao@gmail.com
“Uma só mão não aplaude“
Provérbio Árabe
