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.
Mục lục bài viết
VPS & Hosting cho dự án của bạn
Khám phá các gói máy chủ và lưu trữ phù hợp từ website cá nhân đến ứng dụng production.
Tài liệu này dành cho luồng làm việc đúng mục tiêu hiện tại:
- Tạo file Docker trong repo.
- Build và chạy thử toàn bộ trên local.
- Xác nhận local chạy ổn.
- Đem nguyên bộ file đó lên VPS.
- 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/Dockerfileapps/user/Dockerfileapps/admin/Dockerfile.dockerignoredocker-compose.ymldocker-compose.override.ymldeploy/nginx/default.conf
Ý nghĩa:
docker-compose.yml: cấu hình base dùng chung cho local và VPSdocker-compose.override.yml: cấu hình local, tự được Compose nạp thêmdeploy/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=postgresvì 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 ý:
uservàadminđọ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:
uservàadmindùngARGđể nhận API URL lúc buildapiđọc env lúc runtime
Lưu ý rất quan trọng với api:
runnerchỉ dùng để chạy app productionmigratorlà 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
apidùng để chạy NestJS app - service
api-migratedùng để chạy migration theo yêu cầu api-migratekhông tự chạy khidocker 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ừ targetmigrator- target này có
srcvà dev dependencies - còn container
apichỉ 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.productionapps/user/.env.productionapps/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:
apidùngapps/api/.env.productionapi-migratecũng dùngapps/api/.env.productionuserbuild với API URL productionadminbuild 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:
- Sửa code.
docker compose builddocker compose up -d- 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/healthtrả 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/healthtrả 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
apikhô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
Bài viết liên quan
Hướng dẫn deploy REMAP bằng Docker lên VPS
Triển khai monorepo REMAP gồm NestJS, Next.js, React và PostgreSQL lên VPS Ubuntu bằng Docker Compose, Nginx, domain và SSL.
Đọc bài →Docker là gì? Image, container, volume và Docker Compose
Hướng dẫn Docker từ cơ bản: container hoạt động thế nào, image và container khác gì, Dockerfile, volume, network và Docker Compose.
Đọc bài →Docker Compose production: App, PostgreSQL, healthcheck và backup
Hướng dẫn thiết kế Docker Compose production với app, PostgreSQL, network, volume, healthcheck, secret, restart và deploy an toàn.
Đọc bài →