Guides
Fundamentos ▾
Versionamento ▾
Deploy ▾

Arquitetura de Servidor

Setup completo do zero — padrão de mercado com segurança correta

Stack

CamadaTecnologia
Reverse proxy + TLSNginx + Let's Encrypt (Certbot)
ContainersDocker + Docker Compose
CI/CDGitHub Actions
RegistryDocker Hub
Secrets.env por projeto — nunca commitado
SegurançaUFW + 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
SecretValor
DOCKERHUB_USERusuário Docker Hub
DOCKERHUB_TOKENtoken de acesso (não senha)
SERVER_HOSTIP do servidor
SERVER_SSH_KEYchave 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