Triển khai Node.js bằng Portainer: Image, Env, Domain, SSL

P P T P Chung

Triển khai ứng dụng Node.js bằng Portainer: build image, env, domain, SSL

Triển khai Node.js không chỉ là “chạy npm start trên server”. Khi app đi vào production, bạn cần image ổn định, biến môi trường an toàn, domain trỏ đúng, SSL hợp lệ, log dễ kiểm tra, restart tự động. Làm thủ công bằng Docker CLI vẫn được, nhưng khi nhiều app, nhiều container, nhiều network → dễ rối.

Portainer giải quyết phần lớn vấn đề đó. Đây là giao diện quản trị Docker trực quan, giúp bạn deploy app, quản lý container, image, volume, network, env, stack. Bài này hướng dẫn quy trình thực tế: từ build image Node.js, cấu hình env, gắn domain, bật SSL.

1. Kiến trúc triển khai đề xuất

Mô hình phổ biến:

VPS/Linux server → chạy Docker. – Portainer → quản lý Docker qua UI. – Node.js app → đóng gói bằng Docker image. – Reverse proxy → Nginx Proxy Manager, Traefik, Caddy hoặc Nginx. – Domain → trỏ DNS về IP server. – SSL → Let’s Encrypt.

Luồng request:

User → https://example.com → Reverse Proxy → Node.js container:3000

Node.js app không nhất thiết expose trực tiếp ra internet. Thường chỉ reverse proxy public port 80/443, còn app chạy nội bộ trong Docker network.


2. Chuẩn bị server

Cần:

– VPS Ubuntu/Debian. – Domain. – Docker đã cài. – Portainer đã chạy. – Repo Node.js có Dockerfile.

Cài Docker nhanh:

curl -fsSL https://get.docker.com | sh

Chạy Portainer:

docker volume create portainer_data

docker run -d -p 9000:9000 -p 9443:9443 --name portainer --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer-ce:latest

Truy cập:

https://YOUR_SERVER_IP:9443

Tạo admin user → chọn local Docker environment.


3. Viết Dockerfile cho Node.js

Ví dụ app Express/Nest/Next custom server đều tương tự.

FROM node:20-alpine AS deps

WORKDIR /app

COPY package*.json ./

RUN npm ci

FROM node:20-alpine AS runner

WORKDIR /app

ENV NODE_ENV=production

COPY --from=deps /app/node_modules ./node_modules COPY . .

EXPOSE 3000

CMD ["npm", "start"]

Nếu app cần build trước, ví dụ TypeScript/NestJS:

FROM node:20-alpine AS builder

WORKDIR /app

COPY package*.json ./ RUN npm ci

COPY . . RUN npm run build

FROM node:20-alpine AS runner

WORKDIR /app

ENV NODE_ENV=production

COPY package*.json ./ RUN npm ci --omit=dev

COPY --from=builder /app/dist ./dist

EXPOSE 3000

CMD ["node", "dist/main.js"]

Điểm quan trọng:

– Dùng npm ci → build ổn định. – Không copy .env vào image. – Không expose DB password trong Dockerfile. – Dùng multi-stage build → image nhẹ hơn. – App phải listen trên 0.0.0.0, không phải localhost.

Ví dụ Express:

app.listen(process.env.PORT || 3000, '0.0.0.0')

4. Tạo .dockerignore

Không bỏ qua bước này. Nếu không, image có thể chứa file thừa, secret, cache.

node_modules
npm-debug.log
.git
.gitignore
.env
.env.*
Dockerfile
docker-compose.yml
README.md
.env không nên nằm trong image. Env nên cấu hình ở Portainer/Stack.

5. Build image: 3 cách phổ biến

Cách 1: Build từ Git trong Portainer

Trong Portainer:

Stacks → Add stack → Repository

Nhập:

– Repository URL. – Branch. – Compose path, ví dụ docker-compose.yml. – Git credential nếu repo private.

Portainer sẽ pull repo → deploy theo compose.

Phù hợp khi bạn muốn deploy trực tiếp từ Git.


Cách 2: Build local rồi push registry

Trên máy dev/CI:

