NestJS — deployment aplikacji Node.js na VPS z PM2
Opublikowano: 10 kwietnia 2026 · Kategoria: VPS
NestJS to opinionated framework Node.js zbudowany nad Express (lub Fastify) z TypeScriptem, modułami, dependency injection i dekoratorami w stylu Angular. Sprawdza się świetnie przy większych API, mikrousługach i teamach — wymusza spójną strukturę tam, gdzie Express pozwala na dowolność. Deployment na VPS składa się z: build produkcyjny, PM2 cluster mode dla wielordzeniowych CPU, Nginx reverse proxy z SSL i TypeORM podpiętym do PostgreSQL.
Inicjalizacja projektu i struktura modułów
# Instalacja NestJS CLI i stworzenie projektu npm install -g @nestjs/cli nest new my-api --package-manager npm cd my-api # Dodanie zaleznosci produkcyjnych npm install @nestjs/config @nestjs/typeorm typeorm pg npm install @nestjs/terminus # health checks npm install @nestjs/swagger swagger-ui-express # dokumentacja API # Wygenerowanie modulu, kontrolera i serwisu nest generate module users nest generate controller users nest generate service users # Struktura projektu (po generacji) # src/ # ├── app.module.ts # Glowny modul # ├── main.ts # Entry point (bootstrap) # ├── users/ # │ ├── users.module.ts # │ ├── users.controller.ts # │ ├── users.service.ts # │ ├── entities/user.entity.ts # │ └── dto/ # │ ├── create-user.dto.ts # │ └── update-user.dto.ts # └── health/ # └── health.module.ts
Konfiguracja aplikacji — main.ts i AppModule
// main.ts — produkcyjny bootstrap
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: ['error', 'warn', 'log'],
});
// Globalna walidacja DTO
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // ignoruj nieznane pola
forbidNonWhitelisted: true,
transform: true, // auto-transform typow
}));
// CORS (dostosuj do swojej domeny frontendowej)
app.enableCors({
origin: process.env.FRONTEND_URL ?? 'http://localhost:3000',
credentials: true,
});
// Swagger tylko w dev
if (process.env.NODE_ENV !== 'production') {
const config = new DocumentBuilder()
.setTitle('Moje API')
.setVersion('1.0')
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('docs', app, document);
}
const port = parseInt(process.env.PORT ?? '3000', 10);
await app.listen(port, '127.0.0.1'); // sluchaj tylko na localhost — Nginx stoi przed
console.log(`Application running on port ${port}`);
}
bootstrap();
// app.module.ts — konfiguracja globalnych modułow
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersModule } from './users/users.module';
import { HealthModule } from './health/health.module';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true, cache: true }),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: (config: ConfigService) => ({
type: 'postgres',
host: config.get('DB_HOST', 'localhost'),
port: config.get<number>('DB_PORT', 5432),
username: config.get('DB_USER'),
password: config.get('DB_PASS'),
database: config.get('DB_NAME'),
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: false, // NIGDY true w produkcji — uzyj migracji
ssl: config.get('DB_SSL') === 'true' ? { rejectUnauthorized: false } : false,
}),
inject: [ConfigService],
}),
UsersModule,
HealthModule,
],
})
export class AppModule {} TypeORM — entity, DTO i serwis
// entities/user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
email: string;
@Column()
hashedPassword: string;
@Column({ default: true })
isActive: boolean;
@CreateDateColumn()
createdAt: Date;
}
// dto/create-user.dto.ts
import { IsEmail, IsString, MinLength } from 'class-validator';
export class CreateUserDto {
@IsEmail()
email: string;
@IsString()
@MinLength(8)
password: string;
}
// users/users.service.ts
import { Injectable, ConflictException, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import * as bcrypt from 'bcrypt';
import { User } from './entities/user.entity';
import { CreateUserDto } from './dto/create-user.dto';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly usersRepository: Repository<User>,
) {}
async create(dto: CreateUserDto): Promise<User> {
const existing = await this.usersRepository.findOneBy({ email: dto.email });
if (existing) throw new ConflictException('Email already registered');
const hashedPassword = await bcrypt.hash(dto.password, 12);
const user = this.usersRepository.create({ email: dto.email, hashedPassword });
return this.usersRepository.save(user);
}
async findAll(): Promise<User[]> {
return this.usersRepository.find({ where: { isActive: true } });
}
async findOne(id: number): Promise<User> {
const user = await this.usersRepository.findOneBy({ id });
if (!user) throw new NotFoundException(`User #${id} not found`);
return user;
}
} Build produkcyjny i PM2 cluster mode
# Build produkcyjny (kompilacja TypeScript)
npm run build
# Wynik: katalog dist/ z plikami .js
# ecosystem.config.js dla PM2
module.exports = {
apps: [
{
name: 'nestjs-api',
script: 'dist/main.js',
instances: 'max', // jeden proces per CPU
exec_mode: 'cluster',
autorestart: true,
watch: false,
max_memory_restart: '512M',
env_production: {
NODE_ENV: 'production',
PORT: 3000,
},
error_file: '/var/log/nestjs/error.log',
out_file: '/var/log/nestjs/out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
},
],
};
# Start i autostart
pm2 start ecosystem.config.js --env production
pm2 save
pm2 startup # wygeneruje komende do uruchomienia przy restarcie
# Zero-downtime reload (bez przerwy w dzialaniu)
pm2 reload nestjs-api
# Status i logi
pm2 status
pm2 logs nestjs-api --lines 50
# Monitorowanie w czasie rzeczywistym
pm2 monit Nginx reverse proxy dla NestJS
# /etc/nginx/sites-available/nestjs-api
upstream nestjs_backend {
server 127.0.0.1:3000;
keepalive 64;
}
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:10m;
location / {
proxy_pass http://nestjs_backend;
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;
}
# WebSockets (NestJS Gateway)
location /socket.io/ {
proxy_pass http://nestjs_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
} Health checks z @nestjs/terminus
// health/health.controller.ts
import { Controller, Get } from '@nestjs/common';
import { HealthCheck, HealthCheckService, TypeOrmHealthIndicator,
MemoryHealthIndicator } from '@nestjs/terminus';
@Controller('health')
export class HealthController {
constructor(
private health: HealthCheckService,
private db: TypeOrmHealthIndicator,
private memory: MemoryHealthIndicator,
) {}
@Get()
@HealthCheck()
check() {
return this.health.check([
() => this.db.pingCheck('database'),
() => this.memory.checkHeap('memory_heap', 512 * 1024 * 1024), // 512 MB
]);
}
}
// Przyklad odpowiedzi GET /health:
// {
// "status": "ok",
// "info": {
// "database": { "status": "up" },
// "memory_heap": { "status": "up" }
// }
// } NestJS vs Express — porównanie
| Cecha | NestJS | Express |
|---|---|---|
| Struktura projektu | Narzucona (moduły, DI) | Dowolna (elastyczna) |
| TypeScript | First-class (natywny) | Opcjonalny (@types/express) |
| Dependency Injection | Wbudowany kontener | Brak (ręcznie lub biblioteka) |
| Walidacja | class-validator + Pipes | express-validator / joi |
| Dokumentacja API | @nestjs/swagger (auto) | swagger-jsdoc (ręcznie) |
| Mikrousługi | Wbudowane transporty | Osobne biblioteki |
| Krzywa nauki | Wyższa (dekoratory) | Niska |
| Kiedy wybrać | Duże projekty, teamy | Proste API, szybki prototyp |