Featured image of post Triển khai Node.js với Docker + Nginx

Triển khai Node.js với Docker + Nginx

Case study triển khai ứng dụng Node.js (Express) với Docker và Nginx reverse proxy — từ viết code đến chạy production bằng Docker Compose.

Triển khai Node.js với Docker + Nginx — Case Study

Bạn đã viết xong API bằng Node.js, test local ngon lành. Nhưng khi đưa lên server thật, bạn gặp hàng loạt câu hỏi:

  • Làm sao chạy app ổn định, tự restart khi crash?
  • Làm sao để client không truy cập thẳng vào port 3000?
  • Làm sao xử lý SSL, rate limit, static files?

Câu trả lời: Docker + Nginx reverse proxy. Trong bài viết này, tôi sẽ hướng dẫn bạn triển khai một ứng dụng Node.js (Express) từ đầu đến cuối, sử dụng Docker để đóng gói và Nginx làm reverse proxy phía trước.


Kiến trúc tổng quan

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Client (Browser / Mobile)
   ┌─────────┐
   │  Nginx   │  ← Port 80/443 (HTTP/HTTPS)
   │  (proxy) │
   └────┬─────┘
        │ proxy_pass http://app:3000
   ┌─────────┐
   │ Node.js  │  ← Port 3000 (internal)
   │ (Express)│
   └─────────┘

Nginx đứng trước nhận request từ client, sau đó chuyển tiếp (proxy) đến Node.js chạy bên trong. Client không bao giờ truy cập trực tiếp vào Node.js.

Tại sao cần Nginx phía trước?

Lý doGiải thích
Reverse proxyẨn port thật của app, client chỉ thấy port 80/443
Static filesNginx phục vụ file tĩnh (CSS, JS, ảnh) nhanh hơn Node.js rất nhiều
SSL terminationNginx xử lý HTTPS, Node.js chỉ nhận HTTP thuần
Rate limitingGiới hạn request ở tầng Nginx, bảo vệ Node.js khỏi quá tải
Load balancingKhi scale nhiều instance Node.js, Nginx phân phối request

Bước 1: Tạo ứng dụng Node.js (Express)

Cấu trúc project

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
my-app/
├── src/
│   └── server.js
├── package.json
├── package-lock.json
├── Dockerfile
├── .dockerignore
├── nginx/
│   └── default.conf
└── docker-compose.yml

File package.json

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
  "name": "my-app",
  "version": "1.0.0",
  "main": "src/server.js",
  "scripts": {
    "start": "node src/server.js"
  },
  "dependencies": {
    "express": "^4.21.0"
  }
}

File src/server.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const express = require("express");
const app = express();
const PORT = process.env.PORT || 3000;

app.get("/", (req, res) => {
  res.json({ message: "Hello from Node.js!", timestamp: new Date() });
});

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

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

Lưu ý: app.listen(PORT, "0.0.0.0") — phải bind 0.0.0.0 để container nhận được request từ bên ngoài. Nếu bind 127.0.0.1 (localhost), Nginx sẽ không thể kết nối đến app.


Bước 2: Viết Dockerfile cho Node.js

File .dockerignore

1
2
3
4
node_modules
npm-debug.log
.git
.env

Theo tài liệu chính thức của Docker, Dockerfile cho Node.js nên dùng multi-stage build, tạo non-root user và sử dụng cache mount để tối ưu.

File Dockerfile

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# Stage 1: Install dependencies
FROM node:18-alpine AS deps
WORKDIR /app

COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci --only=production

# Stage 2: Production
FROM node:18-alpine AS production
WORKDIR /app

# Tạo non-root user (theo best practice từ Docker docs)
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001 -G nodejs

ENV NODE_ENV=production

COPY --from=deps /app/node_modules ./node_modules
COPY src ./src
COPY package.json ./

USER nodejs

EXPOSE 3000

CMD ["node", "src/server.js"]

Giải thích:

Kỹ thuậtMục đích
node:18-alpineBase image nhỏ (~170 MB thay vì ~900 MB của node:18)
Multi-stage buildStage deps cài dependencies, stage production chỉ copy kết quả
npm ciCài đặt chính xác theo package-lock.json, đảm bảo reproducible builds
--mount=type=cacheCache npm packages giữa các lần build
Non-root userKhông chạy app bằng root, tăng bảo mật
COPY src ./srcChỉ copy source code cần thiết, không copy toàn bộ project

Bước 3: Cấu hình Nginx làm Reverse Proxy

File nginx/default.conf

Theo tài liệu chính thức của Nginx, directive proxy_pass được dùng để chuyển tiếp request đến server khác. Các header proxy_set_header cần được thiết lập để server backend nhận đúng thông tin client.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
upstream nodejs_app {
    server app:3000;
}

