Tự Động Deploy React Lên VPS Với Docker Và GitHub Actions
Xin chào mọi người.
Khi deploy một ứng dụng React lên VPS, chúng ta thường phải làm thủ công:
- SSH vào VPS
git pull- Cài dependencies
- Build project
- Restart application hoặc Docker container
Với GitHub Actions + Docker + SSH, toàn bộ quy trình có thể được tự động hóa.
Mỗi lần push code lên main, GitHub Actions sẽ kết nối tới VPS và thực hiện deployment.
Kiến trúc
Developer
↓ git push
GitHub Actions
↓ SSH
VPS
↓
Docker
↓
Nginx
↓
UsersVới React/Vite hoặc TanStack Router:
React
↓
Vite
↓
Bun / npm / pnpm / yarn
↓
dist/
↓
NginxVới Next.js hoặc Astro SSR:
Application
↓
Build
↓
Node.js / Bun
↓
Docker
↓
NginxGitHub Actions
Tạo:
.github/workflows/deploy.ymlVí dụ:
name: Deploy
on:
push:
branches:
- main
workflow_dispatch:
permissions: {}
concurrency:
group: production
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy to VPS
uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USERNAME }}
key: ${{ secrets.SERVER_SSH_KEY }}
fingerprint: ${{ secrets.SERVER_FINGERPRINT }}
command_timeout: 20m
script: |
set -e
cd /var/www/my-app
git pull --ff-only
docker compose up -d --build
docker compose psappleboy/ssh-action cho phép GitHub Actions chạy command trực tiếp trên VPS. set -e giúp dừng deployment khi command bị lỗi. Có thể dùng SSH key và fingerprint để xác thực kết nối.
Docker
Ví dụ React + Vite sử dụng Bun:
FROM oven/bun:1 AS build
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]Đây là multi-stage build: Bun chỉ được sử dụng trong build stage, còn production chỉ cần Nginx và dist/.
Docker Compose
Tạo:
compose.ymlservices:
web:
build:
context: .
dockerfile: Dockerfile
container_name: my-react-app
restart: unless-stopped
ports:
- "127.0.0.1:8080:80"Deployment trên VPS:
docker compose up -d --buildDocker Compose phù hợp để chạy ứng dụng production trên một VPS và cần rebuild image khi source thay đổi.
Nginx cho React SPA
Nếu sử dụng React Router hoặc TanStack Router:
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}SPA cần fallback về index.html để các route như:
/about
/dashboard
/settings
/users/123không trả về 404 khi refresh trực tiếp.
Bun, npm, pnpm và Yarn
Docker không bắt buộc phải sử dụng Bun.
Có thể sử dụng package manager tương ứng với lockfile:
bun.lock → Bun
package-lock.json → npm
pnpm-lock.yaml → pnpm
yarn.lock → YarnVí dụ Bun:
RUN bun install --frozen-lockfile
RUN bun run buildnpm:
RUN npm ci
RUN npm run buildpnpm:
RUN pnpm install --frozen-lockfile
RUN pnpm run buildYarn:
RUN yarn install --immutable
RUN yarn buildKhông nên trộn package manager trong cùng một project nếu không có lý do rõ ràng.
React, TanStack Router, Astro và Next.js
React + Vite
Thông thường là static application:
React → Vite → dist/ → NginxReact + TanStack Router
Vẫn có thể deploy như SPA:
React → TanStack Router → Vite → dist/ → NginxNhưng phải có SPA fallback trên server.
Astro
Astro static có thể deploy giống React:
Astro → build → dist/ → NginxNếu dùng SSR/server output thì cần runtime server.
Next.js
Next.js không nên mặc định deploy giống React/Vite.
Có thể chạy:
Next.js → next build → Node.js → Docker → Nginxhoặc dùng static export nếu ứng dụng phù hợp với static hosting.
GitHub Secrets
Không hardcode thông tin SSH trong workflow.
Tạo các secrets:
SERVER_HOST
SERVER_USERNAME
SERVER_SSH_KEY
SERVER_FINGERPRINTSau đó:
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USERNAME }}
key: ${{ secrets.SERVER_SSH_KEY }}
fingerprint: ${{ secrets.SERVER_FINGERPRINT }}Nếu repository private và VPS sử dụng git pull, VPS cũng cần quyền đọc repository, ví dụ thông qua GitHub Deploy Key.
Environment Variables
Cần phân biệt biến dùng lúc build và biến dùng lúc runtime.
Ví dụ frontend:
VITE_API_URLđược đưa vào client bundle và không nên chứa secret.
Các giá trị như:
DATABASE_URL
JWT_SECRET
PRIVATE_API_KEYkhông được đưa vào frontend.
Với server-side application, secret nên được cung cấp cho container ở runtime.
Health Check
docker compose up -d không có nghĩa application chắc chắn hoạt động.
Có thể kiểm tra:
docker compose ps
curl --fail http://127.0.0.1:8080/Trong workflow:
script: |
set -e
cd /var/www/my-app
git pull --ff-only
docker compose up -d --build
docker compose ps
curl --fail --silent --show-error http://127.0.0.1:8080/Nếu health check thất bại, GitHub Actions sẽ đánh dấu deployment là failed.
Test trước khi Deploy
Flow tốt hơn:
Pull Request
↓
Lint
↓
Test
↓
Build
↓
Merge
↓
DeployKhông nên để code chưa được kiểm tra đi thẳng vào production.
Khi project lớn hơn
Cách đơn giản:
GitHub Actions
↓
SSH
↓
git pull
↓
Docker build
↓
Docker ComposeCó thể nâng cấp thành:
GitHub Actions
↓
Test
↓
Docker Build
↓
GHCR
↓
VPS
↓
docker pull
↓
Docker ComposeKhi đó VPS không cần build source code nữa, mà chỉ pull Docker image đã được build từ CI.
Kết luận
Một mô hình đơn giản cho React là:
React / Vite
↓
Bun / npm / pnpm / yarn
↓
Docker
↓
Nginx
↓
VPSDeployment:
git push
↓
GitHub Actions
↓
SSH
↓
Docker
↓
ProductionMô hình này cũng có thể mở rộng cho TanStack Router, Next.js, Astro và các framework JavaScript/TypeScript khác.
Điểm quan trọng là xác định application là static hay server-side, sau đó chọn runtime, Docker image và Nginx configuration phù hợp.