DCM – Docker Compose Maker: Template everything!

DCM: o Docker Compose Maker e a lição do Standard Operating Environment que vem embrulhada nele

Olá a todos!

Prefácio do costume: o que se segue é opinião. Formou-se a partir do que corre na minha casa, de uns bons cem ficheiros compose escritos à mão ao longo dos anos, e de uma carreira passada em ambientes onde um servidor “diferente dos outros” era sinónimo de uma madrugada perdida. Vale o que vale.

Há uma experiência que quase toda a gente com um homelab já viveu. Abres a documentação de uma aplicação nova, procuras a secção de Docker Compose, copias o exemplo e mudas três coisas: a porta, porque a 8080 já está ocupada; o caminho do volume, porque o exemplo usa ./config e tu guardas tudo em /srv; e o fuso horário, porque o exemplo vem com America/New_York. Seis meses depois tens vinte e tal ficheiros, cada um com a sua maneira de dizer onde vivem os dados, metade com o UID 1000 escrito à mão, um ou dois a correr como root porque na altura não apeteceu investigar o erro de permissões, e nenhum a dizer que versão da imagem está a correr.

O DCM, o Docker Compose Maker, nasceu para evitar isso. À superfície é uma página web onde escolhes aplicações de uma lista e recebes um docker-compose.yaml e um .env prontos a usar. Mas por baixo, há uma ideia de engenharia que me parece fascinante: reduzir dezenas de aplicações diferentes ao mesmo punhado de variáveis.

Quem já trabalhou com Standard Operating Environments em empresas reconhece o padrão à primeira. O DCM é um SOE em ponto pequeno, com as virtudes de qualquer SOE e com os mesmos problemas de qualquer SOE que não tem ninguém a tratar dele.

Como diria o Spock, fascinating. Vamos por partes.

O que é o DCM, em concreto

O DCM é um projeto open source mantido por ajnart no GitHub, com cerca de 1,4 mil estrelas à data em que escrevo, escrito em TypeScript sobre Next.js. Podes usá-lo de três formas: na versão online, em compose.ajnart.dev; em casa, com uma imagem multi-arquitetura (amd64, arm64 e armv7, por isso até um Raspberry Pi antigo serve); ou compilado com o Bun.

Para experimentar em casa, uma linha chega:

docker run -p 7576:7576 --name dcm --rm ghcr.io/ajnart/dcm

E para o deixar permanente:

services:   dcm:
    image: ghcr.io/ajnart/dcm
    container_name: dcm
    ports:
      - "7576:7576"
    restart: unless-stopped

Um detalhe que me fez preferir a versão local: o próprio README avisa que a versão online recolhe analytics de utilização e a self-hosted não. Não é um drama, estamos a falar de uma página que gera YAML. Mas se já tens o homelab montado, não há grande razão para mandar para fora a lista do que lá corre.

O fluxo de trabalho é simples. Escolhes contentores numa lista organizada por categorias (servidores de media, automação de downloads, bases de dados, monitorização, dashboards, segurança, armazenamento), ou recorres à Template Gallery, que adiciona de uma vez stacks completas, como um servidor de media com Jellyfin e as aplicações *arr à volta, ou um ambiente de desenvolvimento com base de dados e servidor web. Os templates combinam-se, por isso podes juntar o de media com o de monitorização e continuar a escolher contentores avulsos por cima. Depois ajustas as definições globais, copias ou descarregas os dois ficheiros, guardas na mesma pasta e fazes docker compose up -d.

O resultado também se cola diretamente nas Stacks do Portainer, mas o README faz bem em sublinhar: o Portainer nem sempre lê o .env por si, e as variáveis têm de ir à mão ou por upload. Quem já viu um contentor arrancar com /config montado em /jellyfin, na raiz do disco, porque a variável veio vazia, sabe do que estou a falar.

Há duas funcionalidades menos óbvias. A primeira é a deteção de conflitos de portas. Se juntas na mesma stack dois serviços que querem a 80 e a 443 (um Nginx e um Pi-hole, o exemplo clássico), o DCM dá por isso e reatribui as portas do segundo. Parece pouco, mas é precisamente o tipo de erro que só se descobre quando o segundo contentor se recusa a arrancar.

