Arquitetura
Serviços compartilhados#
Um único conjunto de contêineres de serviço atende todos os projetos da máquina.
Redis, Memcached e o capturador de e-mails rodam sempre. Os servidores de banco de dados não: cada um inicia na primeira vez que um projeto da máquina precisa dele, e depois continua ligado. Uma máquina só com projetos Postgres nunca roda MariaDB nem MongoDB.
Essa é a decisão de design central do Zeltro, e ela é uma troca, não uma vitória de graça — vale entendê-la antes de montar um fluxo de trabalho em cima dela.
O que você ganha. A maioria das configurações Docker por projeto inicia um contêiner de banco de dados
por projeto. Vinte projetos rodando significam vinte contêineres de banco de dados. O Zeltro roda
um. Em uma estação de trabalho de desenvolvimento, essa costuma ser a diferença entre
"consigo deixar todos os sites dos meus clientes no ar" e "consigo deixar três". É também por isso que
o .env de um projeto não precisa de nenhuma configuração de banco de dados por projeto: os nomes de host abaixo são
os mesmos em todos os projetos, para sempre.
Do que você abre mão. Estes pontos decorrem diretamente da mesma decisão:
- Uma versão de cada motor, para a máquina inteira. Os servidores compartilhados rodam MariaDB 12, PostgreSQL 17 e MongoDB 8. Um projeto que precise de MySQL 5.7 ou PostgreSQL 15 não consegue isso deles. Fixe a versão por projeto e você volta a ter um contêiner por projeto.
- Ciclo de vida compartilhado.
zeltro down <project>para só aquele projeto, maszeltro stop-servicespara os serviços de todos os projetos, não só daquele em que você estava trabalhando. - Raio de impacto compartilhado. Um volume de dados corrompido, uma migration descontrolada ou um
DROP DATABASEafeta um servidor que todos os outros também estão usando. Os bancos de dados dos projetos ficam isolados pelo nome, não pelo processo. - Credenciais compartilhadas. Todo projeto se conecta como
root. Esta é uma ferramenta de desenvolvimento local, e os servidores de banco de dados não publicam nenhuma porta fora da rede Docker (só a interface web do capturador de e-mails, porta 8025, é publicada no host), mas isso não é um modelo para copiar em produção.
Quando o Zeltro é a escolha errada: você precisa de versões de banco de dados por projeto, ou precisa que o seu ambiente local espelhe exatamente a topologia de produção. Ferramentas que rodam uma stack completa por projeto — DDEV, Lando ou Docker Compose puro — servem melhor nos dois casos, ao custo do consumo de recursos descrito acima.
Quando é a escolha certa: você roda muitos projetos pequenos e médios em uma máquina e prefere gastar essa memória com os projetos do que com vinte cópias do MariaDB.
Sempre ligados:
| Serviço | Nome de host | Porta | Usuário | Senha |
|---|---|---|---|---|
| Redis 8 | zeltro-redis |
6379 | — | (nenhuma) |
| Memcached 1.6 | zeltro-memcached |
11211 | — | (nenhuma) |
| Capturador de e-mails (Mailpit) | zeltro-mailhog |
SMTP 1025 / UI 8025 | — | (nenhuma) |
O capturador de e-mails é o Mailpit, um substituto do MailHog. Ele mantém o nome de host
zeltro-mailhog para que os projetos existentes não precisem de mudanças. A interface web dele também é publicada
no host em http://localhost:8025.
Bancos de dados, iniciados quando um projeto precisa de um:
| Serviço | Nome de host | Porta | Usuário | Senha |
|---|---|---|---|---|
| MariaDB 12 (compatível com MySQL) | zeltro-mariadb |
3306 | root |
(vazia) |
| PostgreSQL 17 | zeltro-postgres |
5432 | root |
password |
| MongoDB 8 | zeltro-mongo |
27017 | root |
password |
zeltro new, clone, setup, install e up descobrem qual banco de dados um
projeto usa e ativam esse servidor se ele ainda não estiver rodando. Você não precisa
fazer nada. Para ligar um manualmente, use zeltro enable-service mysql,
postgres ou mongo.
Use esses nomes de host e credenciais diretamente ao configurar um projeto — não é preciso inspecionar contêineres para descobri-los. O Zeltro os escreve automaticamente no .env de cada projeto.
Serviços compartilhados opcionais#
Tudo, exceto Redis, Memcached e o capturador de e-mails, é opcional: os três bancos de dados acima, além de armazenamento de objetos, um mecanismo de busca e interfaces web de administração. Eles ficam atrás de profiles do Docker Compose e permanecem desligados até que uma máquina os peça, para que a máquina não gaste RAM com serviços que nenhum dos seus projetos usa.
zeltro enable-service minio
zeltro enable-service adminer
zeltro disable-service minio # data volume is kept
zeltro enable-service --help # list every optional service
Depois de ativados, eles iniciam a cada zeltro up e são resolvidos pelo nome de host de dentro de qualquer contêiner de projeto, exatamente como os serviços sempre ligados. A lista de ativados fica guardada em OPTIONAL_SERVICES em /etc/zeltro-cli/.env.
| Serviço | Slug | Host | Porta | Credenciais |
|---|---|---|---|---|
| Armazenamento S3 compatível com MinIO (Silo, o fork mantido do MinIO) | minio |
zeltro-minio |
API 9000 / console 9001 | root / password |
| Meilisearch (busca de texto completo) | meilisearch |
zeltro-meilisearch |
7700 | master key zeltro-dev-master-key |
| phpMyAdmin (MariaDB) | phpmyadmin |
zeltro-phpmyadmin |
80 | root, sem senha |
| Adminer (MariaDB, PostgreSQL, SQLite, MongoDB) | adminer |
zeltro-adminer |
8080 | root e a senha daquele banco de dados |
| Mongo Express | mongo-express |
zeltro-mongo-express |
8081 | nenhuma |
| RedisInsight | redisinsight |
zeltro-redisinsight |
5540 | nenhuma |
As interfaces de administração já vêm conectadas aos servidores compartilhados. O RedisInsight só mostra o Redis compartilhado depois que você aceita a tela de termos da primeira execução.
Como um projeto é acessado#
São dois mecanismos diferentes, e vale a pena não confundi-los:
- Contêiner → contêiner: pelo nome. O DNS embutido do Docker resolve os nomes dos contêineres na rede compartilhada. O código dentro de um projeto pode fazer
psql -h zeltro-postgresoufetch('http://other-project/')sem nenhuma configuração extra. É assim que os projetos compartilham bancos de dados e chamam uns aos outros. - Host ou LAN → projeto: pelo endereço. O seu navegador não está na rede Docker, então ele usa um endereço. O
zeltro statusmostra dois para cada projeto:
| De onde | Endereço | Por quê |
|---|---|---|
| A máquina que roda o Zeltro | http://10.x.x.219 |
O IP do próprio contêiner. Sem porta — não há mais nada nesse endereço. |
| Outro dispositivo na LAN | http://192.168.1.20:219 |
O IP desta máquina mais a porta publicada do projeto, que é o último número do IP do contêiner. |
No macOS, o Docker Desktop roda os contêineres dentro de uma máquina virtual e não consegue rotear tráfego do host para os IPs dos contêineres, então o zeltro status mostra http://localhost:<port> como endereço local.
No Windows, o Zeltro roda dentro do WSL2. O endereço local funciona de dentro do WSL. Em um navegador do Windows, use o endereço LAN, que é o IP da VM do WSL e muda quando o WSL reinicia.
O Zeltro não escreve no /etc/hosts, então http://my-api/ não funciona em um navegador no host. Use os endereços que o zeltro status mostra. Versões anteriores adicionavam uma entrada de host para cada projeto; isso acabou, e criar ou iniciar um projeto não precisa mais de sudo para isso.
VPC e alocação de IPs#
Todos os contêineres do Zeltro se conectam à rede Docker zeltro-cli_vpc (${VPC_SUBNET}.0/24, definida por máquina em /etc/zeltro-cli/.env). O espaço de endereços é dividido para que IPs estáticos nunca colidam com dinâmicos:
| Faixa | Finalidade | Alocação |
|---|---|---|
.2–.8 |
Serviços sempre ligados, bancos de dados e phpMyAdmin | Estática |
.32–.63 |
Contêineres auxiliares (workers, agendadores) | Dinâmica |
.100–.239 |
Pontos de entrada dos projetos | Estática, sorteada por projeto |
.250–.254 |
MinIO, Meilisearch, Adminer, Mongo Express, RedisInsight | Estática |
.240–.249 fica livre como folga. A porta web de cada projeto é publicada no host com o último octeto do IP, então um projeto em .219 responde na porta 219.
Ao escrever um compose personalizado: dê ao serviço exposto à web um IP estático em .100–.239, deixe os auxiliares sem ipv4_address e nunca mexa em .2–.8 nem em .250–.254.
Imagens base#
Projetos criados do zero são construídos a partir de uma de três imagens no Docker Hub, em canebaycomputers/cbc. Cada uma roda nginx + supervisor no Ubuntu Noble. O código-fonte fica em CaneBayComputers/docker-images, um diretório por imagem.
Substitua a imagem padrão com --image <ref> em new, clone, setup ou install.
cbc:nginx-php8 — projetos PHP#
- nginx com FastCGI para o
php-fpm8.3por um socket Unix; raiz web/usr/share/nginx/html/public - PHP 8.3 com
pdo_mysql,pdo_pgsql,pdo_sqlite,redis,mongodb,gd,intl,mbstring,imagick,zip,soap,xdebug, além dephp-codesnifferephpmd - Composer, WP-CLI e Node 22 (para builds com Vite/npm) incluídos
- O supervisor roda o nginx, o php-fpm e um worker de fila do Laravel (4 processos, início automático)
cbc:nginx-python3 — projetos Python#
- nginx como proxy reverso, porta 80 →
localhost:8000, com cabeçalhos de upgrade para WebSocket - Pré-instalados:
fastapi,uvicorn,django,flask,gunicorn,python-dotenv,sqlalchemy,psycopg2-binary,PyMySQL,pymongo,redis,pymemcache - O supervisor roda
PYTHON_APP_COMMAND; o app precisa escutar na porta 8000 python3está disponível;pythonnão
cbc:nginx-node — projetos Node#
- nginx como proxy reverso, porta 80 →
localhost:3000, com cabeçalhos de upgrade para WebSocket - Node 22 LTS
- O supervisor roda
NODE_APP_COMMAND; o app precisa escutar na porta 3000. DefinaPORT=3000no.envpara frameworks que usam outra porta por padrão.
Os contêineres dos projetos são recriados a partir da imagem base a cada zeltro up. Tudo o que for instalado no sistema de arquivos de um contêiner em execução se perde — só o diretório do projeto montado (bind mount) sobrevive. É por isso que o runtime de cada framework já vem na imagem, e por isso um banco de dados SQLite precisa ficar no diretório do projeto.
Adaptação do compose#
Quando o zeltro clone ou o zeltro setup encontra um docker-compose.yaml com mais de um serviço ou com uma imagem que não é cbc, o Zeltro o adapta:
- Os serviços embutidos
postgres/mysql/mariadb/redis/valkey/mongodbsão removidos, e as variáveis de ambiente que apontavam para eles passam a apontar para os nomes de host compartilhados - O serviço exposto à web recebe um IP estático na VPC e um
container_nameigual ao do projeto - Os outros serviços se conectam à rede sem IP fixo
- O original é guardado como
docker-compose.upstream.yaml(só na primeira execução, para que o verdadeiro original sobreviva às execuções seguintes) - O
docker-compose.yamlé adicionado ao.gitignore, para que o arquivo gerenciado pelo Zeltro não seja enviado de volta a um repositório compartilhado
O tipo de imagem só afeta essa adaptação. As etapas do framework — instalação de dependências, configuração do .env, link simbólico do storage, migrations — dependem da detecção do framework e também rodam em projetos adaptados.
Flags úteis: --no-startup para revisar o compose adaptado antes de ele subir, --overwrite-env para apontar o .env de um app existente para os serviços compartilhados preservando o APP_KEY, --db-name <name> para escolher o banco de dados, --no-migration para pular as migrations.
Estrutura de um projeto#
~/zeltro-projects/
├── my-api/
│ ├── docker-compose.yaml # Zeltro-managed (gitignored)
│ ├── docker-compose.upstream.yaml # the original, if there was one
│ ├── AGENTS.md # hand-off context for AI agents
│ ├── .env
│ └── [your project files]
└── my-shop/
~/zeltro-projects é o padrão. O zeltro projects-dir mostra o que esta máquina usa. O diretório de projetos também recebe um AGENTS.md próprio com os nomes dos serviços desta máquina.
A configuração de runtime fica em /etc/zeltro-cli/.env. Uma máquina instalada antes da mudança de nome para Zeltro pode ainda usar nomes antigos de contêineres e redes; o zeltro migrate-names passa para os nomes zeltro-* sem perder dados.
Achou um erro nesta tradução? Avise a gente no GitHub.