↓Pular para o conteúdo principal
  1. blog/
ARTIGO TÉCNICO

Semaphore self-hosted com PostgreSQL, Traefik e secrets por arquivo: como montei o control plane

Arquitetura de um control plane Semaphore em Docker com banco dedicado, redes separadas, secrets por arquivo, CLIs de cloud e executor não-root.

Quando o Semaphore deixa de ser uma interface para testes e passa a executar automações de produção, eu começo a tratá-lo como infraestrutura de controle.

Isso muda a preocupação.

Não basta o container subir.

Quero saber:

1
2
3
4
5
6
7
8
onde está o banco?
como secrets entram?
qual rede é pública?
qual porta é exposta?
que ferramentas existem no executor?
qual usuário roda as tarefas?
como controlo paralelismo?
como faço backup?

A stack privada que usei como referência responde essas perguntas de forma relativamente simples com Docker Compose.

Dois serviços principais #

O núcleo é:

1
2
3
Semaphore UI/executor
+
PostgreSQL

O banco usa PostgreSQL 16 Alpine e fica apenas na rede interna da stack.

O Semaphore depende do health check do banco antes de iniciar.

Conceitualmente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
services:
  db:
    image: postgres:16-alpine
    healthcheck:
      test: ["CMD-SHELL", "pg_isready ..."]

  semaphore:
    depends_on:
      db:
        condition: service_healthy

Isso não resolve migração de schema ou restore, mas evita iniciar o control plane antes de o banco estar pronto para conexões.

A porta local de debug não precisa ficar aberta na internet #

Na stack real, a UI tem um bind local semelhante a:

1
127.0.0.1:3001 → 3000

Ou seja, a porta auxiliar serve para diagnóstico no host, não como publicação externa.

O acesso externo passa pelo reverse proxy.

Essa diferença é importante:

1
loopback/debug ≠ endpoint público

Rede interna e rede do proxy #

O banco participa somente da rede interna.

O Semaphore participa de:

1
2
3
rede interna
+
rede externa do reverse proxy

A topologia fica:

1
2
3
4
5
6
7
Internet
   ↓
Traefik :443
   ↓
Semaphore :3000
   ↓
PostgreSQL :5432 (somente rede interna)

Banco não precisa de porta publicada no host para atender a aplicação.

Secrets entram por arquivo #

Em vez de colocar senhas diretamente no Compose, a stack usa arquivos montados como Docker secrets/secret files.

Exemplos conceituais:

1
2
3
4
5
/run/secrets/postgres_password
/run/secrets/semaphore_admin_password
/run/secrets/access_key_encryption
/run/secrets/cookie_hash
/run/secrets/cookie_encryption

O Compose aponta variáveis como:

1
2
*_PASSWORD_FILE
*_ENCRYPTION_FILE

Isso separa o template de infraestrutura do valor secreto.

.gitignore também precisa conhecer o que é operacional #

Na fonte privada, ficam fora do Git categorias como:

1
2
3
4
5
6
7
8
9
secrets/
aws/
oci/
ssh/
postgres/
data/
tmp/
backups/
config/

Além de extensões comuns de chaves, dumps e arquivos de ambiente.

Isso é uma boa barreira contra commit acidental.

Mas existe uma ressalva importante:

.gitignore não apaga segredo que já entrou no histórico.

Por isso uma distribuição pública deve nascer em repositório novo e passar por secret scan, em vez de assumir que ignorar arquivos basta.

AWS e OCI entram read-only no filesystem #

Configurações cloud são montadas no executor como read-only.

Conceitualmente:

1
2
3
volumes:
  - ./aws:/home/semaphore/.aws:ro
  - ./oci:/home/semaphore/.oci:ro

Isso impede que uma task comum altere o arquivo do host por acidente através daquele mount.

Ainda é material sensível e precisa de permissões corretas fora do container.

O executor precisa das ferramentas que os playbooks realmente usam #

A imagem privada parte de uma versão fixa do Semaphore e adiciona ferramentas operacionais.

Entre elas:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
bash
curl/wget
git
openssh
sshpass
rsync
jq/yq
AWS CLI
OCI CLI
PostgreSQL/MariaDB/Redis clients
kubectl
Helm
Kustomize