A segunda é mais recente e diz bastante sobre 2026. A versão alojada no Cloudflare expõe três endpoints MCP sem estado, que permitem a um assistente de IA listar as ferramentas do catálogo, listar os templates e gerar o compose e o .env. Atenção que a imagem Docker não inclui esses endpoints, porque serve apenas a parte estática do site; quem quiser o MCP em casa tem de montar um Worker à parte. Volto a isto mais à frente, porque acho que é a parte mais interessante do projeto.

As sete variáveis

Clonei o repositório no início do mês para ver como o catálogo está construído por dentro. As definições vivem em ficheiros TypeScript na pasta tools, agrupadas por categoria, e cada aplicação tem o seu bloco de compose embutido como texto. Contei 121.

Todas, sem exceção, usam duas variáveis: ${CONTAINER_PREFIX} no nome do contentor e ${RESTART_POLICY} na política de reinício. Quase todas usam ${CONFIG_PATH} (113) e ${TZ} (103). Uma boa parte usa ${DATA_PATH} (73), ${PUID} e ${PGID} (50 cada) e ${UMASK} (27). As que não usam PUID e PGID são, na maioria, imagens oficiais que não suportam essa convenção, como bases de dados que têm o seu próprio utilizador interno.

A definição do Jellyfin mostra bem o padrão:

services:
  jellyfin:
    image: ghcr.io/hotio/jellyfin:latest
    container_name: ${CONTAINER_PREFIX}jellyfin
    environment:
      - PUID=${PUID}
      - PGID=${PGID}
      - TZ=${TZ}
      - UMASK=${UMASK}
    volumes:
      - ${CONFIG_PATH}/jellyfin:/config
      - ${DATA_PATH}/media:/data/media
    ports:
      - 8096:8096
    restart: ${RESTART_POLICY}

E o .env que o acompanha fica algo como isto:

CONTAINER_PREFIX=casa-
RESTART_POLICY=unless-stopped
CONFIG_PATH=/srv/appdata
DATA_PATH=/srv/data
PUID=1000
PGID=1000
TZ=Europe/Lisbon
UMASK=002

Parece trivial. Não é.

Standard Operating Environment: o nome técnico para isto

Para quem não vem do mundo empresarial, SOE é a sigla de Standard Operating Environment. A Red Hat define-o como um sistema operativo concreto, mais um conjunto de software, que o departamento de IT decide tratar como build padrão. Na prática, é a resposta organizada a uma pergunta muito simples: como é que garanto que os meus quatrocentos servidores são parecidos o suficiente para os conseguir gerir com uma equipa de cinco pessoas?

Na família Red Hat, isto fazia-se historicamente com ficheiros kickstart, que descreviam a instalação de uma máquina do princípio ao fim. Mais tarde veio o Satellite, que organiza os hosts em host groups, cada um com os seus parâmetros, repositórios aprovados e roles de configuração. A ideia é que um servidor novo nasça já com tudo o padrão define: particionamento, utilizadores, agentes de monitorização, configuração de NTP, políticas de SSH, fontes de pacotes. No mundo Windows, o equivalente foi durante anos a “golden image” acompanhada de GPO. Na cloud, são as imagens de máquina construídas com Packer. Hoje, muitas vezes, é simplesmente um repositório de Ansible com roles bem definidas e um pipeline que as aplica.

Os fornecedores vendem o SOE com os benefícios do costume: menos tempo de deployment, menos erros, menos custo de suporte, menos shadow IT (equipas a montar sistemas à margem do processo). É tudo verdade. Mas quem já desenhou ou manteve um SOE sabe que esses benefícios vêm de meia dúzia de princípios que convém dizer em voz alta, porque são exatamente os mesmos que estão escondidos no DCM.

Onde o DCM pára: o dia zero sem o dia dois

Em operações costuma falar-se de “dia zero” e “dia dois”. O dia zero é o momento em que algo nasce. O dia dois é tudo o que vem depois: atualizações, alterações, correções, auditorias, a lenta deriva entre o que devia estar a correr e o que está realmente a correr.

O DCM é excelente no dia zero. Escolhes, configuras, geras, arrancas. A partir daí, a ferramenta sai de cena. Não sabe o que fizeste com o ficheiro, não sabe se o alteraste, não te avisa quando a definição de origem muda no catálogo e não tem como te dizer que o teu servidor já não se parece com o template. Está no âmbito do projeto, e é razoável que esteja. Mas se tratares o DCM como o teu SOE inteiro, vais ter problemas, e o próprio catálogo dá dois exemplos muito concretos.

