DeployEasy
Menu
LinuxTrung bình

Tạo systemd service cho Node.js: Tự chạy sau khi VPS reboot

Hướng dẫn tạo systemd service cho Node.js/NestJS, cấu hình User, WorkingDirectory, EnvironmentFile, restart, log và graceful shutdown.

· 8 phút đọc · 1.747 từ
Mục lục bài viết

Node.js có thể chạy bằng một terminal:

node dist/main.js

Nhưng khi đóng SSH hoặc VPS reboot, process có thể dừng. Production cần một service manager để quản lý vòng đời ứng dụng.

Trên Ubuntu hiện đại, systemd là hệ thống init và service manager mặc định.

systemd là gì?

systemd quản lý:

  • Service.
  • Startup order.
  • Restart policy.
  • Environment.
  • User chạy process.
  • Log qua journal.
  • Dependency.
  • Resource limit.
  • Shutdown.

Một service được định nghĩa bằng unit file có đuôi:

.service

Khi nào dùng systemd?

Phù hợp khi:

  • Chạy Node.js trực tiếp trên host.
  • Không dùng Docker.
  • Muốn giảm dependency vào PM2.
  • Cần tích hợp chặt với Linux.
  • Muốn log trong journal.
  • Cần quyền và hardening ở service level.

Không cần systemd để quản lý app bên trong container nếu Docker/Compose đã quản lý container. Docker daemon bản thân vẫn do systemd quản lý.

Chuẩn bị ứng dụng

Giả sử:

App directory: /var/www/deployeasy-api/current
Entry file:    dist/main.js
User:          deploy
Port:          3001
Env file:      /var/www/deployeasy-api/shared/.env.production

Kiểm tra chạy thủ công:

cd /var/www/deployeasy-api/current
/usr/bin/node dist/main.js

Terminal khác:

curl -f http://127.0.0.1:3001/health

Chỉ tạo service sau khi app chạy trực tiếp thành công.

Xác định đường dẫn Node.js

command -v node

Ví dụ:

/usr/bin/node

Không viết node mơ hồ trong ExecStart nếu PATH của service không giống shell.

Nếu dùng NVM, đường dẫn có thể nằm trong home user và thay đổi theo version. Production ổn định hơn khi dùng runtime có đường dẫn rõ ràng hoặc symlink được quản lý.

Tạo service file

sudo nano /etc/systemd/system/deployeasy-api.service

Nội dung:

[Unit]
Description=DeployEasy API
After=network.target
Wants=network.target

[Service]
Type=simple

User=deploy
Group=deploy

WorkingDirectory=/var/www/deployeasy-api/current

Environment=NODE_ENV=production
EnvironmentFile=/var/www/deployeasy-api/shared/.env.production

ExecStart=/usr/bin/node dist/main.js

Restart=on-failure
RestartSec=5

TimeoutStopSec=30
KillSignal=SIGTERM

StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

Giải thích [Unit]

Description

Description=DeployEasy API

Mô tả service.

After

After=network.target

Yêu cầu startup order sau target mạng cơ bản. Nó không đảm bảo database bên ngoài đã sẵn sàng.

Wants

Wants=network.target

Tạo dependency yếu với network target.

Giải thích [Service]

Type=simple

Process trong ExecStart được xem là process chính. Phù hợp Node.js chạy foreground.

Không daemonize app bên trong systemd.

UserGroup

User=deploy
Group=deploy

App chạy bằng user thường, không phải root.

User cần quyền:

  • Đọc source.
  • Đọc env.
  • Ghi thư mục upload/cache nếu cần.
  • Bind port trên 1024.

WorkingDirectory

WorkingDirectory=/var/www/deployeasy-api/current

Đặt thư mục chạy ứng dụng.

Nếu đường dẫn không tồn tại, service fail.

Environment

Environment=NODE_ENV=production

Đặt biến trực tiếp.

Không nên đặt secret dài trong unit file vì file thường có thể được đọc bởi user có quyền phù hợp.

EnvironmentFile

EnvironmentFile=/var/www/deployeasy-api/shared/.env.production

File:

PORT=3001
DATABASE_URL=postgresql://...
JWT_SECRET=...

Permission:

sudo chown deploy:deploy \
  /var/www/deployeasy-api/shared/.env.production

sudo chmod 600 \
  /var/www/deployeasy-api/shared/.env.production

Lưu ý: cú pháp EnvironmentFile không hoàn toàn giống parser .env của mọi framework. Tránh syntax phức tạp và kiểm tra thực tế.

ExecStart

ExecStart=/usr/bin/node dist/main.js

Phải dùng đường dẫn executable rõ ràng.

