 Autor: [Adam Nadolny](/autorzy/adam-nadolny) Ekspert DevOps i infrastruktury · Zweryfikowano Kwiecień 2026

1.  [Strona główna](/) ›
2.  [Baza wiedzy](/baza-wiedzy/) ›
3.  Node.js Express — deployment produkcyjny

# 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.

Contabo

VPS z dużą liczbą rdzeni — idealne dla PM2 cluster mode i Node.js apps

Node.js ready

[Aktywuj rabat →](/out/contabo)

#Reklama · link partnerski

[Zobacz kod rabatowy →](/kody-rabatowe/contabo)

Mikrus

Tani VPS do testowania Express.js przed deployem na produkcję

Dev/Staging

[Aktywuj rabat →](/out/mikrus)

#Reklama · link partnerski

[Zobacz kod rabatowy →](/kody-rabatowe/mikrus)

LH.pl

Hosting z Node.js przez reverse proxy — bez zarządzania VPS

Managed

[Aktywuj rabat →](/out/lh-pl)

#Reklama · link partnerski

[Zobacz kod rabatowy →](/kody-rabatowe/lh-pl)

## Powiązane strony

-   [Node.js na hostingu współdzielonym](/baza-wiedzy/nodejs-na-hostingu-wspoldzielonym)
-   [Nginx — load balancer i reverse proxy](/baza-wiedzy/nginx-load-balancer-konfiguracja)
-   [Docker na VPS — instalacja i konfiguracja](/baza-wiedzy/docker-na-vps)
-   [Wszystkie artykuły](/baza-wiedzy/)