O primeiro são as tags. Das 121 definições que contei, 106 usam a tag latest e outras 5 não indicam tag nenhuma, o que na prática vai dar ao mesmo. São 111 serviços em 121 sem versão fixa. Percebo a escolha: para uma ferramenta que quer dar a quem está a começar uma stack que funciona hoje, latest é o caminho com menos atrito, e manter 121 versões fixas atualizadas seria um trabalho a tempo inteiro para um projeto comunitário. Mas num SOE, uma build sem versão é uma build que não consegues reproduzir. Já vi acontecer em casa de amigos, e a base de dados do Immich não costuma sair bem da experiência.

O segundo é o Watchtower. A definição do catálogo aponta ainda para containrrr/watchtower, um projeto que foi arquivado a 17 de dezembro de 2025. A última versão publicada, a 1.7.1, é de 2023 e já não recebe correções de segurança. A imagem continua a arrancar e a fazer o seu trabalho, por isso ninguém dá por nada. Existe um fork mantido, o nickfedor/watchtower, que funciona como substituto direto e que vários utilizadores já confirmaram ser só trocar o nome da imagem. Quem gerou uma stack com o DCM e incluiu o Watchtower tem, sem saber, um contentor com acesso ao socket do Docker (ou seja, root no host para todos os efeitos) assente em código que ninguém mantém.

Não escrevo isto para apontar o dedo ao autor. É um projeto da comunidade, e o caminho certo é abrir um issue ou um pull request; o processo de contribuição até está parcialmente automatizado a partir de um formulário de submissão no GitHub. Escrevo-o porque é exatamente o que acontece numa empresa quando a golden image de 2023 continua a ser usada em 2026 porque “funciona”. O problema nunca é a imagem. É a ausência de alguém cuja função seja perguntar se ela ainda devia ser usada.

Há um terceiro pormenor, menor, que qualquer pessoa que tenha passado por uma revisão de compliance vai reconhecer. O README diz que o projeto está sob licença MIT, mas o ficheiro LICENSE do repositório é a AGPL-3.0, e é essa que o GitHub mostra na página do projeto. Para uso pessoal é irrelevante. Para quem pense em embutir o DCM num portal interno de uma empresa, convém confirmar com o autor qual vale, porque as obrigações são bastante diferentes. Num SOE corporativo, é este género de inconsistência que deixa um pedido de aprovação de software parado três semanas na secretária de alguém do jurídico.

Nada disto torna o DCM pior do que as alternativas. Só deixa claro o que ele é: um gerador. O que gera é um bom ponto de partida e um mau ponto de chegada.

As alternativas, e a pergunta a que cada uma responde

“Que compose escrevo para esta aplicação?”

É a pergunta a que o DCM responde, e não está sozinho.

O Composerize é o mais antigo e o mais focado. Colas um comando docker run e ele devolve o equivalente em compose. Não tem catálogo nem convenções, mas resolve muito bem o caso, ainda frequente, em que a documentação de uma aplicação só traz o comando de uma linha.

As app templates do Portainer fazem o mesmo papel dentro do próprio Portainer. Há listas mantidas pela comunidade, e a de Alicia Sykes, em portainer-templates.as93.net, agrega centenas de templates de várias fontes (incluindo, curiosamente, o próprio DCM). A diferença filosófica é que estes templates foram pensados para ser lançados a partir do Portainer, e a configuração resultante fica a viver na base de dados dele, não num ficheiro teu.

E depois há a opção mais óbvia, de que muita gente se esquece: a documentação da linuxserver.io. Cada imagem tem um exemplo de compose com as mesmas convenções que o DCM usa, porque, como já vimos, foi daí que o DCM as herdou. Se só precisas de uma aplicação, ir à fonte é muitas vezes mais rápido.

“Quero uma loja de aplicações e não quero pensar em YAML”

Aqui entram o CasaOS (e o ZimaOS, da mesma casa), o Umbrel e o Runtipi. Todos dão uma interface onde se instala aplicações com um clique, com volumes, portas e redes decididos pela plataforma. Para quem quer um servidor em casa e não tenciona abrir um terminal, são escolhas legítimas, e o Umbrel em particular tem uma experiência muito polida.

