DeployEasy
Menu
DockerNâng cao

Build Docker trên local rồi deploy lên VPS

Quy trình từng bước tạo Dockerfile, chạy thử monorepo REMAP trên local và dùng cùng cấu hình Docker Compose để triển khai lên VPS.

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

Tài liệu này dành cho luồng làm việc đúng mục tiêu hiện tại:

  1. Tạo file Docker trong repo.
  2. Build và chạy thử toàn bộ trên local.
  3. Xác nhận local chạy ổn.
  4. Đem nguyên bộ file đó lên VPS.
  5. Build lại trên VPS và public bằng domain thật.

Repo hiện tại gồm:

  • API: @remap/api
  • User app: @remap/user
  • Admin app: @remap/admin
  • Shared packages: packages/api-client, packages/theme

Nguyên tắc quan trọng:

  • local và VPS phải dùng cùng Dockerfile, cùng Compose
  • chỉ khác nhau ở env, domain, SSL và reverse proxy production
  • không deploy kiểu local chạy npm, VPS chạy Docker

1. Kết quả cuối cùng cần đạt

Sau khi làm xong, bạn sẽ có:

  • local chạy được bằng docker compose up
  • API local mở ở http://localhost:3000/api/health
  • user app local mở ở http://localhost:3001
  • admin app local mở ở http://localhost:3002
  • VPS dùng lại đúng bộ file đó để chạy production

2. Chuẩn bị trên máy local

Cài sẵn:

  • Docker Desktop hoặc Docker Engine
  • Docker Compose v2
  • Git

Kiểm tra:

docker --version
docker compose version
git --version

Di chuyển vào repo:

cd D:\Thiện\remap

3. Tạo các file Docker cần commit vào repo

Các file nên có trong repo:

  • apps/api/Dockerfile
  • apps/user/Dockerfile
  • apps/admin/Dockerfile
  • .dockerignore
  • docker-compose.yml
  • docker-compose.override.yml
  • deploy/nginx/default.conf

Ý nghĩa:

  • docker-compose.yml: cấu hình base dùng chung cho local và VPS
  • docker-compose.override.yml: cấu hình local, tự được Compose nạp thêm
  • deploy/nginx/default.conf: cấu hình Nginx dùng khi public app

Không nên tạo thủ công các file này trực tiếp trên VPS.


4. Tạo env cho local Docker

4.1 API local

Tạo file apps/api/.env.docker:

NODE_ENV=production
API_PORT=3000
API_PREFIX=/api

DATABASE_HOST=postgres
DATABASE_PORT=5432
DATABASE_USER=remap
DATABASE_PASSWORD=remap_password
DATABASE_NAME=remap

JWT_ACCESS_SECRET=local-access-secret
JWT_REFRESH_SECRET=local-refresh-secret
JWT_ACCESS_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d

CORS_ORIGINS=http://localhost:3001,http://localhost:3002

Giải thích:

  • DATABASE_HOST=postgres vì API sẽ kết nối tới service Postgres trong Docker network
  • local vẫn nên dùng NODE_ENV=production để hành vi gần production hơn

4.2 User app local

Tạo file apps/user/.env.docker:

NEXT_PUBLIC_API_BASE_URL=http://localhost:3000/api

4.3 Admin app local

Tạo file apps/admin/.env.docker:

VITE_API_BASE_URL=http://localhost:3000/api

Lưu ý:

  • useradmin đọc biến public tại thời điểm build image
  • nếu đổi API URL thì phải build lại image tương ứng

5. Tạo env cho production

5.1 API production

Tạo file apps/api/.env.production:

NODE_ENV=production
API_PORT=3000
API_PREFIX=/api

DATABASE_HOST=postgres
DATABASE_PORT=5432
DATABASE_USER=remap
DATABASE_PASSWORD=CHANGE_ME_DB_PASSWORD
DATABASE_NAME=remap

JWT_ACCESS_SECRET=CHANGE_ME_LONG_RANDOM_ACCESS_SECRET
JWT_REFRESH_SECRET=CHANGE_ME_LONG_RANDOM_REFRESH_SECRET
JWT_ACCESS_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d

CORS_ORIGINS=https://remap.io.vn,https://admin.remap.io.vn

5.2 User app production

Tạo file apps/user/.env.production:

NEXT_PUBLIC_API_BASE_URL=https://api.remap.io.vn/api

5.3 Admin app production

Tạo file apps/admin/.env.production:

