Menu
Szybki wybór
Hosting Domeny VPS SSL Kalkulator Porównania FAQ
Aktywne kody
Wszystkie kody rabatowe

Node.js Express — deployment na VPS w produkcji

Opublikowano: 9 kwietnia 2026 · Kategoria: Node.js / VPS

Express.js to minimalny, szybki framework Node.js — ale samo napisanie aplikacji to połowa drogi. Produkcyjny deployment wymaga: menedżera procesów (PM2), reverse proxy (Nginx), HTTPS (certbot), logowania i graceful shutdown. Oto kompletny przewodnik wdrożenia Express na własnym VPS.

Struktura projektu produkcyjnego

my-app/
├── src/
│   ├── app.js          # Express app (bez listen — eksportuje app)
│   ├── server.js       # Entry point (listen + graceful shutdown)
│   ├── routes/         # Express Routers
│   ├── middleware/     # Auth, rate-limit, error handler
│   └── utils/          # Logger, DB connection
├── .env                # Zmienne lokalne (NIE w git!)
├── .env.production     # Template (bez sekretów, w git)
├── ecosystem.config.js # PM2 config
├── package.json
└── .gitignore          # Zawiera: .env, node_modules/, logs/
// src/app.js — Express app bez listen()
import express from 'express';
import morgan from 'morgan';
import helmet from 'helmet';
import cors from 'cors';
import router from './routes/index.js';
import errorHandler from './middleware/errorHandler.js';

const app = express();

app.use(helmet());                          // Nagłówki bezpieczeństwa
app.use(cors({ origin: process.env.CORS_ORIGIN }));
app.use(express.json({ limit: '10mb' }));
app.use(morgan('combined'));                // Logi HTTP do stdout

app.use('/api', router);

// Health check — monitoring i PM2 health probe
app.get('/health', (req, res) => {
  res.json({
    status: 'ok',
    uptime: process.uptime(),
    timestamp: new Date().toISOString(),
  });
});

app.use(errorHandler);

export default app;

PM2 ecosystem.config.js

PM2 to menedżer procesów Node.js — zapewnia auto-restart, klastrowanie i logi. Plik konfiguracyjny ecosystem.config.js:

// ecosystem.config.js
export default {
  apps: [
    {
      name: 'my-express-app',
      script: './src/server.js',

      // Cluster mode — po jednej instancji na rdzeń CPU
      instances: 'max',   // lub konkretna liczba: 2, 4
      exec_mode: 'cluster',

      // Zmienne środowiskowe per environment
      env: {
        NODE_ENV: 'development',
        PORT: 3000,
      },
      env_production: {
        NODE_ENV: 'production',
        PORT: 3000,
      },

      // Logi
      out_file: '/var/log/my-app/out.log',
      error_file: '/var/log/my-app/error.log',
      merge_logs: true,
      log_date_format: 'YYYY-MM-DD HH:mm:ss',

      // Auto-restart przy przekroczeniu pamięci
      max_memory_restart: '1G',

      // Graceful shutdown
      kill_timeout: 5000,     // Czas na obsługę otwartych połączeń (ms)
      wait_ready: true,       // Czekaj na process.send('ready') przed switchem
      listen_timeout: 10000,  // Maks czas na gotowość

      // Ignoruj zmiany w katalogu node_modules
      watch: false,
      ignore_watch: ['node_modules', 'logs'],
    },
  ],
};
# Uruchomienie i zarządzanie przez PM2
npm install -g pm2

# Start z konfiguracją produkcyjną
pm2 start ecosystem.config.js --env production

# Zero-downtime reload (graceful restart klastra)
pm2 reload my-express-app

# Monitorowanie w czasie rzeczywistym
pm2 monit

# Logi
pm2 logs my-express-app --lines 100

# Automatyczny start po reboot systemu
pm2 startup         # Generuje polecenie systemd
pm2 save            # Zapisuje aktualną listę procesów

Nginx reverse proxy

Nginx nasłuchuje na portach 80/443 i przekazuje żądania do Node.js na porcie 3000. Dlaczego nie wystawiać Node.js bezpośrednio na port 80? Nginx obsługuje SSL termination, gzip, statyczne pliki, rate limiting i ukrywa serwer aplikacji:

# /etc/nginx/sites-available/my-express-app.conf

upstream express_app {
    # PM2 cluster — Nginx round-robins między instancjami
    server 127.0.0.1:3000;
    keepalive 64;
}

server {
    listen 80;
    server_name api.twoja-domena.pl;

    # Przekieruj HTTP → HTTPS
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name api.twoja-domena.pl;

    # SSL (certbot uzupełni te ścieżki)
    ssl_certificate     /etc/letsencrypt/live/api.twoja-domena.pl/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.twoja-domena.pl/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

    # Kompresja
    gzip on;
    gzip_types application/json text/plain application/javascript;

    # Nagłówki bezpieczeństwa
    add_header X-Frame-Options DENY;
    add_header X-Content-Type-Options nosniff;

    # Rate limiting
    limit_req_zone $binary_remote_addr zone=api:10m rate=30r/m;
    limit_req zone=api burst=10 nodelay;

    location / {
        proxy_pass http://express_app;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        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_cache_bypass $http_upgrade;
        proxy_read_timeout 60s;
    }
}
# Włącz konfigurację i sprawdź poprawność
sudo ln -s /etc/nginx/sites-available/my-express-app.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

