/opt/server/projetos/<nome>/ deve cumprir pra rodar limpo dentro do server-stack. É opinado por design — segue os padrões aqui e o projeto encaixa sem fricção; foge deles e cria atrito que vai cobrar caro depois.
Escopo
Aplica a: projetos que rodam em containers Docker dentro de/opt/server/projetos/<nome>/, expostos pelo NPM, consumindo a infra compartilhada (Postgres, Redis).
Não aplica a: binários standalone, scripts cron, jobs disparados pelo n8n, ou qualquer coisa que não rode permanentemente como serviço web.
O que este documento NÃO regula:
- Linguagem, framework ou arquitetura interna do projeto
- Padrões de código (tipo style, naming de classes, organização de pastas dentro do app)
- Decisões de domínio
Arquivos obrigatórios no root do projeto
| Arquivo | Função |
|---|---|
Dockerfile | Como buildar a imagem |
docker-compose.yml | Como subir os containers do projeto |
.dockerignore | Excluir lixo do build context (.git, vendor/, node_modules/, .env, etc.) |
.env.example | Template versionado das variáveis de ambiente, sem segredos |
CLAUDE.md | (Recomendado) Apontar Claude do projeto pra estes standards e descrever o domínio |
docker-compose.staging.yml/docker-compose.prod.yml(este é servidor mono-ambiente)- Containers de banco/cache/proxy próprios (use a infra compartilhada)
entrypoint.shcomplexo orquestrando wait-for-DB + migrate + cache + supervisord (use mecanismos do compose e do framework).envversionado (sempre.gitignore)
Contrato do docker-compose.yml
Template mínimo:
- Service
appé o container web principal. Variantes (worker, queue, scheduler) ficam comoapp-queue,app-worker, etc. container_nameexplícito com prefixo do projeto. Sem isso, Docker prefixa pelo nome da pasta e o DNS interno fica imprevisível.restart: unless-stoppedsempre — sobrevive a reboots do host.- Sem
ports:no host — acesso externo é via NPM Proxy Host. Exceção em Portas administrativas abaixo. networks: [server_net]external — a rede é criada pelobootstrap.shda plataforma; o compose só consome.- Bind mounts pros arquivos que precisam persistir:
.env(lido pelo app) estorage/(uploads/logs/cache).
Convenções de naming
Consistência aqui faz a diferença entre “intuitivo” e “decorebá”.| Item | Padrão | Exemplo |
|---|---|---|
| Repo no GitHub | livre (PascalCase ou kebab-case) | SistemaShalom |
| Pasta no servidor | igual ao repo | /opt/server/projetos/SistemaShalom/ |
| Deploy name (slug curto) | lowercase, sem hífens se possível | shalom |
container_name | <deploy>-<role> | shalom-app, shalom-queue |
image | <deploy>-<role>:latest | shalom-app:latest |
| Subdomínio público | <deploy>.<dominio> | shalom.example.com |
| DB no Postgres compartilhado | igual ao deploy ou bem próximo | sistemashalom |
| Role do DB | igual ao DB name | sistemashalom |
infra/postgres/secrets/<deploy>.env) usam ele como prefixo.
Banco de dados
Use o Postgres compartilhado do server-stack. Provisionamento por projeto:infra/postgres/secrets/<deploy>.env. Detalhes em Criar bancos.
Depois disso, no .env do projeto:
infra/redis/.env. Pra evitar coleção de keys entre projetos:
- Use prefixos no app (
<deploy>:cache:foo) - OU use DBs diferentes (
redis://default:PASS@redis:6379/3)
Reverse proxy e portas
Apenas o NPM publica portas no host (80/443). Projetos não publicam portas externas. Acesso externo = configurar Proxy Host na UI do NPM apontando pro container do projeto.App principal
- Domain:
<deploy>.<seu-dominio> - Forward Hostname:
<container_name>(ex.:shalom-app) - Forward Port:
80(interno do app) - SSL: Request new + Force SSL + HTTP/2
Portas administrativas
Se o projeto tem uma UI de admin separada (ex.: porta8025 do Mailhog), bind em loopback:
ufw deny 8025 não bloqueia portas Docker. Bindar em 127.0.0.1 é a única defesa confiável que não depende de firewall do host. Acesso externo dessas portas vira SSH tunnel ou Proxy Host adicional no NPM.
Volumes e permissões
Bind mounts em./<pasta>/ são preferidos a named volumes — backup é só copiar a pasta, fica visível no filesystem do host.
Gotcha de UID em bind mounts
A maioria das imagens oficiais (Postgres, Redis, etc.) faz self-chown do volume no startup, então bind mount funciona out-of-the-box. Outras não. Caso real registrado: n8n. O container roda como usernode (UID 1000) e tenta escrever em /home/node/.n8n/config. Se a pasta ./data foi criada pelo Docker como root (acontece no primeiro up se a pasta não existia), o n8n falha em loop com EACCES: permission denied.
Padrão defensivo: se a imagem que você vai usar não faz self-chown e roda como user específico, pré-crie a pasta com o dono correto. A forma mais clean (sem sudo) é via container alpine descartável:
bootstrap.sh da plataforma faz isso automaticamente pro n8n. Se você adicionar um serviço novo com o mesmo perfil (user não-root + sem self-chown), adicione um bloco equivalente lá.
Variáveis de ambiente
.env.exampleversionado, com placeholders vazios. Documenta o que precisa ser configurado..envreal gitignored sempre. Nunca commit.- Single env model: um único
.env, sem.env.stagingou.env.prod. Esse servidor é dev-only; produções vivem em outro lugar. - APP_KEY (e similares) geradas uma vez via
openssl rand -base64 32(ouphp artisan key:generate) e copiadas pro.env. Não viram no.env.example.
.env na primeira deployar:
Dockerfile patterns
Camadas cacheáveis
Ordem das camadas afeta diretamente o tempo de build em iterações. Padrão:Comandos stateful: build time, não runtime
Comandos que produzem efeitos no filesystem do container (storage:link, route:cache, view:cache, criação de symlinks) devem rodar no Dockerfile durante build, não no entrypoint.
Por quê: se rodar no entrypoint, o efeito vive só no filesystem desse container. Quando você faz docker compose up -d --force-recreate ou um restart, perde o estado e precisa rodar de novo. Bake na imagem garante que o estado está lá toda vez que o container sobe.
migrate deve ser manual após deploy, não no entrypoint nem no Dockerfile. Migrations em runtime criam race conditions quando você sobe múltiplas réplicas; em build, a imagem fica “amarrada” a um schema específico. Padrão é:
Recomendação por stack
- Laravel / PHP: FrankenPHP base (
dunglas/frankenphp:latest). Veja Laravel-specific pra detalhes. - Node:
node:lts-alpine(multi-stage com build separado em produção) - Python:
python:slim
Anti-padrões
Lista negra de coisas que parecem boa ideia até cobrarem caro.❌ Postgres/Redis próprio dentro do compose do projeto
Por quê: Quebra o “uma infra pra todos”, duplica backup, gasta RAM, e a versão da DB fica desalinhada entre projetos. Alternativa: Use o compartilhado, ver Banco de dados.❌ Publicar portas no host (exceto admin loopback)
Por quê: Expõe diretamente, conflita com outros projetos, força você a tracking manual de portas livres, bypassa o UFW. Alternativa: NPM Proxy Host pro app principal;127.0.0.1:porta:porta pra UIs admin.
❌ Múltiplos compose files (compose.staging.yml, compose.prod.yml)
Por quê: Esse servidor é dev-only. Ambientes separados viram outros servidores ou outras redes Docker, não overrides de compose dentro do projeto.
Alternativa: Um docker-compose.yml, um .env. Quando precisar de homolog/prod, é setup separado.
❌ migrate, route:cache, storage:link em entrypoint runtime
Por quê: Race conditions, perda de estado em recreates, deploy não-determinístico.
Alternativa: Stateful no Dockerfile (build time), migrate manual após deploy.
❌ Confiar em UFW pra portas publicadas pelo Docker
Por quê: Docker escreve regras de iptables que rodam antes das do UFW.ufw deny 81/tcp não bloqueia uma porta 81:81 no compose.
Alternativa: 127.0.0.1:81:81 no compose (só loopback) ou regra na chain DOCKER-USER do iptables.
❌ .env, data/, secrets/ versionados
Por quê: Vazamento permanente em git history. Mesmo private repo, mesmo deletado depois, o blob persiste.
Alternativa: .gitignore rigoroso. Veja exemplo no server-stack.
❌ DB::statement com SQL específico de um driver (em projetos Laravel)
Por quê: Quebra portabilidade. MySQL aceita MODIFY COLUMN ENUM(...), Postgres não. Mesmo se hoje você só usa MySQL, amanhã pode mudar.
Alternativa: Use Schema::Blueprint (portável). Se realmente precisa SQL bruto, ramifique por driver via DB::connection()->getDriverName().
Onboarding de projeto novo
Passo a passo pra colocar um projeto novo no servidor:Criar DNS
No registrador, criar
CNAME <deploy> apontando pro apex (ou A direto).
Ver Configurar DNS.Configurar Proxy Host no NPM
Domain
<deploy>.<dominio> → <container>:80 com Let’s Encrypt.
Ver Nginx Proxy Manager.Onboarding de projeto existente
Pra trazer um projeto que já roda em outro servidor (ou em outro padrão de Docker) pro server-stack, segue um playbook adaptado do caso real do SistemaShalom (Laravel + MySQL → Postgres).Auditoria
Antes de mexer em qualquer coisa, mapeie:
- Já tem Dockerfile? Se sim, qual padrão (php-fpm+nginx, FrankenPHP, etc.)?
- Já tem docker-compose? Quais serviços ele sobe?
- Migrations são portáveis ou usam SQL específico de driver?
- O DB de prod (se existir) tem dados a preservar?
- Existem múltiplas branches divergentes (cuidado com isso — cheque
git branch -r)
Substituir setup Docker
- Dropar
docker/(entrypoint, nginx.conf, supervisord.conf) se existirem - Substituir
Dockerfilepelo padrão da stack (FrankenPHP pra Laravel, etc.) - Substituir
docker-compose.ymlpelo padrão server-stack (server_net, sem postgres próprio, sem porta exposta) - Dropar
compose.staging/prod.ymle.env.staging/prod.example
Adaptar `.env.example`
Apontar pro Postgres compartilhado:
DB_HOST=postgres, DB_DATABASE=<deploy>, etc.(Stack-specific) Resolver pegadinhas
Pra Laravel veja Laravel-specific — TrustProxies, migrations portáveis, etc.
(Opcional) Squash de migrations
Se o projeto tem 20+ migrations com refactors históricos, vale consolidar em N migrations representando o estado final. Procedimento documentado por projeto se for o caso.
Restore do DB de prod (se aplicável)
pg_dump(ou conversão MySQL→PG viapgloader) no servidor antigo- Transfer pro servidor novo (NÃO via git — ver anti-padrões)
create-db.sh <deploy>pra preparar role + DB destinopsql ... -d <deploy> < dump.sqlpra restaurar- Se houve squash: aplicar
database/squash-transition.sql(ou equivalente) pra alinhar a tabelamigrations
Validar
migrate:status(se Laravel) — tudo Yes- Smoke test no browser
- Verificar dados via tinker / psql
Páginas relacionadas
- Laravel-specific — TrustProxies, FrankenPHP, migrations portáveis, queue worker
- Repositório —
.gitignoreda plataforma - Configurar DNS — registros pro subdomínio do projeto
- Nginx Proxy Manager — configurar Proxy Host
- Criar bancos —
create-db.sh