docker build -t yourname/node-app:1.0.0 .
docker push yourname/node-app:1.0.0

Trên Portainer dùng image:

services:
  app:
    image: yourname/node-app:1.0.0

Ưu điểm:

– CI/CD rõ ràng. – Rollback dễ. – Không build nặng trên VPS. – Version image kiểm soát tốt.

Production nên dùng tag version như 1.0.0, 2025-01-15, commit SHA. Tránh phụ thuộc latest.


Cách 3: Upload Dockerfile trong Portainer

Portainer có thể build image từ UI. Phù hợp test nhanh, nhưng production nên dùng Git/CI.


6. Deploy bằng Docker Compose trong Portainer

Tạo file docker-compose.yml:

services:
  node_app:
    image: yourname/node-app:1.0.0
    container_name: node_app
    restart: unless-stopped
    environment:
      NODE_ENV: production
      PORT: 3000
      DATABASE_URL: ${DATABASE_URL}
      JWT_SECRET: ${JWT_SECRET}
    networks:
      - web
    expose:
      - "3000"

networks: web: external: true

Trong Portainer:

Stacks → Add stack → Web editor

Paste compose → nhập env ở phần Environment variables:

DATABASE_URL=postgresql://user:password@db:5432/app
JWT_SECRET=your-long-secret

Deploy stack.

Lưu ý:

expose chỉ mở trong Docker network. – ports mở ra host. – Với reverse proxy, thường dùng expose, không dùng ports. – Nếu cần debug tạm:

ports:
  - "3000:3000"

Sau khi xong nên đóng lại.


7. Quản lý biến môi trường an toàn

Env thường gồm:

NODE_ENVPORTDATABASE_URLREDIS_URLJWT_SECRETSMTP_HOSTSMTP_USERSMTP_PASSS3_ACCESS_KEYS3_SECRET_KEY

Nguyên tắc:

– Không commit .env. – Không hardcode secret trong code. – Không ghi secret vào Dockerfile. – Dùng Portainer env hoặc Docker secrets nếu cần bảo mật hơn. – Rotate secret định kỳ. – Tách env staging/production.

Trong Node.js, validate env khi app start. Ví dụ:

const required = ['DATABASE_URL', 'JWT_SECRET']

for (const key of required) { if (!process.env[key]) { throw new Error(Missing env: ${key}) } }

App fail sớm → tốt hơn chạy lỗi ngầm.


8. Tạo Docker network cho reverse proxy

Nếu dùng Nginx Proxy Manager hoặc Traefik, app và proxy cần chung network.

Tạo network:

docker network create web

Nếu reverse proxy đã chạy, attach nó vào network web.

Ví dụ Nginx Proxy Manager compose:

services:
  npm:
    image: jc21/nginx-proxy-manager:latest
    container_name: npm
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "81:81"
    volumes:
      - npm_data:/data
      - npm_letsencrypt:/etc/letsencrypt
    networks:
      - web

volumes: npm_data: npm_letsencrypt:

networks: web: external: true

Port 81 là UI quản trị. Nên hạn chế IP hoặc đổi rule firewall nếu public.


9. Trỏ domain về server

Vào DNS provider, tạo record:

A     example.com        YOUR_SERVER_IP
A     www.example.com    YOUR_SERVER_IP

Nếu dùng IPv6:

AAAA  example.com        YOUR_IPV6

Chờ DNS propagate. Kiểm tra:

dig example.com

Hoặc:

nslookup example.com

Domain phải trỏ đúng IP trước khi cấp SSL Let’s Encrypt.


10. Cấu hình reverse proxy với Nginx Proxy Manager

Truy cập NPM:

http://YOUR_SERVER_IP:81

Tạo Proxy Host:

Hosts → Proxy Hosts → Add Proxy Host

Tab Details:

– Domain Names: example.com – Scheme: http – Forward Hostname/IP: node_app – Forward Port: 3000 – Websockets Support: bật nếu app dùng socket – Block Common Exploits: bật

node_app cùng Docker network với NPM, NPM resolve được container name.

Nếu không resolve được:

– Kiểm tra cùng network chưa. – Kiểm tra container name. – Kiểm tra app listen 0.0.0.0. – Kiểm tra port đúng chưa.


