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

⚡ W skrócie · 10 min czytania

-   Express.js na VPS: PM2 ecosystem.config.js, Nginx reverse proxy z proxy\_pass, certbot SSL, zmienne ENV (dotenv), Morgan + winston logger, health check endpoint, graceful shutdown.
-   Struktura projektu produkcyjnego.
-   PM2 ecosystem.config.js.
-   Nginx reverse proxy.
-   SSL przez certbot (Let’s Encrypt).

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 "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 "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 "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/)
-   [VPS pod Node.js i Next.js - PM2, Nginx, SSL, CI/CD
    
    Jak uruchomić Node.js i Next.js na VPS? PM2, reverse proxy Nginx/Caddy, SSL Let's Encrypt i CI/CD przez GitHub Actions.…
    
    ](/baza-wiedzy/vps-pod-nodejs-nextjs/)
-   [NestJS - deployment aplikacji Node.js na VPS z PM2
    
    NestJS na VPS: build produkcyjny, PM2 cluster mode, Nginx reverse proxy, TypeORM + PostgreSQL, ConfigModule, health…
    
    ](/baza-wiedzy/nestjs-nodejs-vps/)
-   [Kubernetes Ingress - Traefik i Nginx Ingress Controller
    
    Kubernetes Ingress: instalacja Traefik (Helm), TLS z cert-manager i Let's Encrypt, IngressClass, rate limiting,…
    
    ](/baza-wiedzy/kubernetes-ingress-traefik/)
-   [Jaki VPS wybrać?](/jaki-vps-wybrac/)

Autor: [Redakcja HostGrade.pl](/autorzy/) Zespół redakcyjny · Aktualizacja: 9 kwietnia 2026

Artykuły HostGrade.pl przygotowuje i aktualizuje redakcja serwisu. Ceny startowe, ceny odnowienia i warunki planów sprawdzamy w cennikach i regulaminach dostawców, a potem liczymy całkowity koszt za 2 lata (cena startowa plus odnowienia). Zasady zbierania danych i liczenia kosztów opisujemy na stronie Metodologia. Linki partnerskie oznaczamy jako #reklama.

[O redakcji i metodologii →](/autorzy/)