SSL przez certbot (Let's Encrypt)

# Instalacja certbota
sudo apt install certbot python3-certbot-nginx

# Pobierz certyfikat (certbot automatycznie edytuje nginx.conf)
sudo certbot --nginx -d api.twoja-domena.pl

# Sprawdź automatyczne odnawianie
sudo systemctl status certbot.timer
sudo certbot renew --dry-run

Zmienne środowiskowe

// src/server.js — załaduj .env jako pierwsze
import 'dotenv/config';     // npm install dotenv
import app from './app.js';

const PORT = process.env.PORT || 3000;
const server = app.listen(PORT, () => {
  console.log(`Server running on port ${PORT} in ${process.env.NODE_ENV} mode`);
  // PM2 wait_ready — powiadom PM2 że proces jest gotowy
  if (process.send) process.send('ready');
});
# .env.production (template — bez prawdziwych sekretów, w git)
NODE_ENV=production
PORT=3000
DATABASE_URL=postgresql://user:PASSWORD@localhost:5432/mydb
JWT_SECRET=CHANGE_ME_IN_PRODUCTION
CORS_ORIGIN=https://twoja-domena.pl

# Na serwerze: skopiuj i uzupełnij prawdziwymi wartościami
# cp .env.production .env
# Edytuj .env — dodaj prawdziwe hasła
# Plik .env MUSI być w .gitignore

Logowanie: Morgan + Winston

// src/utils/logger.js
import winston from 'winston';

const logger = winston.createLogger({
  level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  transports: [
    // Produkcja: logi do pliku (PM2 przekieruje do /var/log/my-app/)
    new winston.transports.Console(),
    new winston.transports.File({
      filename: 'logs/error.log',
      level: 'error',
      maxsize: 10 * 1024 * 1024,  // 10 MB
      maxFiles: 5,
    }),
  ],
});

export default logger;

Graceful shutdown

// src/server.js — obsługa SIGTERM i SIGINT
const shutdown = (signal) => {
  console.log(`Received ${signal}. Starting graceful shutdown...`);

  // Zamknij serwer HTTP — nie przyjmuj nowych połączeń
  server.close((err) => {
    if (err) {
      console.error('Error during shutdown:', err);
      process.exit(1);
    }

    // Zamknij połączenie z bazą danych
    db.end().then(() => {
      console.log('Database connection closed.');
      process.exit(0);
    });
  });

  // Wymuszony shutdown po 10 sekundach (safety net)
  setTimeout(() => {
    console.error('Forced shutdown after timeout');
    process.exit(1);
  }, 10000);
};

process.on('SIGTERM', () => shutdown('SIGTERM'));  // PM2 reload/stop
process.on('SIGINT', () => shutdown('SIGINT'));    // Ctrl+C lokalne

Najczęstsze pytania

Dlaczego nie uruchamiać Express przez node server.js bezpośrednio w produkcji? +
Bezpośrednie uruchomienie przez node server.js ma kilka problemów: (1) Brak auto-restart po błędzie — uncaught exception zatrzyma serwer na zawsze. (2) Brak auto-restart po restarcie systemu — po reboot serwera aplikacja nie wstaje sama. (3) Brak logowania — output leciarze do /dev/null. (4) Brak klastrowania — Node.js jest single-threaded, nie wykorzystuje wszystkich rdzeni CPU. PM2 rozwiązuje wszystkie te problemy.
Czym jest PM2 i jakie ma tryby pracy? +
PM2 (Process Manager 2) to menedżer procesów dla Node.js. Tryby pracy: (1) fork — uruchamia jeden proces Node.js (proste aplikacje). (2) cluster — uruchamia N instancji aplikacji (tyle ile rdzeni CPU) z wbudowanym load balancerem — żadne zmiany w kodzie nie są wymagane. Tryb cluster jest zalecany dla produkcji — zwiększa przepustowość liniowo z liczbą rdzeni.
Jak bezpiecznie zarządzać zmiennymi środowiskowymi w Node.js? +
Nigdy nie hardcoduj sekretów w kodzie. Zalecane podejście: (1) Plik .env z pakietem dotenv (npm install dotenv) — wyłącznie na maszynie, nie w git (.gitignore). (2) Zmienne systemowe — ustaw przez export lub /etc/environment, ładowane przez PM2 automatycznie. (3) Menedżer sekretów (Vault, AWS SSM) — dla enterprise. W PM2 możesz podać plik .env przez env_file w ecosystem.config.js lub ustawić env per environment (env_production, env_staging).
Co to jest graceful shutdown i dlaczego jest ważny? +
Graceful shutdown to obsługa sygnału SIGTERM (który PM2 wysyła przy pm2 reload/stop) przez zamknięcie nowych połączeń i dokończenie aktywnych żądań przed wyłączeniem procesu. Bez graceful shutdown: żądania w trakcie przetwarzania są abruptly przerywane (błąd 500 u klienta), otwarte połączenia do bazy danych mogą wyciekać, transakcje mogą być niezatwierdzone. Z graceful shutdown: reload jest zero-downtime — PM2 uruchamia nowy proces zanim zatrzyma stary.

Sprawdź oferty pasujące do tego scenariusza

Poniżej masz szybkie przejścia do ofert i stron z kodami rabatowymi tam, gdzie są dostępne.