server {
    listen 80;
    server_name _;

    # Giới hạn kích thước request body (chống upload quá lớn)
    client_max_body_size 10m;

    location / {
        proxy_pass http://nodejs_app;

        # Chuyển tiếp thông tin client gốc đến Node.js
        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;

        # Hỗ trợ WebSocket (nếu app cần)
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    # Health check endpoint (Nginx trả thẳng, không cần qua Node.js)
    location = /nginx-health {
        access_log off;
        return 200 "ok";
        add_header Content-Type text/plain;
    }
}

Giải thích các directive quan trọng:

DirectiveÝ nghĩa
upstream nodejs_appĐịnh nghĩa nhóm backend server. Khi scale nhiều instance, thêm server vào đây
proxy_pass http://nodejs_appChuyển tiếp request đến upstream đã định nghĩa
proxy_set_header Host $hostGiữ nguyên hostname gốc từ client (mặc định Nginx đổi thành $proxy_host)
proxy_set_header X-Real-IPGửi IP thật của client đến Node.js (không phải IP của Nginx)
proxy_set_header X-Forwarded-ForChuỗi IP qua các proxy, giúp Node.js biết client thật
proxy_set_header X-Forwarded-ProtoCho Node.js biết client dùng HTTP hay HTTPS
proxy_http_version 1.1Bắt buộc để hỗ trợ WebSocket và keep-alive

Bước 4: Docker Compose — Kết nối tất cả

Theo Docker Compose production guide, khi deploy production nên: bỏ volume bind cho code, set restart policy, và tách file compose cho từng môi trường.

File docker-compose.yml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
      target: production
    container_name: nodejs-app
    restart: unless-stopped
    environment:
      - NODE_ENV=production
      - PORT=3000
    expose:
      - "3000"
    healthcheck:
      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s
    networks:
      - app-network

  nginx:
    image: nginx:1.27-alpine
    container_name: nginx-proxy
    restart: unless-stopped
    ports:
      - "80:80"
    volumes:
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      app:
        condition: service_healthy
    networks:
      - app-network

networks:
  app-network:
    driver: bridge

Các điểm quan trọng:

Cấu hìnhGiải thích
expose: "3000"Chỉ mở port nội bộ giữa các container, không expose ra host
ports: "80:80"Chỉ Nginx được expose ra ngoài
depends_on: condition: service_healthyNginx chỉ start sau khi Node.js healthy
restart: unless-stoppedTự restart khi crash, trừ khi bạn chủ động stop
:ro (read-only)Mount config Nginx ở chế độ chỉ đọc
networks: app-networkCả 2 container cùng network, Nginx gọi app:3000 qua tên service

Bước 5: Build và chạy

1
2
3
4
5
6
7
8
# Build và chạy tất cả
docker compose up --build -d

# Xem logs
docker compose logs -f

# Kiểm tra trạng thái
docker compose ps

Kiểm tra hoạt động:

1
2
3
4
5
6
7
8
# Request qua Nginx (port 80)
curl http://localhost

# Kiểm tra health của Node.js
curl http://localhost/health

# Kiểm tra health của Nginx
curl http://localhost/nginx-health

Kết quả mong đợi:

1
{"message":"Hello from Node.js!","timestamp":"2026-02-10T12:00:00.000Z"}

Bước 6: Deploy lại khi thay đổi code

Theo Docker Compose docs, khi cập nhật code, bạn chỉ cần rebuild service cần thiết:

1
2
3
# Rebuild chỉ service app (không ảnh hưởng Nginx)
docker compose build app
docker compose up --no-deps -d app

Flag --no-deps đảm bảo chỉ restart service app, không restart Nginx. Client không bị gián đoạn.


Mở rộng: Scale nhiều instance Node.js

Khi traffic tăng, bạn có thể chạy nhiều instance Node.js và để Nginx load balance:

1
docker compose up --scale app=3 -d

Cập nhật nginx/default.conf để Nginx biết có nhiều instance:

1
2
3
4
5
upstream nodejs_app {
    # Docker Compose DNS tự động resolve tên service
    # thành tất cả container IP đang chạy
    server app:3000;
}

Khi dùng Docker Compose với --scale, Docker DNS tự động resolve app thành tất cả IP của các container app. Nginx sẽ round-robin request đến từng instance.


Ghi chú triển khai

  • Khi áp dụng vào dự án của bạn, hãy lưu ý:
    • Luôn dùng expose thay vì ports cho service backend — chỉ Nginx mới cần expose ra ngoài
    • Bind 0.0.0.0 trong Node.js, không bind 127.0.0.1
    • Dùng npm ci thay npm install để đảm bảo reproducible builds
    • Tạo non-root user trong Dockerfile cho bảo mật
  • Best practices:
    • Dùng docker compose up --no-deps -d app khi chỉ cần redeploy app
    • Pin version cho base image (node:18-alpine, nginx:1.27-alpine) thay vì latest
    • Tách docker-compose.prod.yml riêng cho production config
  • Troubleshooting:
    • Nginx báo 502 Bad Gateway? → Node.js chưa start xong hoặc bind sai address. Kiểm tra docker compose logs app
    • Không kết nối được giữa Nginx và Node.js? → Đảm bảo cả 2 cùng network và dùng đúng tên service trong proxy_pass
    • Request bị timeout? → Kiểm tra client_max_body_sizeproxy_read_timeout trong Nginx config

🎯 Lời kết

Tóm tắt lại flow triển khai:

  1. Viết app Node.js (Express) với health check endpoint
  2. Dockerfile multi-stage build + non-root user
  3. Nginx config reverse proxy với proxy_pass và các header cần thiết
  4. Docker Compose kết nối app + Nginx, chỉ expose port 80
  5. Deploy bằng docker compose up --build -d
  6. Update bằng docker compose build app && docker compose up --no-deps -d app

Đây là kiến trúc cơ bản nhất để đưa một ứng dụng Node.js lên production. Từ đây bạn có thể mở rộng thêm SSL (Let’s Encrypt), database, monitoring tuỳ nhu cầu. 🚀

Nếu bạn thấy bài viết này hữu ích, hãy nhớ ⭐ star repo hoặc 📱 chia sẻ với bạn bè nhé! 😊


Tài liệu tham khảo