Arquitetura de Servidor
Setup completo do zero — padrão de mercado com segurança correta
Stack
| Camada | Tecnologia |
|---|---|
| Reverse proxy + TLS | Nginx + Let's Encrypt (Certbot) |
| Containers | Docker + Docker Compose |
| CI/CD | GitHub Actions |
| Registry | Docker Hub |
| Secrets | .env por projeto — nunca commitado |
| Segurança | UFW + Fail2ban |
Estrutura de Diretórios
/
├── home/
│ ├── mysql/ # infra — banco compartilhado
│ │ ├── docker-compose.yml
│ │ ├── .env ← chmod 600, nunca commitado
│ │ └── mysql_data/
│ ├── mongodb/
│ │ ├── docker-compose.yml
│ │ ├── .env
│ │ └── mongo_data/
│ ├── projeto-a.api/ # app dinâmica
│ │ ├── docker-compose.yml
│ │ ├── .env
│ │ └── .github/workflows/deploy.yml
│ └── projeto-b.api/
│
├── var/www/
│ ├── dominio.com.br/ # frontend estático
│ └── outro.dominio.dev/
│
└── etc/nginx/sites-enabled/
├── api-projeto-a.conf # um arquivo por domínio
└── dominio.com.br.conf
1
Usuários
# conectar como root
ssh root@<IP>
# usuário principal
adduser alfredo
usermod -aG sudo alfredo
usermod -aG docker alfredo
# usuário de deploy — sem sudo, usado pelo CI/CD
adduser deploy --disabled-password
usermod -aG docker deploy
# copiar chave SSH
rsync --archive --chown=alfredo:alfredo ~/.ssh /home/alfredo
mkdir -p /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
# desabilitar login por senha
sed -i 's/PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
systemctl restart sshd
2
Pacotes Base
apt update && apt upgrade -y
apt install -y \
curl wget git unzip \
ufw fail2ban \
nginx certbot python3-certbot-nginx \
htop ncdu iotop
# Docker
curl -fsSL https://get.docker.com | sh
3
Firewall (UFW)
ufw default deny incoming
ufw default allow outgoing
ufw allow from <IP-fixo> to any port 22 # SSH restrito por IP
ufw allow 80
ufw allow 443
ufw enable
ufw status
4
Fail2ban
cat > /etc/fail2ban/jail.local << 'EOF'
[sshd]
enabled = true
port = ssh
maxretry = 5
bantime = 3600
findtime = 600
[nginx-http-auth]
enabled = true
[nginx-limit-req]
enabled = true
EOF
systemctl enable fail2ban
systemctl restart fail2ban
5
Redes Docker
Criar uma vez. Cada rede isola um tipo de banco — apenas os serviços que precisam se conectam a ela.
docker network create mysql-network
docker network create mongodb-network
mysql-network
- mysql-server
- APIs que usam MySQL
mongodb-network
- mongodb
- APIs que usam MongoDB
redis-network
- redis
- APIs com cache/sessão
6
Bancos de Dados
Sempre bind
127.0.0.1:3306:3306 — nunca 3306:3306. Banco exposto em 0.0.0.0 é vulnerabilidade crítica.MySQL
mkdir -p /home/mysql
# /home/mysql/docker-compose.yml
name: mysql
services:
mysql-server:
image: mysql:8
container_name: mysql-server
env_file:
- .env
ports:
- "127.0.0.1:3306:3306" # bind local obrigatório
volumes:
- ./mysql_data:/var/lib/mysql
restart: unless-stopped
networks:
- mysql-network
networks:
mysql-network:
external: true
# /home/mysql/.env
MYSQL_ROOT_PASSWORD=senha-root-forte
MYSQL_USER=dbuser
MYSQL_PASSWORD=senha-usuario-forte
chmod 600 /home/mysql/.env
cd /home/mysql && docker compose up -d
MongoDB
mkdir -p /home/mongodb
# /home/mongodb/docker-compose.yml
name: mongodb
services:
mongodb:
image: mongo:7
container_name: mongodb
env_file:
- .env
ports:
- "127.0.0.1:27017:27017" # bind local obrigatório
volumes:
- ./mongo_data:/data/db
restart: unless-stopped
networks:
- mongodb-network
networks:
mongodb-network:
external: true
# /home/mongodb/.env
MONGO_INITDB_ROOT_USERNAME=mongouser
MONGO_INITDB_ROOT_PASSWORD=senha-mongo-forte
chmod 600 /home/mongodb/.env
cd /home/mongodb && docker compose up -d
7
Template — API (Docker Compose)
Secrets nunca inline no yaml. Usar
env_file: .env. O arquivo .env fica no servidor, nunca no git.# /home/projeto.api/docker-compose.yml
name: projeto.api
services:
api:
image: usuario/projeto.api:latest
container_name: projeto.api
env_file:
- .env
ports:
- "4001:4001"
restart: unless-stopped
networks:
- mysql-network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:4001/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 15s
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.1'
memory: 128M
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
networks:
mysql-network:
external: true
# /home/projeto.api/.env
PORT=4001
DATABASE_URL=mysql://dbuser:senha@mysql-server:3306/projeto_db
JWT_SECRET=<openssl rand -hex 32>
APP_URL=https://api-projeto.dominio.com.br
chmod 600 /home/projeto.api/.env
# .gitignore do repositório
echo '.env' >> .gitignore
echo '.env.*' >> .gitignore
8
Nginx — Reverse Proxy (API)
# /etc/nginx/sites-available/api-projeto.dominio.com.br
limit_req_zone $binary_remote_addr zone=api_projeto:10m rate=30r/m;
server {
listen 80;
server_name api-projeto.dominio.com.br;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name api-projeto.dominio.com.br;
ssl_certificate /etc/letsencrypt/live/api-projeto.dominio.com.br/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api-projeto.dominio.com.br/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
limit_req zone=api_projeto burst=20 nodelay;
location / {
proxy_pass http://localhost:4001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400;
}
}
ln -s /etc/nginx/sites-available/api-projeto.dominio.com.br /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
9
Nginx — Frontend Estático (SPA)
mkdir -p /var/www/dominio.com.br
# /etc/nginx/sites-available/dominio.com.br
server {
listen 80;
server_name dominio.com.br www.dominio.com.br;
return 301 https://dominio.com.br$request_uri;
}
server {
listen 443 ssl;
server_name dominio.com.br;
ssl_certificate /etc/letsencrypt/live/dominio.com.br/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/dominio.com.br/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
root /var/www/dominio.com.br;
index index.html;
location ~* \.(js|css|png|jpg|ico|woff2|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
location / {
try_files $uri $uri/ /index.html;
}
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
add_header Referrer-Policy "strict-origin-when-cross-origin";
}
ln -s /etc/nginx/sites-available/dominio.com.br /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
10
SSL — Certbot
# emitir (nginx já com server_name configurado)
certbot --nginx -d dominio.com.br -d www.dominio.com.br
certbot --nginx -d api-projeto.dominio.com.br
# testar renovação automática
certbot renew --dry-run
11
CI/CD — GitHub Actions (API)
# .github/workflows/deploy.yml
name: CI/CD
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
test:
name: CI
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm test
deploy:
name: CD
needs: test
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Login Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build e push
uses: docker/build-push-action@v5
with:
push: true
platforms: linux/amd64,linux/arm64
tags: |
usuario/projeto.api:${{ github.sha }}
usuario/projeto.api:latest
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: deploy
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
cd /home/projeto.api
sed -i "s|image: .*|image: usuario/projeto.api:${{ github.sha }}|" docker-compose.yml
docker compose pull
docker compose up -d --remove-orphans
docker image prune -f
Chave SSH para o usuário deploy
# gerar par de chaves (local)
ssh-keygen -t ed25519 -C "github-actions" -f ~/.ssh/deploy_key
# adicionar chave pública ao servidor
ssh-copy-id -i ~/.ssh/deploy_key.pub deploy@<IP>
# conteúdo da chave privada → GitHub Secret SERVER_SSH_KEY
cat ~/.ssh/deploy_key
Secrets no GitHub
Settings → Secrets and variables → Actions
| Secret | Valor |
|---|---|
| DOCKERHUB_USER | usuário Docker Hub |
| DOCKERHUB_TOKEN | token de acesso (não senha) |
| SERVER_HOST | IP do servidor |
| SERVER_SSH_KEY | chave privada ED25519 do usuário deploy |
12
CI/CD — GitHub Actions (Frontend Estático)
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci && npm run build
- name: Upload para servidor
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.SERVER_HOST }}
username: deploy
key: ${{ secrets.SERVER_SSH_KEY }}
source: "dist/"
target: "/var/www/dominio.com.br"
strip_components: 1
13
Verificação Final
# containers e status
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
# bancos NÃO devem aparecer em 0.0.0.0
docker ps --format "{{.Names}}: {{.Ports}}" | grep -E '3306|27017'
# nginx ok
nginx -t && systemctl status nginx
# firewall
ufw status
# fail2ban
fail2ban-client status
# certificados
certbot certificates
# testar endpoint de saúde
curl -f https://api-projeto.dominio.com.br/health