VITE_API_BASE_URL=https://api.remap.io.vn/api

6. Viết Dockerfile theo monorepo hiện tại

Vì đây là npm workspaces monorepo, image phải build từ root repo, không build rời từng app.

6.1 API Dockerfile

Tạo apps/api/Dockerfile:

FROM node:22-alpine AS deps
WORKDIR /app

COPY package*.json ./
COPY apps/api/package*.json apps/api/
COPY packages/api-client/package*.json packages/api-client/
COPY packages/theme/package*.json packages/theme/
RUN npm install

FROM deps AS builder
WORKDIR /app

COPY . .
RUN npm run api:build

FROM deps AS migrator
WORKDIR /app

COPY . .
WORKDIR /app/apps/api

FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production

COPY package*.json ./
COPY apps/api/package*.json apps/api/
COPY packages/api-client/package*.json packages/api-client/
COPY packages/theme/package*.json packages/theme/
RUN npm install --omit=dev

COPY --from=builder /app/apps/api/dist apps/api/dist

WORKDIR /app/apps/api
EXPOSE 3000
CMD ["node", "dist/main.js"]

6.2 User app Dockerfile

Tạo apps/user/Dockerfile:

FROM node:22-alpine AS builder
WORKDIR /app

COPY package*.json ./
COPY apps/user/package*.json apps/user/
COPY packages/api-client/package*.json packages/api-client/
COPY packages/theme/package*.json packages/theme/
RUN npm install

COPY . .
ARG NEXT_PUBLIC_API_BASE_URL
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
RUN npm run user:build

FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production

COPY package*.json ./
COPY apps/user/package*.json apps/user/
COPY packages/api-client/package*.json packages/api-client/
COPY packages/theme/package*.json packages/theme/
RUN npm install --omit=dev

COPY --from=builder /app/apps/user/.next apps/user/.next
COPY --from=builder /app/apps/user/public apps/user/public
COPY --from=builder /app/apps/user/next.config.ts apps/user/next.config.ts

WORKDIR /app/apps/user
EXPOSE 3001
CMD ["npm", "start"]

6.3 Admin app Dockerfile

Tạo apps/admin/Dockerfile:

FROM node:22-alpine AS builder
WORKDIR /app

COPY package*.json ./
COPY apps/admin/package*.json apps/admin/
COPY packages/api-client/package*.json packages/api-client/
COPY packages/theme/package*.json packages/theme/
RUN npm install

COPY . .
ARG VITE_API_BASE_URL
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
RUN npm run admin:build

FROM nginx:stable-alpine
COPY --from=builder /app/apps/admin/dist /usr/share/nginx/html
EXPOSE 80

Ở đây:

  • useradmin dùng ARG để nhận API URL lúc build
  • api đọc env lúc runtime

Lưu ý rất quan trọng với api:

  • runner chỉ dùng để chạy app production
  • migrator là stage riêng để chạy migration
  • migration không chạy qua runtime container api

7. Tạo .dockerignore

Tạo file .dockerignore ở root:

node_modules
**/node_modules
.git
.next
dist
coverage
*.log
.env
.env.local

Mục tiêu là giảm context khi build Docker.


8. Tạo docker-compose.yml base

Tạo file docker-compose.yml:

services:
  postgres:
    image: postgres:16-alpine
    container_name: remap-postgres
    environment:
      POSTGRES_DB: remap
      POSTGRES_USER: remap
      POSTGRES_PASSWORD: remap_password
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - remap_net

  api:
    build:
      context: .
      dockerfile: apps/api/Dockerfile
    container_name: remap-api
    env_file:
      - apps/api/.env.docker
    depends_on:
      - postgres
    networks:
      - remap_net

  api-migrate:
    build:
      context: .
      dockerfile: apps/api/Dockerfile
      target: migrator
    env_file:
      - apps/api/.env.docker
    depends_on:
      - postgres
    networks:
      - remap_net
    profiles:
      - tools
    working_dir: /app/apps/api
    command: npm run migration:run

  user:
    build:
      context: .
      dockerfile: apps/user/Dockerfile
      args:
        NEXT_PUBLIC_API_BASE_URL: http://localhost:3000/api
    container_name: remap-user
    depends_on:
      - api
    networks:
      - remap_net

  admin:
    build:
      context: .
      dockerfile: apps/admin/Dockerfile
      args:
        VITE_API_BASE_URL: http://localhost:3000/api
    container_name: remap-admin
    depends_on:
      - api
    networks:
      - remap_net