Không viết shell pipeline trực tiếp:

ExecStart=cd /app && npm start

ExecStart không chạy qua shell mặc định.

Nếu cần shell:

ExecStart=/bin/bash -lc 'exec /usr/bin/node dist/main.js'

Nhưng nên tránh shell khi không cần.

Restart policy

Restart=on-failure
RestartSec=5

on-failure restart khi process thoát lỗi hoặc bị signal bất thường.

Không restart vô hạn mà thiếu alert. Service có thể crash-loop.

Shutdown

TimeoutStopSec=30
KillSignal=SIGTERM

systemd gửi SIGTERM và chờ app tắt.

Ứng dụng phải xử lý graceful shutdown.

[Install]

WantedBy=multi-user.target

Cho phép enable service trong boot target thông thường.

Nạp unit mới

sudo systemctl daemon-reload

Start:

sudo systemctl start deployeasy-api

Kiểm tra:

sudo systemctl status deployeasy-api

Enable boot:

sudo systemctl enable deployeasy-api

Hoặc start và enable cùng lúc:

sudo systemctl enable --now deployeasy-api

Xem log

sudo journalctl -u deployeasy-api

Realtime:

sudo journalctl -u deployeasy-api -f

100 dòng cuối:

sudo journalctl -u deployeasy-api -n 100

Từ 30 phút trước:

sudo journalctl \
  -u deployeasy-api \
  --since "30 minutes ago"

Restart và reload

Restart:

sudo systemctl restart deployeasy-api

Stop:

sudo systemctl stop deployeasy-api

Start:

sudo systemctl start deployeasy-api

reload chỉ hoạt động nếu service có ExecReload và ứng dụng hỗ trợ.

Kiểm tra unit file

sudo systemd-analyze verify \
  /etc/systemd/system/deployeasy-api.service

Xem effective unit:

systemctl cat deployeasy-api

Xem property:

systemctl show deployeasy-api

Graceful shutdown với NestJS

Trong bootstrap:

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.enableShutdownHooks();

  await app.listen(
    Number(process.env.PORT ?? 3001),
    "127.0.0.1",
  );
}

Provider có thể triển khai lifecycle hook để đóng resource.

Node.js thuần:

const server = app.listen(3001, "127.0.0.1");

process.on("SIGTERM", () => {
  server.close(() => {
    process.exit(0);
  });
});

Cần đóng thêm:

  • Database pool.
  • Redis connection.
  • Queue worker.
  • Cron.
  • File handle.

Bind localhost

Nếu Nginx cùng VPS:

127.0.0.1:3001

Không cần app public 0.0.0.0 trừ khi kiến trúc yêu cầu.

Kiểm tra:

sudo ss -ltnp | grep :3001

Nginx reverse proxy

server {
    listen 80;
    server_name api.deployeasy.vn;

    location / {
        proxy_pass http://127.0.0.1:3001;

        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;
    }
}

Start bằng npm có được không?

Có thể:

ExecStart=/usr/bin/npm run start:prod

Nhưng đường dẫn, working directory và signal forwarding phải được kiểm tra.

Chạy trực tiếp Node entry thường đơn giản hơn:

ExecStart=/usr/bin/node dist/main.js
/var/www/deployeasy-api/
├── releases/
│   ├── 20260710-100000/
│   └── 20260710-120000/
└── current -> releases/20260710-120000/

Unit dùng:

WorkingDirectory=/var/www/deployeasy-api/current

Khi deploy:

  1. Build release mới.
  2. Test.
  3. Đổi symlink.
  4. Restart service.
  5. Healthcheck.
  6. Rollback symlink nếu lỗi.

Environment thay đổi

Sau khi sửa env:

sudo systemctl restart deployeasy-api

daemon-reload chỉ cần khi unit file/drop-in thay đổi, không phải mọi lần sửa EnvironmentFile.

Override unit an toàn

Thay vì sửa file package cung cấp:

sudo systemctl edit service-name

Tạo drop-in:

[Service]
Environment=EXAMPLE=value

Với service tự tạo trong /etc/systemd/system, bạn có thể quản lý file trực tiếp trong hệ thống cấu hình.

Resource limit

Ví dụ:

[Service]
MemoryMax=600M
CPUQuota=100%
TasksMax=200

Limit cần load test. Đặt quá thấp có thể khiến app bị kill.

Kiểm tra:

systemctl show deployeasy-api \
  -p MemoryCurrent \
  -p MemoryMax \
  -p CPUUsageNSec

Security hardening

Có thể thêm:

[Service]
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/www/deployeasy-api/shared/uploads

Các tùy chọn hardening có thể làm app fail nếu nó cần ghi/đọc vị trí bị chặn.

