Se você acabou de contratar uma VPS (Virtual Private Server) e quer hospedar seus projetos de forma profissional, automatizada e sem complicação, este guia é para você.
Neste post, vamos aprender a configurar uma infraestrutura leve, segura e de baixo custo ideal para desenvolvedores independentes ou pequenas equipes. Usaremos:
Docker Swarm: Para orquestração dos containers (em modo single-node, super leve).
Traefik (v2.11): Como proxy reverso, com SSL automático via Let's Encrypt e roteamento inteligente.
Portainer (CE): Para gerenciamento visual de tudo que roda no servidor.
UFW & Fail2ban: Para garantir que as portas do servidor estejam devidamente trancadas.
🛡 Passo 1: Preparando a VPS (Segurança em Primeiro Lugar)
Antes de rodar qualquer container, precisamos blindar o nosso sistema operacional (usaremos o Ubuntu como base). Uma VPS exposta com configurações padrão é alvo constante de bots de varredura.
1.1 Alterando a Porta SSH Padrão
Por padrão, o SSH escuta na porta 22. Mudar essa porta para um número aleatório (como 1119 ou similar) reduz drasticamente as tentativas automáticas de invasão.
Edite o arquivo
/etc/ssh/sshd_config:
sudo nano /etc/ssh/sshd_config
Encontre a linha
#Port 22e altere para:
Port 1119
(Substitua 1119 por uma porta de sua preferência entre 1024 e 65535).
Reinicie o serviço SSH:
sudo systemctl restart ssh
Não feche sua sessão de terminal atual até testar a nova conexão em uma nova janela para garantir que você não perderá o acesso!
A Pegadinha das Nuvens Públicas (Firewall Externo)
Se a sua VPS está hospedada em provedores como AWS (EC2), Oracle Cloud, Google Cloud ou Azure, eles possuem um firewall externo próprio no painel web (conhecido como Security Groups ou Ingress Rules).
Mudar a porta interna no arquivo SSH e no UFW não funcionará se o painel web da sua hospedagem continuar bloqueando a nova porta (ex: 1578). Você será trancado para fora do servidor se a conexão atual expirar ou cair.
Antes de testar ou fechar o terminal atual, acesse o console web da sua hospedagem e adicione uma regra de entrada (Ingress Rule) liberando o tráfego TCP para a nova porta SSH customizada (ex: 1578), além de garantir que as portas 80 e 443 também estejam liberadas na nuvem.
1.2 Configurando o Firewall (UFW)
No Docker Swarm, a comunicação interna do cluster usa portas específicas. Se você não bloquear essas portas externamente, qualquer pessoa poderá tentar se comunicar com o seu Swarm.
Vamos bloquear todo o tráfego de entrada por padrão e abrir apenas as portas essenciais:
# Bloquear conexões de entrada e permitir de saída
sudo ufw default deny incoming
sudo ufw default allow outgoing
# Liberar sua porta SSH customizada
sudo ufw allow 1578/tcp
# Liberar tráfego HTTP e HTTPS público
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Habilitar o firewall
sudo ufw enable
Dessa forma, portas sensíveis do Swarm (como 2377/tcp, 7946/tcp/udp e 4789/udp) ficam completamente isoladas do mundo externo, protegidas dentro do loop de rede do host.
O Perigo Oculto: Docker vs UFW
Por padrão, o Docker manipula as regras de iptables diretamente e faz isso antes das regras do UFW entrarem em ação. Isso significa que o Docker ignora completamente o UFW para qualquer porta exposta na diretiva ports (ex: ports: - "5432:5432").
Se você expor uma porta no seu container/stack, ela estará 100% visível para o mundo, mesmo que você não a tenha liberado no UFW! No Docker Swarm (mesmo em nó único), o roteamento do ingress também pode expor portas publicamente se expostas via ports.
1.3 Protegendo contra Brute Force com Fail2ban
O fail2ban monitora as tentativas de login e bloqueia temporariamente IPs que demonstrem comportamento suspeito.
Instale o pacote:
sudo apt update && sudo apt install fail2ban -y
Crie a configuração local em
/etc/fail2ban/jail.local:
[sshd]
enabled = true
port = 1578
filter = sshd
logpath = /var/log/auth.log
maxretry = 5
findtime = 600
bantime = 3600
Reinicie o Fail2ban:
sudo systemctl restart fail2ban
🐋 Passo 2: Docker & Docker Swarm
Se você ainda não tem o Docker instalado, siga o guia oficial de instalação do Docker.
2.1 Iniciando o Swarm
Com o Docker instalado, transforme a sua VPS em um gerenciador Swarm de nó único com o comando:
docker swarm init --advertise-addr <IP_PUBLICO_DA_SUA_VPS>
2.2 Criando a Rede Overlay Pública
O Traefik precisa de uma rede virtual do tipo overlay para se comunicar de forma segura e privada com os containers de suas aplicações. Vamos criar uma rede chamada public:
docker network create --driver=overlay public
🌐 Passo 3: Apontando o DNS (Pré-requisito Obrigatório)
Antes de criar o arquivo de configuração e subir a nossa stack, precisamos configurar o DNS.
O Traefik foi modelado neste guia para usar o desafio HTTP do Let's Encrypt para gerar os certificados SSL automáticos. Isso significa que, no exato momento em que a stack subir, o Let's Encrypt tentará acessar o seu servidor web através do domínio especificado (ex: traefik.exemplo.com.br e portainer.exemplo.com.br).
Perigo de Rate Limit (Bloqueio) do Let's Encrypt
Se você subir a stack sem antes apontar os domínios para a VPS, o Let's Encrypt tentará validar o domínio, falhará e tentará novamente em loop.
O Let's Encrypt possui um limite rígido de 5 falhas de validação por domínio/host por hora. Se você estourar esse limite (o que é muito fácil ao reiniciar a stack várias vezes tentando "corrigir" o SSL), seu IP/domínio será temporariamente bloqueado pelo Let's Encrypt e você poderá ficar até algumas horas sem conseguir gerar o certificado, vendo erros de SSL inválido ou conexão insegura no navegador.
O que fazer antes de prosseguir:
Vá até o gerenciador do seu domínio (Cloudflare, Registro.br, HostGator, etc.) e crie dois apontamentos do tipo A:
traefik.exemplo.com.brapontando para o<IPPUBLICODA_VPS>portainer.exemplo.com.brapontando para o<IPPUBLICODA_VPS>
Certifique-se de que os apontamentos já se propagaram (você pode verificar rodando ping traefik.exemplo.com.br no seu terminal local) antes de prosseguir para a criação do arquivo.
📦 Passo 4: O arquivo de Stack (docker-compose.yml)
Agora vamos criar o arquivo de configuração que instalará o Traefik e o Portainer ao mesmo tempo.
Crie um arquivo chamado traefik-portainer-stack.yml e adicione o conteúdo abaixo.
(Atenção: substitua exemplo.com.br pelo seu domínio real e seu-email@provedor.com pelo seu e-mail real para a geração dos certificados SSL).
version: '3.8'
services:
traefik:
image: traefik:v2.11
ports:
- "80:80"
- "443:443"
command:
# Ativa o Dashboard do Traefik de forma segura
- "--api.dashboard=true"
- "--api.insecure=false"
# Provedor Docker em modo Swarm
- "--providers.docker=true"
- "--providers.docker.swarmMode=true"
- "--providers.docker.exposedByDefault=false"
# Configuração dos Entrypoints
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
# Redirecionamento global automático de HTTP para HTTPS
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
# Configuração de Certificados SSL Automáticos (Let's Encrypt)
- "--certificatesresolvers.myresolver.acme.email=seu-email@provedor.com"
- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
- "--certificatesresolvers.myresolver.acme.httpchallenge=true"
- "--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- traefik-letsencrypt:/letsencrypt
networks:
- public
deploy:
mode: replicated
replicas: 1
placement:
constraints: [node.role == manager]
labels:
- "traefik.enable=true"
- "traefik.docker.network=public"
# Rota para acessar o painel do Traefik com SSL automático
- "traefik.http.routers.traefik-dashboard.rule=Host(`traefik.exemplo.com.br`)"
- "traefik.http.routers.traefik-dashboard.entrypoints=websecure"
- "traefik.http.routers.traefik-dashboard.tls=true"
- "traefik.http.routers.traefik-dashboard.tls.certresolver=myresolver"
- "traefik.http.routers.traefik-dashboard.service=api@internal"
# Autenticação Básica (BasicAuth) para o Painel do Traefik
- "traefik.http.routers.traefik-dashboard.middlewares=traefik-dashboard-auth"
# Usuário: admin | Senha gerada em hash Bcrypt (Substitua pelo seu próprio hash gerado!)
- "traefik.http.middlewares.traefik-dashboard-auth.basicauth.users=admin:$$2y$$05$$ck9kC76LmcHTJScbPmRUdOrAgDPxqsWLI.llY6DLGbY273hPsl/MW"
portainer:
image: portainer/portainer-ce:latest
command: -H unix:///var/run/docker.sock
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- portainer_data:/data
networks:
- public
deploy:
mode: replicated
replicas: 1
placement:
constraints: [node.role == manager]
labels:
- "traefik.enable=true"
- "traefik.docker.network=public"
# Rota para acessar a interface web do Portainer
- "traefik.http.routers.portainer.rule=Host(`portainer.exemplo.com.br`)"
- "traefik.http.routers.portainer.entrypoints=websecure"
- "traefik.http.routers.portainer.tls=true"
- "traefik.http.routers.portainer.tls.certresolver=myresolver"
# Aponta para a porta interna 9000 do Portainer
- "traefik.http.services.portainer.loadbalancer.server.port=9000"
networks:
public:
name: public
driver: overlay
volumes:
portainer_data:
traefik-letsencrypt:
🔒 Passo 5: Entendendo as Configurações de Segurança
4.1 Segurança do Dashboard do Traefik
Por padrão, muitos tutoriais ativam o dashboard em modo inseguro (--api.insecure=true) exposto diretamente na porta 8080. Evite isso.
No nosso arquivo Compose:
Desabilitamos o modo inseguro (
--api.insecure=false).Criamos uma rota segura que aponta para o serviço interno do Traefik (
api@internal).Adicionamos um middleware de autenticação (
basicauth.users).
Como gerar o hash da senha?
Você pode instalar a ferramenta apache2-utils e executar o comando abaixo para gerar o hash bcrypt seguro:
htpasswd -nB admin
Insira a senha desejada e cole o resultado no campo correspondente no arquivo .yml (lembre-se de dobrar os símbolos de cifrão $ para $$ para evitar conflito com variáveis de ambiente do Docker/Compose).
O Bug do Escapamento de $$ no Swarm / Portainer
Dependendo da versão do Docker CLI que você estiver usando no terminal, ou se você implantar esta stack usando a interface do Portainer (que possui seu próprio interpretador de compose), o comportamento de conversão do $$ pode variar.
O problema: Alguns interpretadores não removem o cifrão duplicado (passando o hash com
$$literal para o Traefik, invalidando o bcrypt). Outros, se você colocar apenas um$, tentam fazer interpolação e corrompem o hash.Como testar: Assim que subir os serviços (Passo 6), tente logar no dashboard do Traefik. Se der erro de autenticação usando a senha correta, o culpado é esse caractere de escape no YAML.
Como diagnosticar e resolver: Inspecione o serviço rodando na VPS com o comando:
docker service inspect proxy_traefik --format '{{json .Spec.Template.Metadata.Labels}}'
Verifique no resultado se o middleware do BasicAuth está com $$ ou $. Se estiver mostrando os $$ duplicados no serviço ativo, altere os $$ de volta para $ simples no arquivo .yml e execute o deploy da stack novamente.
4.2 Isolamento de Serviços Internos (ex: Bancos de Dados)
Uma das grandes vantagens desta arquitetura é o isolamento de rede.
O Traefik e o Portainer precisam estar conectados na rede
publicpara responder à internet.No entanto, serviços internos como bancos de dados (PostgreSQL, Redis, MySQL) nunca devem expor suas portas públicas no arquivo Compose (não use a diretiva
portsneles).Coloque-os em redes internas isoladas (ex:
dbnetwork). O Traefik não precisará conhecê-los diretamente, mantendo-os seguros contra varreduras de portas externas.
Se um serviço interno (como o seu banco de dados) precisar ser acessado de fora do servidor em algum momento, em vez de expor a porta publicamente usando ports, utilize uma conexão segura como um Túnel SSH direcionado, ou configure a rede para expor apenas no endereço local (127.0.0.1:5432:5432), o que impede o acesso externo direto e respeita o isolamento.
4.3 A Exposição do Socket do Docker (docker.sock)
O Docker Socket (/var/run/docker.sock) é o canal de controle do Docker. Montar esse arquivo dentro dos containers do Traefik e do Portainer é necessário para que eles saibam quais containers estão subindo/descendo.
No entanto, isso traz um risco: se algum desses containers sofrer uma invasão por vulnerabilidade de software, o invasor terá acesso total (root) à sua VPS.
Boa prática recomendada: Para ambientes de alta segurança, utilize um proxy do Docker socket (como
tecnativa/docker-socket-proxy). Ele atua como um firewall para o socket do Docker, liberando apenas requisições de leitura (GET) para o Traefik, impedindo que requisições de escrita/deleção sejam feitas por aplicações de borda.
🛠 Passo 6: Implantando e Operando a Infraestrutura
Com tudo configurado, envie sua stack para o Docker Swarm. No diretório onde salvou o arquivo traefik-portainer-stack.yml, execute:
docker stack deploy -c traefik-portainer-stack.yml proxy
Comandos de Operação Rápidos
Verificar se os serviços subiram corretamente:
docker stack services proxy
(Você deverá ver 1/1 réplicas ativas para o Traefik e o Portainer).
Olhar os logs do Traefik em tempo real:
docker service logs -f proxy_traefik
Dica de Resolução de Problemas: A armadilha do acme.json
O Traefik armazena as chaves de segurança e os certificados SSL gerados em um arquivo chamado acme.json (dentro da pasta /letsencrypt). Por questões de segurança, o Traefik exige que este arquivo tenha permissões de acesso estritas (chmod 600), permitindo leitura/escrita apenas ao proprietário.
O problema: Se você mapear um arquivo direto do host (por exemplo,
- ./acme.json:/letsencrypt/acme.json) e o arquivo não existir no host antes de subir a stack, o Docker criará automaticamente uma pasta chamadaacme.jsonno host, quebrando a inicialização. Além disso, se o arquivo existir mas as permissões forem abertas demais (ex:755ou644), o Traefik se recusará a gerar o SSL e registrará o seguinte erro nos logs:
permissions 755 for /letsencrypt/acme.json are too open, please use chmod 600 /letsencrypt/acme.json
Como resolvemos no post: No nosso arquivo
docker-compose.yml(Passo 4), usamos um volume nomeado do Docker (traefik-letsencrypt), o que deixa a criação e o controle das permissões do arquivo a cargo do próprio Traefik, evitando dores de cabeça. Caso você crie o arquivo manualmente no host, certifique-se de ajustar as permissões antes de rodar o deploy:
touch acme.json && chmod 600 acme.json
Acessar os painéis:
Acesse
https://portainer.exemplo.com.brpara criar seu usuário administrador inicial do Portainer.Acesse
https://traefik.exemplo.com.br(insira o usuárioadmine a senha definida no passo anterior) para acompanhar as rotas ativas de seus containers.
Conclusão
Pronto! Você acaba de construir uma infraestrutura extremamente robusta, automatizada e pronta para produção em uma única VPS. A partir de agora, qualquer novo container de site ou API que você subir no Swarm poderá se conectar à rede public e usar labels do Traefik para ganhar um subdomínio e um certificado SSL automático em poucos segundos, sem precisar mexer em arquivos de configuração do Nginx ou reiniciar serviços.