Deploy Web App với Docker: Nginx + Node.js

Photo of author

Văn Ngọc Tân

Bạn có biết?

Bạn có bao giờ deploy một ứng dụng Node.js lên server, rồi phải cài đặt nginx, cấu hình proxy, systemd services, và tự hỏi “sao phải phức tạp vậy không?”

Với Docker, bạn chỉ cần một file docker-compose.yml — chạy nginx + Node.js + database trong vài giây!

Kiến trúc Web App đơn giản

Chúng ta sẽ deploy một web app có:

  • Node.js API — Express server xử lý business logic
  • Nginx — Reverse proxy và static files
  • Communicate — Nginx forward requests tới Node.js

Project Structure

my-webapp/
├── docker-compose.yml
├── nginx/
│   └── default.conf
├── app/
│   ├── package.json
│   ├── index.js
│   └── Dockerfile
└── public/
    └── index.html

Node.js App

1. package.json

{
  "name": "my-webapp",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "express": "^4.18.2"
  }
}

2. index.js

const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

app.use(express.json());

// API endpoint
app.get('/api/hello', (req, res) => {
  res.json({ message: 'Hello from Node.js API!' });
});

// Health check
app.get('/health', (req, res) => {
  res.json({ status: 'ok' });
});

app.listen(PORT, () => {
  console.log(\`Server running on port \${PORT}\`);
});

3. App Dockerfile

FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY index.js ./

EXPOSE 3000

CMD ["node", "index.js"]

Nginx Configuration

default.conf

server {
    listen 80;
    server_name localhost;

    location / {
        root /usr/share/nginx/html;
        index index.html;
    }

    location /api/ {
        proxy_pass http://node-app:3000;
        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;
    }

    location /health {
        proxy_pass http://node-app:3000/health;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
    }
}

Nginx Dockerfile

FROM nginx:alpine

COPY default.conf /etc/nginx/conf.d/default.conf
COPY public/ /usr/share/nginx/html/

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

Docker Compose

docker-compose.yml

version: '3.8'

services:
  nginx:
    build: ./nginx
    ports:
      - "80:80"
    depends_on:
      - node-app
    networks:
      - webapp-net
    restart: unless-stopped

  node-app:
    build: ./app
    environment:
      - NODE_ENV=production
      - PORT=3000
    networks:
      - webapp-net
    restart: unless-stopped

networks:
  webapp-net:
    driver: bridge

Workflow

# Build và chạy
$ docker-compose up -d

# Xem logs
$ docker-compose logs -f

# Xem status
$ docker-compose ps

# Stop
$ docker-compose down

# Rebuild sau khi thay đổi code
$ docker-compose up -d --build

Production Considerations

1. Health Checks

# docker-compose.yml với health checks
version: '3.8'

services:
  nginx:
    build: ./nginx
    ports:
      - "80:80"
    healthcheck:
      test: ["CMD", "wget", "-q", "--spider", "http://localhost/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  node-app:
    build: ./app
    healthcheck:
      test: ["CMD", "wget", "-q", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

2. Resource Limits

  node-app:
    build: ./app
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 256M

3. Environment Variables

version: '3.8'

services:
  node-app:
    build:
      context: ./app
      args:
        NODE_ENV: production
    environment:
      - NODE_ENV=${NODE_ENV:-production}
      - DB_HOST=${DB_HOST:-localhost}
      - DB_PORT=${DB_PORT:-5432}
    env_file:
      - .env

4. Multiple Environments

# Development
$ docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d

# Production
$ docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d

5. SSL/HTTPS với Nginx

# Cấu hình SSL
server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate /etc/nginx/ssl/cert.crt;
    ssl_certificate_key /etc/nginx/ssl/cert.key;

    location / {
        proxy_pass http://node-app:3000;
        ...
    }
}

server {
    listen 80;
    server_name example.com;
    return 301 https://$server_name$request_uri;
}

Multi-stage Dockerfile cho Node.js

Cách này gọn hơn — build và run trong một image:

# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Production stage
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY package*.json ./
RUN npm ci --only=production

EXPOSE 3000
CMD ["node", "dist/index.js"]

Flow Request

Bước Thành phần Mô tả
1 Browser Gửi request tới nginx:80
2 Nginx Nhận request, rewrite URL
3 Nginx Proxy_pass tới node-app:3000
4 Node.js Xử lý business logic
5 Node.js Return JSON response
6 Nginx Forward response về browser

Best Practices

  1. Không dùng Alpine cho dev — Alpine bỏ debug symbols, khó debug
  2. Luôn có health checks — Docker sẽ restart container nếu fails
  3. Giới hạn resources — Tránh container chiếm full server
  4. Environment variables — Dùng env_file cho secrets
  5. SSL trong production — Nginx handle HTTPS
  6. Logging to stdout — Docker sẽ tự thu thập logs
  7. Multi-stage build — Giảm image size
  8. Non-root user — Bảo mật
  9. Graceful shutdown — Xử lý SIGTERM
  10. Separate dev và prod — Dùng docker-compose.*.yml

Bước tiếp theo

Tiếp theo, hãy tìm hiểu cách chạy database với Docker!

👉 Đọc tiếp: Docker cho Database: MySQL, PostgreSQL, Redis

0 0 đánh giá
Đánh giá bài viết
Theo dõi
Thông báo của
guest
0 Góp ý
Cũ nhất
Mới nhất Được bỏ phiếu nhiều nhất
Phản hồi nội tuyến
Xem tất cả bình luận