11. Bật SSL Let’s Encrypt

Trong Proxy Host → tab SSL:

– Request a new SSL Certificate. – Chọn domain. – Force SSL: bật. – HTTP/2 Support: bật. – HSTS: cân nhắc bật sau khi chắc chắn HTTPS ổn. – Nhập email. – Agree Let’s Encrypt Terms. – Save.

Nếu lỗi cấp SSL:

– Domain chưa trỏ đúng IP. – Port 80/443 bị firewall chặn. – Có service khác chiếm port. – DNS proxy/CDN gây sai xác thực. – IPv6 trỏ sai.

Kiểm tra port:

sudo ss -tulpn | grep -E ':80|:443'

Kiểm tra firewall:

sudo ufw status

Mở port:

sudo ufw allow 80
sudo ufw allow 443

12. Kiểm tra logs, health, restart

Trong Portainer:

Containers → node_app → Logs

Hoặc CLI:

docker logs -f node_app

Kiểm tra app:

curl -I https://example.com

Compose nên có restart policy:

restart: unless-stopped

Có thể thêm healthcheck:

healthcheck:
  test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
  interval: 30s
  timeout: 5s
  retries: 3

Trong app tạo endpoint:

app.get('/health', (req, res) => res.send('ok'))

Healthcheck giúp phát hiện container lỗi nhanh hơn.


13. Cập nhật phiên bản app

Quy trình an toàn:

1. Build image mới. 2. Push registry. 3. Đổi tag trong stack. 4. Redeploy. 5. Xem logs. 6. Test domain. 7. Rollback nếu lỗi.

Ví dụ:

image: yourname/node-app:1.0.1

Trong Portainer:

Stacks → app_stack → Editor → Update the stack

Rollback:

image: yourname/node-app:1.0.0

Không nên sửa code trực tiếp trong container. Container là immutable. Muốn đổi app → build image mới.


14. Một số lỗi thường gặp

App chạy local OK, Docker lỗi

Nguyên nhân thường gặp:

– Thiếu env. – App listen localhost. – Thiếu build step. – Sai CMD. – Port không khớp. – File .env không được copy.

Fix:

app.listen(PORT, '0.0.0.0')

Reverse proxy báo 502

Kiểm tra:

– Container app có running không. – NPM và app có cùng network không. – Forward hostname đúng container name không. – App port đúng không. – Log app có crash không.

SSL không cấp được

Kiểm tra:

– DNS A record. – Port 80/443. – Firewall. – Cloudflare proxy. – IPv6 sai.

Env không nhận

Trong Portainer stack, nếu dùng ${VAR}, phải khai báo ở Environment variables hoặc env file tương ứng. Sau khi đổi env cần redeploy container.


15. Checklist production ngắn gọn

– Dockerfile multi-stage. – .dockerignore sạch. – Image có version tag. – Env tách khỏi image. – Container restart: unless-stopped. – App listen 0.0.0.0. – Reverse proxy dùng Docker network riêng. – Domain trỏ đúng IP. – SSL Let’s Encrypt bật Force SSL. – Logs kiểm tra sau deploy. – Backup DB/volume. – Không expose port app nếu không cần. – Không dùng secret yếu.


Kết luận

Portainer giúp việc triển khai Node.js bằng Docker trở nên rõ ràng hơn: build image, cấu hình env, redeploy, xem logs, quản lý network đều có UI. Tuy vậy, nền tảng vẫn là Docker đúng chuẩn: image sạch, env an toàn, container không state, reverse proxy tách biệt, SSL đầy đủ.

Quy trình thực tế nên là: code → build image → push registry → Portainer deploy stack → reverse proxy domain → bật SSL → kiểm tra logs. Khi đã chuẩn hóa, bạn có thể triển khai thêm nhiều app Node.js trên cùng server mà vẫn dễ quản lý, dễ rollback, ít lỗi production hơn.

Tác giả

P T P

Chia sẻ

Bài viết liên quan

Bình luận (0)

Email của bạn sẽ không được hiển thị công khai.

Chưa có bình luận. Hãy là người đầu tiên!