Do ponto de vista de SOE, ficam no extremo oposto do DCM. O padrão existe, e até é mais rígido, só que não é teu: pertence à plataforma. Ganhas consistência e perdes portabilidade. Se um dia quiseres sair, ou se a plataforma decidir mudar a forma como organiza as aplicações, ficas dependente das ferramentas de migração que ela te der. É a mesma conversa que se tem nas empresas sobre appliances fechados: funcionam muito bem até ao dia em que precisas que façam uma coisa que o fabricante não previu.

“Quero gerir as stacks que já tenho”

É a camada do dia dois, e é onde a escolha tem mais consequências a prazo.

O Portainer continua a ser a consola mais completa e madura, com gestão de utilizadores, vários ambientes, Swarm e Kubernetes. Mas por omissão guarda as stacks internamente, e várias funcionalidades de controlo de acessos e GitOps mais avançadas estão reservadas à edição Business.

O Dockge, do Louis Lam (o mesmo do Uptime Kuma), fez a escolha contrária: guarda cada stack como um ficheiro compose normal, numa pasta do disco. Traz também um conversor de docker run para compose. É leve, simples e assume os seus limites: não tem RBAC, nem OIDC, nem API. Uma nota de prudência: a versão mais recente data de março de 2025, o que, para uma ferramenta deste tipo, começa a ser tempo.

O Komodo é o mais ambicioso do grupo. Faz deploy de stacks a partir de repositórios Git para várias máquinas, com um agente em cada servidor e uma base de dados central (MongoDB, ou FerretDB sobre PostgreSQL). Foi claramente desenhado para frotas, e de tudo o que existe neste espaço é o que mais se parece com um Satellite para contentores. O preço é a complexidade: para uma máquina só, é provavelmente demasiado.

Entre os dois extremos aparecem nomes mais recentes, como o Arcane e o Dockhand, que seguem a escola do Dockge (ficheiros compose no disco como fonte de verdade) com mais funcionalidades à volta. Ainda são jovens. Eu esperaria mais uns meses antes de lhes confiar a gestão da produção, mesmo que essa produção seja a sala de estar.

“Quero só fazer deploy, como num PaaS”

O Coolify e o Dokploy tentam ser um Heroku em casa: ligas um repositório, eles fazem build e deploy, tratam do reverse proxy e dos certificados. São ótimos para quem desenvolve as suas próprias aplicações e quer um fluxo de push para produção. Para correr um Jellyfin e um Paperless, são uma camada a mais entre ti e o que realmente está a correr.

“Quero que o padrão seja código”

E finalmente a família que, na minha opinião, é a resposta a sério para quem leva o homelab a peito: o próprio Docker Compose bem usado, o Ansible com templates Jinja2 para o host, e o Renovate para manter as versões em dia. Não têm interface bonita. Têm uma coisa melhor, que é o diff.

Como transformo isto num SOE de casa

O que faço é usar o DCM como catálogo e ponto de partida, e depois aproveitar o contrato e reescrever o ficheiro. O ficheiro gerado é descartável. O contrato não. Na prática, isso traduz-se em quatro peças e num host que as acompanha.

1. Um repositório com uma estrutura que não muda

homelab/
├── .env                  # as variáveis do padrão
├── compose.yaml          # só includes
├── base/
│   └── defaults.yaml     # o que é comum a todos os serviços
└── stacks/
    ├── media/compose.yaml  
    ├── monitoring/compose.yaml  
    └── docs/compose.yaml  

O .env na raiz tem exatamente as variáveis que o DCM usa, mais as poucas que fui acrescentando ao padrão ao longo do tempo. As palavras-passe não vão lá: ficam num ficheiro à parte, fora do Git, ou num gestor de segredos. Parece óbvio, mas o número de repositórios públicos com palavras-passe de PostgreSQL num .env esquecido diz-me que não é.

2. Defaults partilhados

O Compose tem duas formas nativas de dizer “todos os meus serviços têm isto”. Dentro de um mesmo ficheiro, usam-se campos de extensão começados por x- e âncoras de YAML:

x-base: &base
  restart: ${RESTART_POLICY:-unless-stopped}
  security_opt:
    - no-new-privileges:true
  logging:
    driver: json-file
    options:
      max-size: "10m"
      max-file: "3"

x-env: &env
  PUID: ${PUID}
  PGID: ${PGID}
  TZ: ${TZ}
  UMASK: ${UMASK:-002}

services:
  jellyfin:
    <<: *base
    image: ghcr.io/hotio/jellyfin:release-X.Y.Z
    container_name: ${CONTAINER_PREFIX}jellyfin
    environment:
      <<: *env
    volumes:
      - ${CONFIG_PATH}/jellyfin:/config
      - ${DATA_PATH}/media:/data/media

Um aviso que me custou uma hora a perceber da primeira vez: o merge com <<: é superficial. Se um serviço definir o seu próprio bloco environment sem fazer lá dentro o merge do &env, perde as variáveis comuns sem dar erro nenhum. Por isso separo o ambiente numa âncora própria, e faço o merge explicitamente em cada serviço.

As âncoras de YAML, no entanto, não atravessam ficheiros. Para partilhar defaults entre stacks diferentes, a ferramenta certa é o extends, que vai buscar um serviço-base a outro ficheiro e, ao contrário da âncora, junta as variáveis de ambiente em vez de as substituir:

# stacks/media/compose.yaml  
services:
  jellyfin:
    extends:
      file: ../../base/defaults.yaml
      service: base

Repara em duas coisas que acrescentei ao padrão e que o DCM não tem. O limite de logs, porque um contentor com logs JSON sem rotação é das formas mais clássicas de encher um disco em silêncio, normalmente o do sistema. E o no-new-privileges, que impede que um processo dentro do contentor ganhe privilégios através de binários setuid. São duas linhas. Num SOE de empresa estariam no documento de hardening, revisto uma vez por ano. Em casa, estão na âncora, e aplicam-se a tudo o que lá entra.

3. Includes em vez de um ficheiro gigante

Desde a versão 2.20 que o Compose suporta a diretiva include, que permite montar um projeto a partir de vários ficheiros:

services:
  include:
    - stacks/media/compose.yaml
    - stacks/monitoring/compose.yaml
    - stacks/docs/compose.yaml

Cada stack fica autónoma (consegues trabalhar nela sozinha, com o seu próprio docker compose up) e o projeto como um todo continua a ser um só. É o equivalente doméstico aos host groups do Satellite: o media é um papel, a monitorização é outro, e uma máquina nova recebe os papéis de que precisa. Um erro comum, que já cometi: assumir que os caminhos relativos dentro de um ficheiro incluído se resolvem a partir da raiz do projeto. Não se resolvem. São relativos à pasta do próprio ficheiro, e é por isso que o extends acima sobe dois níveis.

4. Versões fixas, e alguém que as atualize

Tags fixas resolvem a reprodutibilidade e criam o problema contrário: ninguém as atualiza, e daqui a um ano estás a correr versões com falhas conhecidas. A solução que uso é o Renovate, que lê os ficheiros compose do repositório, deteta imagens com versões novas e abre pull requests no Forgejo. Lês o changelog, fazes merge, aplicas. Se correr mal, git revert e voltas atrás.

Isto substitui o Watchtower com vantagem. Em vez de atualizações cegas às quatro da manhã, ficas com um histórico de cada mudança, quem a aprovou e porquê. Quem não quiser ir tão longe pode usar o fork mantido do Watchtower em modo de apenas monitorização (WATCHTOWER_MONITOR_ONLY=true), que avisa quando há versões novas sem tocar em nada. É a diferença, para usar o vocabulário de empresa, entre patch management e patch roulette.

E o host?

O SOE não acaba nos contentores. O host que os corre também tem de seguir o padrão: o utilizador com o UID que puseste no PUID, as pastas de CONFIG_PATH e DATA_PATH criadas com o dono e as permissões certas, o Docker instalado sempre da mesma forma, o fuso horário, o NTP, as chaves SSH, as regras de firewall. É aqui que um playbook de Ansible devolve, com juros, o tempo que custou a escrever.

