Tự Động Deploy React Lên VPS Với Docker Và GitHub Actions


Tuan Duc Tran

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
   ↓
Users

Với React/Vite hoặc TanStack Router:

React
   ↓
Vite
   ↓
Bun / npm / pnpm / yarn
   ↓
dist/
   ↓
Nginx

Với Next.js hoặc Astro SSR:

Application
   ↓
Build
   ↓
Node.js / Bun
   ↓
Docker
   ↓
Nginx

GitHub Actions

Tạo:

.github/workflows/deploy.yml

Ví 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 ps

appleboy/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.yml
services:
  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 --build

Docker 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/123

khô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         → Yarn

Ví dụ Bun:

RUN bun install --frozen-lockfile
RUN bun run build

npm:

RUN npm ci
RUN npm run build

pnpm:

RUN pnpm install --frozen-lockfile
RUN pnpm run build

Yarn:

RUN yarn install --immutable
RUN yarn build

Khô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/ → Nginx

React + TanStack Router

Vẫn có thể deploy như SPA:

React → TanStack Router → Vite → dist/ → Nginx

Nhưng phải có SPA fallback trên server.

Astro

Astro static có thể deploy giống React:

Astro → build → dist/ → Nginx

Nế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 → Nginx

hoặ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_FINGERPRINT

Sau đó:

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_KEY

khô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
   ↓
Deploy

Khô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 Compose

Có thể nâng cấp thành:

GitHub Actions
   ↓
Test
   ↓
Docker Build
   ↓
GHCR
   ↓
VPS
   ↓
docker pull
   ↓
Docker Compose

Khi đó 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
      ↓
VPS

Deployment:

git push
   ↓
GitHub Actions
   ↓
SSH
   ↓
Docker
   ↓
Production

Mô 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.