volumes:
  postgres_data:

networks:
  remap_net:

File này là base dùng được cho cả local lẫn VPS.

Lưu ý:

  • service api dùng để chạy NestJS app
  • service api-migrate dùng để chạy migration theo yêu cầu
  • api-migrate không tự chạy khi docker compose up -d

9. Tạo docker-compose.override.yml cho local

Tạo file docker-compose.override.yml:

services:
  api:
    ports:
      - "3000:3000"

  user:
    ports:
      - "3001:3001"

  admin:
    ports:
      - "3002:80"

File này chỉ để local mở port ra ngoài.


10. Build trên local

Từ root repo:

cd D:\Thiện\remap
docker compose build

Nếu cần build sạch:

docker compose build --no-cache

11. Chạy trên local

docker compose up -d
docker compose ps

Xem log nếu cần:

docker compose logs -f api
docker compose logs -f user
docker compose logs -f admin

12. Chạy migration trên local

docker compose --profile tools run --rm api-migrate
docker compose --profile tools run --rm api-migrate npm run migration:show

Nếu muốn ghi rõ lệnh đầy đủ:

docker compose --profile tools run --rm api-migrate npm run migration:run
docker compose --profile tools run --rm api-migrate npm run migration:show

Vì sao dùng run thay vì exec api:

  • api-migrate được build từ target migrator
  • target này có src và dev dependencies
  • còn container api chỉ là runtime image để chạy app

13. Restore dữ liệu mẫu trên local

Nếu bạn muốn local có sẵn dữ liệu để test UI hoặc API, có thể restore file backup:

apps/api/remap_backup.sql

Lưu ý:

  • file này là full dump PostgreSQL, có cả schema và data
  • vì vậy không restore đè trực tiếp lên database đã có schema
  • cách đúng là xóa database remap, tạo lại, rồi restore file dump

Trình tự:

13.1 Xóa database hiện tại

docker compose exec postgres psql -U remap -d postgres -c "DROP DATABASE remap WITH (FORCE);"

13.2 Tạo lại database rỗng

docker compose exec postgres psql -U remap -d postgres -c "CREATE DATABASE remap OWNER remap;"

13.3 Restore file backup vào database mới

Get-Content apps/api/remap_backup.sql | docker compose exec -T postgres psql -U remap -d remap

13.4 Kiểm tra dữ liệu đã vào hay chưa

docker compose exec postgres psql -U remap -d remap -c "SELECT 'users' AS table, count(*) FROM users UNION ALL SELECT 'properties', count(*) FROM properties UNION ALL SELECT 'property_locations', count(*) FROM property_locations UNION ALL SELECT 'property_images', count(*) FROM property_images UNION ALL SELECT 'favorites', count(*) FROM favorites UNION ALL SELECT 'refresh_tokens', count(*) FROM refresh_tokens UNION ALL SELECT 'typeorm_migrations', count(*) FROM typeorm_migrations;"

Kỳ vọng sẽ thấy các bảng có dữ liệu thay vì 0 rows.

Ghi chú:

  • nếu file dump là bản đã được làm sạch, restore sẽ ít lỗi hơn
  • nếu còn một vài warning liên quan owner/config mà dữ liệu vẫn vào đủ, thường không ảnh hưởng đến local test

14. Verify local

Kiểm tra health API:

curl http://localhost:3000/api/health

Kỳ vọng:

{"status":"ok","service":"api"}

Mở trình duyệt:

http://localhost:3001
http://localhost:3002

Nếu local chưa chạy ổn, dừng ở đây và sửa trước khi nghĩ đến VPS.


15. Commit bộ file Docker vào repo

Sau khi local chạy ổn:

git status
git add .
git commit -m "chore: add docker workflow for local and vps"
git push

Không nên deploy VPS trước khi bộ file Docker đã được commit.


16. Chuẩn bị VPS

SSH vào VPS:

ssh root@YOUR_SERVER_IP

Cài Docker, Compose, Nginx, Certbot:

apt update && apt upgrade -y
apt install -y ca-certificates curl gnupg nginx certbot python3-certbot-nginx
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
docker --version
docker compose version

Mở firewall:

ufw allow OpenSSH
ufw allow 'Nginx Full'
ufw enable
ufw status

17. Clone repo lên VPS

mkdir -p /var/www
cd /var/www
git clone YOUR_GITHUB_REPO_URL remap
cd remap