Đánh giá:

systemd-analyze security deployeasy-api.service

Áp dụng từng bước và test.

PrivateTmp

PrivateTmp=true

Service có /tmp riêng tương đối với service khác.

Nếu app cần chia sẻ file qua /tmp, cấu hình này có thể phá luồng đó.

ProtectSystem

ProtectSystem=strict

Làm phần lớn filesystem read-only với service.

Cho phép ghi cụ thể:

ReadWritePaths=/var/www/deployeasy-api/shared

Đây là cách tốt để bảo đảm app không tự sửa source.

Service phụ thuộc database

Nếu PostgreSQL cùng host và do systemd quản lý:

After=postgresql.service
Wants=postgresql.service

Điều này chỉ ảnh hưởng thứ tự và dependency, không thay thế retry connection.

Nếu database là Docker hoặc remote, app vẫn cần retry.

Lỗi status=203/EXEC

Thường do:

  • ExecStart sai đường dẫn.
  • File không executable.
  • Shebang sai.
  • Permission.
  • Binary không tồn tại.

Kiểm tra:

command -v node
ls -l /usr/bin/node

Lỗi status=200/CHDIR

WorkingDirectory không tồn tại hoặc user không có quyền traverse.

namei -l /var/www/deployeasy-api/current

Lỗi environment không được đọc

Kiểm tra:

  • Đường dẫn.
  • Owner/permission.
  • Cú pháp.
  • Tên biến.
  • User service.

Xem environment mà systemd biết:

sudo systemctl show deployeasy-api \
  --property=Environment

Không đăng output nếu chứa secret.

Service restart liên tục

sudo systemctl status deployeasy-api
sudo journalctl -u deployeasy-api -n 200

Xem restart counter:

systemctl show deployeasy-api \
  -p NRestarts

Nguyên nhân:

  • Port bận.
  • Env thiếu.
  • Database lỗi.
  • Entry file sai.
  • Permission.
  • OOM.
  • App exit ngay.

Reset trạng thái failed

sudo systemctl reset-failed deployeasy-api

Sau khi sửa lỗi:

sudo systemctl restart deployeasy-api

Deploy script

#!/usr/bin/env bash

set -Eeuo pipefail

APP_ROOT="/var/www/deployeasy-api"
RELEASE="$APP_ROOT/releases/$(date +%Y%m%d-%H%M%S)"

mkdir -p "$RELEASE"

# Copy artifact vào release
tar -xzf /tmp/release.tar.gz -C "$RELEASE"

ln -sfn "$APP_ROOT/shared/.env.production" \
  "$RELEASE/.env.production"

ln -sfn "$RELEASE" "$APP_ROOT/current"

sudo systemctl restart deployeasy-api

curl -f http://127.0.0.1:3001/health

Cần rollback nếu healthcheck fail.

sudo giới hạn cho CI

Dùng visudo:

sudo visudo -f /etc/sudoers.d/deployeasy-ci

Ví dụ:

ci ALL=(root) NOPASSWD: /usr/bin/systemctl restart deployeasy-api
ci ALL=(root) NOPASSWD: /usr/bin/systemctl status deployeasy-api

Đánh giá kỹ vì restart service có thể thực thi source do CI user kiểm soát.

Checklist

[ ] App chạy thủ công trước
[ ] ExecStart dùng đường dẫn tuyệt đối
[ ] User/Group không phải root
[ ] WorkingDirectory đúng
[ ] Env file permission hạn chế
[ ] Restart policy rõ
[ ] Graceful shutdown
[ ] Enable boot
[ ] Log đọc bằng journalctl
[ ] Bind localhost
[ ] Resource limit đã test
[ ] Hardening áp dụng từng bước
[ ] Healthcheck sau restart
[ ] Có rollback release

Câu hỏi thường gặp

systemd hay PM2 tốt hơn?

Cả hai đều dùng được. systemd tích hợp sâu Linux; PM2 có CLI và tính năng Node.js thuận tiện hơn. Chọn một process manager chính.

Có cần daemon-reload sau khi sửa env file?

Không, nếu chỉ sửa EnvironmentFile. Cần restart service để process nhận env mới.

systemd có tự build app không?

Không. Build nên nằm trong pipeline hoặc script deploy.

Service có tự restart khi VPS reboot không?

Có nếu đã enable và unit không lỗi.

Kết luận

systemd quản lý Node.js bằng cơ chế chuẩn của Linux. Unit production tốt cần user riêng, đường dẫn rõ, env được bảo vệ, restart có kiểm soát và graceful shutdown.

Nguồn tham khảo

Đọc tiếp