Deploy de app Node.js na VPS com Docker

    Deploy de aplicação Node.js na VPS com Docker garante que o ambiente de produção é idêntico ao de desenvolvimento — sem surpresas com versões de pacotes ou configuração do SO. Com Nginx como proxy reverso e HTTPS via Let's Encrypt, a aplicação fica acessível pelo domínio em menos de 10 minutos.

    Dockerfile para Node.js em produção

    O Dockerfile de produção usa multi-stage build para separar o ambiente de build do de execução — imagem final menor e sem dependências de desenvolvimento:

    dockerfile
    # Dockerfile
    # Stage 1: instalação de dependências
    FROM node:22-alpine AS deps
    WORKDIR /app
    COPY package*.json ./
    RUN npm ci --only=production
    
    # Stage 2: build (para TypeScript ou bundlers)
    FROM node:22-alpine AS build
    WORKDIR /app
    COPY package*.json ./
    RUN npm ci
    COPY . .
    RUN npm run build
    
    # Stage 3: imagem de execução (mínima)
    FROM node:22-alpine AS runner
    WORKDIR /app
    ENV NODE_ENV=production
    
    # Usuário não-root para segurança
    RUN addgroup -S appgroup && adduser -S appuser -G appgroup
    COPY --from=deps --chown=appuser:appgroup /app/node_modules ./node_modules
    COPY --from=build --chown=appuser:appgroup /app/dist ./dist
    COPY --from=build --chown=appuser:appgroup /app/package.json .
    
    USER appuser
    EXPOSE 3000
    CMD ["node", "dist/index.js"]
    Dica
    Usar usuário não-root (appuser) é obrigatório em produção — processos como root dentro do container têm mais superfície de ataque. NODE_ENV=production ativa otimizações do Node.js e desabilita mensagens de debug.

    docker-compose.yml com Nginx e PostgreSQL

    Stack completo para uma aplicação Node.js com banco de dados e proxy reverso:

    yaml
    services:
      app:
        build: .
        restart: always
        environment:
          NODE_ENV: production
          DATABASE_URL: postgresql://${DB_USER}:${DB_PASSWORD}@postgres:5432/${DB_NAME}
          PORT: 3000
        depends_on:
          postgres:
            condition: service_healthy
        healthcheck:
          test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
          interval: 30s
          timeout: 10s
          retries: 3
        networks:
          - app_network
    
      postgres:
        image: postgres:16-alpine
        restart: always
        environment:
          POSTGRES_USER: ${DB_USER}
          POSTGRES_PASSWORD: ${DB_PASSWORD}
          POSTGRES_DB: ${DB_NAME}
        volumes:
          - pg_data:/var/lib/postgresql/data
        healthcheck:
          test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
          interval: 10s
          retries: 5
        networks:
          - app_network
    
      nginx:
        image: nginx:alpine
        restart: always
        ports:
          - "80:80"
          - "443:443"
        volumes:
          - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
          - certbot_conf:/etc/letsencrypt
          - certbot_www:/var/www/certbot
        networks:
          - app_network
    
    volumes:
      pg_data:
      certbot_conf:
      certbot_www:
    
    networks:
      app_network:

    Endpoint /health para healthcheck

    O healthcheck do Docker precisa de um endpoint que responda 200 quando a aplicação está pronta para receber tráfego:

    typescript
    // src/routes/health.ts (ou .js)
    import { Router } from 'express'
    import { db } from '../database'
    
    const router = Router()
    
    router.get('/health', async (req, res) => {
      try {
        // Verificar conexão com banco (opcional mas recomendado)
        await db.query('SELECT 1')
        res.json({ status: 'ok', uptime: process.uptime() })
      } catch (err) {
        res.status(503).json({ status: 'error', message: 'Database unreachable' })
      }
    })
    
    export default router
    Dica
    O healthcheck com verificação de banco expõe dois problemas de uma vez: a aplicação em si e a conectividade com o banco. Um 503 no /health faz o Docker marcar o container como unhealthy e reiniciá-lo (com restart: always).

    Pipeline de deploy via SSH

    Automatize o deploy com um script que envia o código e reinicia os containers:

    bash
    #!/bin/bash
    # deploy.sh — execute no seu computador local
    
    VPS_USER=deploy
    VPS_HOST=IP_DA_VPS
    APP_DIR=/opt/minha-app
    
    # 1. Sincronizar arquivos (exceto node_modules e .env)
    rsync -avz --exclude='node_modules' --exclude='.env' --exclude='.git' \
      ./ $VPS_USER@$VPS_HOST:$APP_DIR/
    
    # 2. Na VPS: rebuild e restart
    ssh $VPS_USER@$VPS_HOST "
      cd $APP_DIR
      docker compose build app
      docker compose up -d --no-deps app
      docker compose ps
    "
    
    echo "Deploy concluído!"
    Dica
    --no-deps reinicia apenas o container app sem reiniciar nginx e postgres — útil para deploys sem downtime quando só o código da aplicação mudou.

    Nginx para proxy reverso da API Node.js

    Configuração Nginx com keep-alive e timeout adequados para APIs Node.js:

    nginx
    # nginx.conf
    upstream nodejs_app {
        server app:3000;
        keepalive 64;
    }
    
    server {
        listen 80;
        server_name api.seudominio.com.br;
        return 301 https://$host$request_uri;
    }
    
    server {
        listen 443 ssl;
        http2 on;
        server_name api.seudominio.com.br;
    
        ssl_certificate /etc/letsencrypt/live/api.seudominio.com.br/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/api.seudominio.com.br/privkey.pem;
    
        location / {
            proxy_pass http://nodejs_app;
            proxy_http_version 1.1;
            proxy_set_header Connection "";
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_read_timeout 60s;
            proxy_connect_timeout 10s;
        }
    }

    $ runstack deploy --plan starter

    Não quer configurar manualmente?

    Não quer configurar manualmente? Implante o VPS em menos de 3 minutos com a Runstack. Infraestrutura da OPEN DATACENTER, com servidores no Brasil.

    Perguntas frequentes