18. Chuyển sang env production trên VPS

Trên VPS, sửa các file:

  • apps/api/.env.production
  • apps/user/.env.production
  • apps/admin/.env.production

Không nên sửa tay docker-compose.yml base trên VPS.

Cách cụ thể và an toàn hơn là tạo file override riêng cho production:

cd /var/www/remap
nano docker-compose.prod.yml

Nội dung:

services:
  api:
    env_file:
      - apps/api/.env.production
    ports:
      - "127.0.0.1:3000:3000"

  api-migrate:
    env_file:
      - apps/api/.env.production

  user:
    build:
      args:
        NEXT_PUBLIC_API_BASE_URL: https://api.remap.io.vn/api
    ports:
      - "127.0.0.1:3001:3001"

  admin:
    build:
      args:
        VITE_API_BASE_URL: https://api.remap.io.vn/api
    ports:
      - "127.0.0.1:3002:80"

Ý nghĩa:

  • api dùng apps/api/.env.production
  • api-migrate cũng dùng apps/api/.env.production
  • user build với API URL production
  • admin build với API URL production
  • các port chỉ bind vào 127.0.0.1, để chỉ Nginx trên host truy cập được

Kiểm tra lại nội dung 3 file env production trước khi build:

apps/api/.env.production

NODE_ENV=production
API_PORT=3000
API_PREFIX=/api

DATABASE_HOST=postgres
DATABASE_PORT=5432
DATABASE_USER=remap
DATABASE_PASSWORD=CHANGE_ME_DB_PASSWORD
DATABASE_NAME=remap

JWT_ACCESS_SECRET=CHANGE_ME_LONG_RANDOM_ACCESS_SECRET
JWT_REFRESH_SECRET=CHANGE_ME_LONG_RANDOM_REFRESH_SECRET
JWT_ACCESS_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d

CORS_ORIGINS=https://remap.io.vn,https://admin.remap.io.vn

apps/user/.env.production

NEXT_PUBLIC_API_BASE_URL=https://api.remap.io.vn/api

apps/admin/.env.production

VITE_API_BASE_URL=https://api.remap.io.vn/api

Sau bước này, trên VPS sẽ dùng:

docker-compose.yml
docker-compose.prod.yml

19. Build và chạy trên VPS

Từ root repo trên VPS:

cd /var/www/remap
docker compose -f docker-compose.yml -f docker-compose.prod.yml build
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
docker compose -f docker-compose.yml -f docker-compose.prod.yml ps

Chạy migration bằng service api-migrate với env production:

docker compose -f docker-compose.yml -f docker-compose.prod.yml --profile tools run --rm api-migrate
docker compose -f docker-compose.yml -f docker-compose.prod.yml --profile tools run --rm api-migrate npm run migration:show

20. Restore dữ liệu mẫu trên VPS

Bước này chỉ nên dùng khi:

  • bạn đang deploy môi trường staging
  • hoặc bạn chủ động muốn nạp dữ liệu mẫu lên VPS

Không nên restore file backup mẫu lên production thật nếu đó là dữ liệu test hoặc dữ liệu cũ không còn phù hợp.

Nếu vẫn muốn restore, dùng file:

apps/api/remap_backup.sql

Lưu ý:

  • file này là full dump PostgreSQL, có cả schema và data
  • không restore đè trực tiếp lên database đã có schema/data
  • cách đúng là xóa database remap, tạo lại, rồi restore

20.1 Xóa database hiện tại trên VPS

docker compose -f docker-compose.yml -f docker-compose.prod.yml exec postgres psql -U remap -d postgres -c "DROP DATABASE remap WITH (FORCE);"

20.2 Tạo lại database rỗng

docker compose -f docker-compose.yml -f docker-compose.prod.yml exec postgres psql -U remap -d postgres -c "CREATE DATABASE remap OWNER remap;"

20.3 Restore file backup

cat apps/api/remap_backup.sql | docker compose -f docker-compose.yml -f docker-compose.prod.yml exec -T postgres psql -U remap -d remap

20.4 Kiểm tra dữ liệu đã vào

docker compose -f docker-compose.yml -f docker-compose.prod.yml exec postgres psql -U remap -d remap -c "SELECT 'users' AS table, count(*) FROM users UNION ALL SELECT 'properties', count(*) FROM properties UNION ALL SELECT 'property_locations', count(*) FROM property_locations UNION ALL SELECT 'property_images', count(*) FROM property_images UNION ALL SELECT 'favorites', count(*) FROM favorites UNION ALL SELECT 'refresh_tokens', count(*) FROM refresh_tokens UNION ALL SELECT 'typeorm_migrations', count(*) FROM typeorm_migrations;"