Se o servidor morrer, a sequência de recuperação fica assim: instalar o sistema base, correr o playbook, clonar o repositório, restaurar o CONFIG_PATH a partir do backup, docker compose up -d. Cinco passos, todos documentados pelo próprio código. E a política de backup simplifica-se: tudo o que importa está debaixo de CONFIG_PATH e DATA_PATH, com a ressalva habitual de que as bases de dados devem ser copiadas através de um dump e não a quente, ficheiro a ficheiro, a meio de uma escrita.

Escrevi sobre isto no post dos serviços essenciais do homelab e no post sobre a migração do CERN para Debian, e a conclusão repete-se: a capacidade de reconstruir sem drama vale mais do que a escolha de qualquer ferramenta em particular. O SOE é o que torna essa reconstrução possível.

Onde o DCM continua a ter lugar

Depois disto tudo, pode parecer que estou a desaconselhar o DCM. Pelo contrário. Uso-o, e acho que tem pelo menos três lugares legítimos.

O primeiro é para quem está a começar. Um compose gerado pelo DCM é melhor do que a média dos que se encontram copiados de fóruns: usa convenções coerentes, separa configuração de dados, evita conflitos de portas e não corre tudo como root. Quem começa por aqui aprende o padrão antes de aprender as exceções, e isso é exatamente a ordem certa. A minha única recomendação é que, ao fim da primeira semana, troques as tags latest por versões fixas e metas os ficheiros num repositório.

O segundo é como catálogo de descoberta. Cada entrada tem uma descrição, o número de estrelas no GitHub e uma configuração que funciona. Para ver rapidamente que alternativas existem a uma aplicação que já usas, ou o que costuma andar à volta de um Jellyfin, é mais agradável do que percorrer a lista awesome-selfhosted.

O terceiro é como referência para desenhar o teu próprio padrão. Se nunca pensaste nas variáveis que todos os teus serviços deviam partilhar, abrir o DCM e ver as sete que ele escolheu é um ótimo exercício. Concordas com umas, discordas de outras (eu, por exemplo, separo os dados de media dos restantes dados, porque vivem em pools diferentes), acrescentas as tuas. No fim tens um SOE de casa que é teu e que consegues explicar a alguém em dois minutos.

E há um quarto lugar, mais especulativo: os endpoints MCP. Com um assistente de IA ligado a um catálogo curado, pedir “dá-me uma stack com Immich e Paperless” passa a devolver algo que segue convenções revistas por pessoas, em vez de YAML improvisado a partir do que o modelo se lembra da documentação. Se isto amadurecer, e se o catálogo for mantido, é das utilizações mais sensatas de IA em infraestrutura que vi nos últimos tempos. O modelo escolhe e preenche. O padrão vem de um repositório com histórico, revisão e dono. É, aliás, o único modelo de automação com IA em infraestrutura em que eu confiaria a sério, em casa ou no trabalho: a IA opera dentro do SOE, nunca por cima dele.

O padrão é teu, o gerador é descartável

O nome do post vem do mote do DCM, “template everything”, e tem uma armadilha. Fazer templates de tudo é fácil. Qualquer pessoa com uma tarde livre e um editor consegue gerar 121 ficheiros. O que é difícil, e o que separa um SOE de uma pasta de exemplos, é decidir o que entra no template, manter essa decisão ao longo do tempo e saber quando alguém se afastou dela.

O DCM acerta na primeira parte, e acerta bem: escolheu poucas variáveis, as certas, e herdou-as de um padrão que o ecossistema já tinha validado. A segunda e a terceira partes não são trabalho dele. São tuas, e resolvem-se com ferramentas aborrecidas: um repositório Git, versões fixas, um bot que abre pull requests e um playbook que deixa o host igual ao do dia anterior.

Se tiveres uma hora este fim de semana, faz este exercício. Abre o DCM, escolhe os serviços que já tens a correr, e compara o compose que ele gera com o que tens no servidor. Cada diferença é uma pergunta: é uma exceção que faz sentido, ou é deriva? As que forem deriva, corriges. As que forem exceções, documentas. Quando acabares, tens o teu primeiro SOE, e provavelmente uma ou duas surpresas sobre o que andava a correr como root ou administrator (esta lenga lenga também se aplica a universos de janelas)

Até ao próximo post

Abraço!
Nuno

Documentação:

DCM

Standard Operating Environment

Watchtower

Alternativas

Compose como código

Nota: Este post foi feito com o auxílio de um LLM privado.