Também instala bibliotecas Python para bancos e Kubernetes.

Isso transforma o container num executor multi-cloud/multi-stack, em vez de depender do host ter cada CLI instalada.

Instalar como root e executar como usuário não-root #

O Dockerfile precisa de root durante a instalação de pacotes.

Depois, a imagem cria/ajusta diretórios e volta para o usuário do Semaphore.

A sequência é conceitualmente:

1
2
3
4
USER root
RUN instalar_dependencias
RUN ajustar_ownership
USER 1001

É melhor do que deixar o executor permanentemente como root só porque o build precisou de privilégios.

Ferramenta demais também aumenta superfície #

Há um trade-off.

Colocar AWS, OCI, Kubernetes, bancos e SSH na mesma imagem facilita automação heterogênea.

Também aumenta:

  • quantidade de pacotes;
  • superfície de vulnerabilidade;
  • tempo de build;
  • necessidade de atualização;
  • poder disponível para um playbook comprometido.

Em um blueprint público, eu documentaria dois perfis:

1
2
executor-base
executor-cloud-full

ou pelo menos deixaria claro que ferramentas extras devem ser removidas quando não forem necessárias.

Paralelismo é guardrail do control plane #

A configuração privada define limite máximo de tarefas paralelas e duração máxima de task.

Esses parâmetros são úteis porque um orquestrador pode amplificar erro.

Se um operador dispara várias automações grandes simultaneamente, o problema pode atingir:

  • CPU/memória do executor;
  • SSH bastion;
  • APIs cloud;
  • Git checkout/cache;
  • rede;
  • próprios alvos.

Limite de plataforma não substitui serial, throttle ou estratégia Ansible, mas cria uma camada global adicional.

Backup do control plane precisa incluir mais que o banco #

A rotina privada faz pg_dump do banco e empacota arquivos necessários para recuperar o ambiente.

Isso inclui material sensível como secrets e configurações cloud.

Por isso o backup é privado e precisa de proteção equivalente às próprias credenciais.

Uma boa política deveria ainda acrescentar, conforme o ambiente:

  • criptografia do backup;
  • cópia fora do host;
  • teste periódico de restore;
  • retenção documentada;
  • controle de acesso.

O script recuperado prova backup e retenção local; não vou afirmar aqui que todos esses controles adicionais já existem.

O reverse proxy também faz parte do estado #

A stack usa Traefik para TLS e roteamento.

Também adiciona headers para impedir cache da interface.

Essa decisão veio de comportamento de sessão/proxy observado durante a implantação.

Num blueprint público, o hostname real deve desaparecer e virar variável/exemplo fictício.

O que eu publicaria num blueprint #

Não copiaria o Compose real.

Criaria uma versão genérica contendo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Semaphore version pinada
PostgreSQL
rede interna
Traefik opcional
secrets por arquivo
.env.example sem segredo
executor mínimo
health checks
loopback debug opcional
backup genérico

E deixaria AWS/OCI/Kubernetes como módulos opcionais ou documentados.

O que a fonte sustenta #

O repositório privado comprova:

  • Semaphore em versão pinada;
  • PostgreSQL 16 Alpine;
  • health check;
  • publicação por Traefik;
  • porta de debug em loopback;
  • rede interna separada;
  • secrets por arquivo;
  • mounts AWS/OCI read-only;
  • executor com CLIs e clients adicionais;
  • retorno para usuário não-root;
  • limites de paralelismo/duração;
  • rotina de backup com pg_dump e retenção local.

Referências específicas de domínio, cliente, contas e operadores foram omitidas.

O principal aprendizado #

Uma interface de automação é parte da infraestrutura que ela controla.

Então eu trato o Semaphore como trataria qualquer control plane:

1
2
3
4
5
6
7
8
segredos separados
rede mínima
banco persistente
executor conhecido
privilégio reduzido
limites operacionais
backup
validação

O playbook pode ser excelente. Se o lugar que executa todos eles for improvisado, o risco só mudou de endereço.

TEM UM CENÁRIO PARECIDO?

Me chama.

Manda o contexto, os sintomas e o que já foi testado. Bora organizar as evidências antes de sair mexendo.

Falar com Castro →
Sem enrolação. Com evidência.