Nếu dữ liệu đã restore, bạn sẽ thấy các bảng có số dòng lớn hơn 0.

Nếu sau restore bạn muốn chắc schema mới nhất vẫn đúng, có thể chạy lại:

docker compose -f docker-compose.yml -f docker-compose.prod.yml --profile tools run --rm api-migrate npm run migration:show

21. Tạo Nginx public ra Internet

Nếu muốn public app bằng domain thật, tạo thêm service Nginx trong Docker hoặc dùng Nginx trên host. Cách đơn giản nhất là dùng Nginx trên host để proxy vào container.

Ví dụ local production ports trên VPS:

  • API container map ra 127.0.0.1:3000
  • User container map ra 127.0.0.1:3001
  • Admin container map ra 127.0.0.1:3002

Sau đó tạo file:

nano /etc/nginx/sites-available/remap

Nội dung:

server {
    listen 80;
    server_name api.remap.io.vn;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        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;
    }
}

server {
    listen 80;
    server_name remap.io.vn www.remap.io.vn;

    location / {
        proxy_pass http://127.0.0.1:3001;
        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;
    }
}

server {
    listen 80;
    server_name admin.remap.io.vn;

    location / {
        proxy_pass http://127.0.0.1:3002;
        proxy_http_version 1.1;
        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;
    }
}

Kích hoạt:

ln -s /etc/nginx/sites-available/remap /etc/nginx/sites-enabled/remap
nginx -t
systemctl reload nginx

22. Trỏ DNS về VPS

Tạo các bản ghi:

A   @       YOUR_SERVER_IP
A   www     YOUR_SERVER_IP
A   api     YOUR_SERVER_IP
A   admin   YOUR_SERVER_IP

Kiểm tra:

ping remap.io.vn
ping api.remap.io.vn
ping admin.remap.io.vn

23. Cấp SSL

certbot --nginx \
  -d remap.io.vn \
  -d www.remap.io.vn \
  -d api.remap.io.vn \
  -d admin.remap.io.vn

Test renew:

certbot renew --dry-run

24. Verify production

Kiểm tra:

curl https://api.remap.io.vn/api/health

Mở:

https://remap.io.vn
https://admin.remap.io.vn

25. Quy trình deploy lại sau này

Trên local:

  1. Sửa code.
  2. docker compose build
  3. docker compose up -d
  4. test xong thì commit và push

Trên VPS:

cd /var/www/remap
git pull origin main
docker compose build
docker compose up -d
docker compose --profile tools run --rm api-migrate

Đây là điểm quan trọng nhất của tài liệu này: VPS chỉ lặp lại đúng quy trình đã verify trên local.


26. Checklist hoàn thành

  • Đã tạo Dockerfile cho api, user, admin
  • Đã tạo .dockerignore
  • Đã tạo docker-compose.yml
  • Đã tạo docker-compose.override.yml
  • Đã tạo env Docker local
  • Đã build thành công trên local
  • Đã chạy thành công trên local
  • Đã chạy migration local bằng docker compose --profile tools run --rm api-migrate
  • http://localhost:3000/api/health trả về ok
  • Đã mở được http://localhost:3001
  • Đã mở được http://localhost:3002
  • Đã commit và push bộ file Docker
  • VPS đã cài Docker
  • VPS đã clone đúng repo
  • VPS đã dùng env production
  • VPS đã build và up thành công
  • Đã chạy migration trên VPS bằng docker compose --profile tools run --rm api-migrate
  • Domain đã trỏ đúng về VPS
  • SSL đã cấp thành công
  • https://api.remap.io.vn/api/health trả về ok

27. Ghi chú quan trọng

Tài liệu này là luồng làm việc chuẩn. Để chạy được thực tế, repo vẫn cần có các file Docker thật.

Ngoài ra có một điểm kỹ thuật quan trọng:

  • runtime image của api không phải migration image
  • migration phải chạy qua service api-migrate
  • command chuẩn là docker compose --profile tools run --rm api-migrate

Nếu muốn, bước tiếp theo mình có thể tạo luôn các file đó trong repo để tài liệu này khớp hoàn toàn với cấu hình triển khai thực tế.

Đọc tiếp