diff --git a/.github/workflows/backend-cd.yml b/.github/workflows/backend-cd.yml index e1330816..821836c5 100644 --- a/.github/workflows/backend-cd.yml +++ b/.github/workflows/backend-cd.yml @@ -48,6 +48,15 @@ jobs: echo "=== Initial disk space ===" df -h + - name: Upload nginx config to server + uses: appleboy/scp-action@v0.1.7 + with: + host: ${{ secrets.LIGHTSAIL_HOST }} + username: ${{ secrets.LIGHTSAIL_USER }} + key: ${{ secrets.LIGHTSAIL_SSH_KEY }} + source: "backend/nginx/nginx.conf,backend/nginx/conf.d/cors-map.conf" + target: "/tmp/nginx-deploy" + - name: Deploy via SSH uses: appleboy/ssh-action@v1.1.0 with: @@ -63,6 +72,57 @@ jobs: IMAGE=$IMAGE_NAME SHA=${{ github.sha }} + # ================================================================= + # BƯỚC 0: Apply nginx config (TRƯỚC khi restart container) + # - Đã SCP file nginx lên /tmp/nginx-deploy/ ở step trước + # - Tự rollback nếu nginx -t fail + # ================================================================= + echo "=== [0/10] Apply nginx config from git ===" + if [ -f /tmp/nginx-deploy/nginx.conf ] && [ -f /tmp/nginx-deploy/conf.d/cors-map.conf ]; then + NGINX_TS=$(date +"%Y%m%d_%H%M%S") + + # 1. Backup config hiện tại + if [ -f /etc/nginx/conf.d/cors-map.conf ]; then + sudo cp /etc/nginx/conf.d/cors-map.conf \ + /etc/nginx/conf.d/cors-map.conf.bak.$NGINX_TS + fi + if [ -f /etc/nginx/nginx.conf ] && [ ! -f /etc/nginx/nginx.conf.original.backup ]; then + sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.original.backup + fi + if [ -f /etc/nginx/nginx.conf ]; then + sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak.$NGINX_TS + fi + + # 2. Copy file mới + sudo cp /tmp/nginx-deploy/nginx.conf /etc/nginx/nginx.conf + sudo mkdir -p /etc/nginx/conf.d + sudo cp /tmp/nginx-deploy/conf.d/cors-map.conf /etc/nginx/conf.d/cors-map.conf + + # 3. Test syntax - FAIL thì rollback NGAY, không restart container + if sudo nginx -t 2>&1; then + # 4. Reload nginx (zero downtime) + sudo systemctl reload nginx + echo " ✓ Nginx config applied and reloaded" + else + echo ">>> FATAL: nginx -t failed, rolling back nginx config..." + if [ -f /etc/nginx/conf.d/cors-map.conf.bak.$NGINX_TS ]; then + sudo cp /etc/nginx/conf.d/cors-map.conf.bak.$NGINX_TS \ + /etc/nginx/conf.d/cors-map.conf + fi + if [ -f /etc/nginx/nginx.conf.bak.$NGINX_TS ]; then + sudo cp /etc/nginx/nginx.conf.bak.$NGINX_TS \ + /etc/nginx/nginx.conf + fi + echo ">>> ABORTING: Nginx config rolled back, deployment stopped" + exit 1 + fi + + # Cleanup tmp + rm -rf /tmp/nginx-deploy + else + echo " ⊘ No nginx files uploaded (skip nginx deploy)" + fi + echo "=== Cleanup disk space - Before ===" sudo docker system prune -af --volumes || true echo "=== Disk space after cleanup ===" diff --git a/.gitignore b/.gitignore index a6809a73..290d8069 100644 --- a/.gitignore +++ b/.gitignore @@ -503,3 +503,43 @@ monitoring/nginx/.htpasswd # User configuration files backend/MenuGreen.API/appsettings.json backend/MenuGreen.API/appsettings.Development.json + +# ─── Secrets & deployment keys (NEVER commit) ─────────────────── +# SSL certificates (Let's Encrypt / self-signed) +**/*.pem +**/*.key +**/*.crt +**/*.cer +# Keep nginx/ssl/ folder tracked via .gitkeep but exclude actual keys +backend/nginx/ssl/fullchain.pem +backend/nginx/ssl/privkey.pem +backend/nginx/ssl/*.pem +backend/nginx/ssl/*.key + +# Android signing & API keys +frontend/android/app/keystore_pass.txt +frontend/android/app/*.jks +frontend/android/app/*.keystore +frontend/android/app/google-services.json +frontend/firebase/service-account*.json + +# Flutter build artifacts (do NOT push) +frontend/app-release.aab +frontend/app-debug.apk +frontend/build/app/outputs/**/*.aab +frontend/build/app/outputs/**/*.apk +frontend/build/ +frontend/.dart_tool/ +frontend/.flutter-plugins +frontend/.flutter-plugins-dependencies +frontend/.packages +frontend/pubspec.lock + +# Generated platforms (avoid noisy diffs) +frontend/macos/Flutter/GeneratedPluginRegistrant.swift +frontend/windows/flutter/GeneratedPluginRegistrant.cpp +frontend/linux/flutter/GeneratedPluginRegistrant.cc + +# Local helper scripts (keystore generators, screenshot tools, etc.) +# Keep code in scripts/ and ci/ directories; ignore ad-hoc assets/ +/assets/ diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 00000000..77bd2029 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,47 @@ +# Git +.git +.gitignore + +# IDE +.vs/ +.vscode/ +.idea/ +*.suo +*.user + +# Build outputs +**/bin/ +**/obj/ +**/out/ + +# Docker +Dockerfile +docker-compose*.yml +docker-compose*.yaml +.docker/ + +# Node (frontend) +node_modules/ + +# Logs +**/*.log +logs/ + +# OS +.DS_Store +Thumbs.db + +# Certificates (these are mounted as volumes or generated at runtime) +# ssl/ + +# Secrets (these are passed via .env or environment variables) +.env +.env.* +!.env.example + +# Database migrations backup +**/Migrations/*.backup.sql + +# Test results +TestResults/ +coverage/ diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 00000000..621329ba --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,45 @@ +# ============================================== +# MenuGreen Docker Environment Configuration +# ============================================== +# Copy file: cp .env.example .env +# Edit .env với giá trị thực của bạn + +# ============================================== +# PostgreSQL Database +# ============================================== +POSTGRES_DB=MenuGreenDb +POSTGRES_USER=postgres +POSTGRES_PASSWORD=ChangeMe123! +POSTGRES_PORT=5432 + +# ============================================== +# JWT Authentication +# ============================================== +JWT_SECRET_KEY=YourSuperSecretKeyHere_ChangeMeInProduction_MinLength32Chars! +JWT_ISSUER=MenuGreenAPI +JWT_AUDIENCE=MenuGreenApp + +# ============================================== +# Allowed CORS Origins (comma-separated) +# ============================================== +# Mặc định đã có: admin.menugreen.food, www.menugreen.food, menugreen.food +# Thêm domain mới bằng cách uncomment và thêm vào đây +ALLOWED_ORIGINS=https://admin.menugreen.food,https://www.menugreen.food,https://menugreen.food + +# ============================================== +# Firebase Cloud Messaging (Optional) +# ============================================== +# Đường dẫn đến Firebase service account JSON file +# Đặt file trong thư mục backend/credentials/ +# FIREBASE_CREDENTIAL_PATH=credentials/firebase-service-account.json + +# ============================================== +# Redis Cache (Optional) +# ============================================== +# Nếu không có Redis, app sẽ dùng In-Memory cache +REDIS_CONNECTION_STRING=redis://localhost:6379 + +# ============================================== +# API Port +# ============================================== +API_PORT=5000 diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 00000000..d5e30222 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,38 @@ +# Stage 1: Build +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +WORKDIR /src + +# Copy csproj files and restore dependencies +COPY ["MenuGreen.API/MenuGreen.API.csproj", "MenuGreen.API/"] +COPY ["MenuGreen.BusinessLogicLayer/MenuGreen.BusinessLogicLayer.csproj", "MenuGreen.BusinessLogicLayer/"] +COPY ["MenuGreen.DataAccessLayer/MenuGreen.DataAccessLayer.csproj", "MenuGreen.DataAccessLayer/"] +RUN dotnet restore "MenuGreen.API/MenuGreen.API.csproj" + +# Copy everything else and build +COPY . . +WORKDIR "/src/MenuGreen.API" +RUN dotnet build "MenuGreen.API.csproj" -c Release -o /app/build + +# Stage 2: Publish +FROM build AS publish +RUN dotnet publish "MenuGreen.API.csproj" -c Release -o /app/publish /p:UseAppHost=false + +# Stage 3: Final runtime image +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS final +WORKDIR /app + +# Install curl for health checks +RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* + +# Copy published app +COPY --from=publish /app/publish . + +# Expose port (Render injects PORT env) +EXPOSE 5000 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ + CMD curl -f http://localhost:5000/health/live || exit 1 + +# Run the app +ENTRYPOINT ["dotnet", "MenuGreen.API.dll"] diff --git a/backend/README-DOCKER.md b/backend/README-DOCKER.md new file mode 100644 index 00000000..421f7e4a --- /dev/null +++ b/backend/README-DOCKER.md @@ -0,0 +1,239 @@ +# MenuGreen Docker Setup + +## Cấu trúc thư mục + +``` +backend/ +├── docker-compose.yml # Main compose file (nginx + api + postgres) +├── Dockerfile # Multi-stage build cho .NET API +├── .env.example # Template biến môi trường +├── .dockerignore # Ignore file cho Docker build +├── nginx/ +│ ├── nginx.conf # Main nginx config +│ ├── conf.d/ +│ │ └── cors-map.conf # CORS whitelist - THÊM DOMAIN MỚI Ở ĐÂY +│ └── ssl/ +│ └── .gitkeep # Placeholder cho SSL certificates +├── MenuGreen.API/ # ASP.NET Core Web API +├── MenuGreen.BusinessLogicLayer/ +├── MenuGreen.DataAccessLayer/ +├── database/ +│ ├── seeddata.sql # Seed data +│ └── MenuGreen_AI_SeedData/ # AI seed data (thư mục) +└── migrations/ # EF Core migrations (tự động apply) +``` + +## Quick Start + +### 1. Setup Environment + +```bash +# Copy environment file +cp .env.example .env + +# Edit .env với giá trị thực +nano .env +``` + +### 2. Build và Run + +```bash +# Build images +docker-compose build + +# Run tất cả services (nginx + api + postgres) +docker-compose up -d + +# Xem logs +docker-compose logs -f + +# Xem logs của một service cụ thể +docker-compose logs -f api +docker-compose logs -f nginx +docker-compose logs -f postgres +``` + +### 3. Verify + +```bash +# Kiểm tra containers đang chạy +docker-compose ps + +# Test CORS headers +curl -I -X OPTIONS http://localhost/api/Auth/login \ + -H "Origin: https://admin.menugreen.food" \ + -H "Access-Control-Request-Method: POST" + +# Test health check +curl http://localhost/health/live +``` + +## Commands Cheatsheet + +```bash +# Start services +docker-compose up -d + +# Stop services +docker-compose down + +# Stop + xóa volumes (CLEAN RESET) +docker-compose down -v + +# Rebuild sau khi sửa code +docker-compose up -d --build + +# Rebuild không cache +docker-compose build --no-cache + +# Restart một service cụ thể +docker-compose restart api +docker-compose restart nginx + +# Shell vào container +docker exec -it menugreen-api /bin/sh +docker exec -it menugreen-postgres psql -U postgres -d MenuGreenDb + +# Xem resource usage +docker stats + +# Xem logs tất cả +docker-compose logs -f --tail=100 +``` + +## Thêm Domain Mới vào CORS + +Mở file `nginx/conf.d/cors-map.conf`: + +```nginx +map $http_origin $cors_origin { + default ""; + + # === PRODUCTION === + "https://www.menugreen.food" "https://www.menugreen.food"; + "https://menugreen.food" "https://menugreen.food"; + "https://admin.menugreen.food" "https://admin.menugreen.food"; + + # === THÊM DOMAIN MỚI Ở ĐÂY === + "https://staging.menugreen.food" "https://staging.menugreen.food"; + + # === LOCALHOST === + "http://localhost:3000" "http://localhost:3000"; +} +``` + +Sau đó restart nginx: +```bash +docker-compose restart nginx +``` + +## Deploy lên Production Server + +### Trên server (Lightsail): + +```bash +# 1. Clone/pull code +cd /opt/menugreen +git pull origin main + +# 2. Copy và edit .env +cp .env.example .env +nano .env +# Điền: JWT_SECRET_KEY, Firebase credentials, Redis URL + +# 3. Build và start +docker-compose build --no-cache +docker-compose up -d + +# 4. Verify +curl -I http://localhost/api/Auth/login \ + -H "Origin: https://admin.menugreen.food" \ + -H "Access-Control-Request-Method: POST" +``` + +### Backup Database + +```bash +# Backup +docker exec menugreen-postgres pg_dump -U postgres MenuGreenDb > backup_$(date +%Y%m%d_%H%M%S).sql + +# Restore +docker exec -i menugreen-postgres psql -U postgres MenuGreenDb < backup_file.sql +``` + +## Troubleshooting + +### 1. Nginx không start được + +```bash +# Check nginx logs +docker-compose logs nginx + +# Test config trong container +docker exec menugreen-nginx nginx -t + +# Xem full config +docker exec menugreen-nginx nginx -T +``` + +### 2. API không kết nối được database + +```bash +# Kiểm tra postgres health +docker-compose ps postgres + +# Test connection từ API +docker exec menugreen-api curl -f http://localhost:5000/health/ready + +# Check connection string +docker exec menugreen-api env | grep ConnectionStrings +``` + +### 3. CORS vẫn lỗi sau khi thêm domain + +1. Restart nginx: `docker-compose restart nginx` +2. Clear browser cache (DevTools → Network → Disable cache) +3. Verify config đã được mount đúng: + ```bash + docker exec menugreen-nginx cat /etc/nginx/conf.d/cors-map.conf + ``` + +### 4. Clean Reset (xoá hết và bắt đầu lại) + +```bash +# DANGER: Xóa toàn bộ data! +docker-compose down -v --rmi all +docker system prune -f + +# Rebuild và chạy lại +docker-compose up -d --build +``` + +## Environment Variables + +| Variable | Default | Mô tả | +|---|---|---| +| `POSTGRES_DB` | `MenuGreenDb` | Tên database | +| `POSTGRES_USER` | `postgres` | PostgreSQL username | +| `POSTGRES_PASSWORD` | `postgres` | PostgreSQL password | +| `POSTGRES_PORT` | `5432` | PostgreSQL port | +| `JWT_SECRET_KEY` | (required) | JWT secret key (min 32 chars) | +| `JWT_ISSUER` | `MenuGreenAPI` | JWT issuer | +| `JWT_AUDIENCE` | `MenuGreenApp` | JWT audience | +| `API_PORT` | `5000` | API internal port | +| `FIREBASE_CREDENTIAL_PATH` | (optional) | Path to Firebase JSON | +| `REDIS_CONNECTION_STRING` | (optional) | Redis connection string | +| `ALLOWED_ORIGINS` | (default domains) | Additional CORS origins | + +## SSL/HTTPS Setup + +1. Place certificates in `nginx/ssl/`: + - `fullchain.pem` - Certificate + intermediates + - `privkey.pem` - Private key + +2. Uncomment SSL section in `nginx/nginx.conf` + +3. Restart nginx: + ```bash + docker-compose restart nginx + ``` diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml new file mode 100644 index 00000000..7723bdf8 --- /dev/null +++ b/backend/docker-compose.yml @@ -0,0 +1,86 @@ +version: '3.8' + +services: + # PostgreSQL Database + postgres: + image: postgres:16-alpine + container_name: menugreen-postgres + restart: unless-stopped + environment: + POSTGRES_DB: ${POSTGRES_DB:-MenuGreenDb} + POSTGRES_USER: ${POSTGRES_USER:-postgres} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} + ports: + - "${POSTGRES_PORT:-5432}:5432" + volumes: + - postgres-data:/var/lib/postgresql/data + - ./database/seeddata.sql:/docker-entrypoint-initdb.d/01-seeddata.sql:ro + - ./database/MenuGreen_AI_SeedData:/docker-entrypoint-initdb.d/02-ai-seeddata:ro + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-MenuGreenDb}"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - menugreen-network + + # Backend API + api: + build: + context: . + dockerfile: Dockerfile + container_name: menugreen-api + restart: unless-stopped + ports: + - "${API_PORT:-5000}:5000" + environment: + - ASPNETCORE_ENVIRONMENT=Production + - ASPNETCORE_URLS=http://0.0.0.0:5000 + - ConnectionStrings__DefaultConnection=Host=postgres;Port=5432;Database=${POSTGRES_DB:-MenuGreenDb};Username=${POSTGRES_USER:-postgres};Password=${POSTGRES_PASSWORD:-postgres} + - JwtSettings__SecretKey=${JWT_SECRET_KEY} + - JwtSettings__Issuer=${JWT_ISSUER:-MenuGreenAPI} + - JwtSettings__Audience=${JWT_AUDIENCE:-MenuGreenApp} + - Firebase__CredentialPath=${FIREBASE_CREDENTIAL_PATH:-} + - Redis__ConnectionString=${REDIS_CONNECTION_STRING:-} + - ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-*} + depends_on: + postgres: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:5000/health/live || exit 1"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 20s + networks: + - menugreen-network + + # Nginx Reverse Proxy + nginx: + image: nginx:1.25-alpine + container_name: menugreen-nginx + restart: unless-stopped + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./nginx/conf.d:/etc/nginx/conf.d:ro + - ./nginx/snippets:/etc/nginx/snippets:ro + - ./nginx/ssl:/etc/nginx/ssl:ro + - nginx-logs:/var/log/nginx + depends_on: + api: + condition: service_healthy + networks: + - menugreen-network + +volumes: + postgres-data: + driver: local + nginx-logs: + driver: local + +networks: + menugreen-network: + driver: bridge diff --git a/backend/nginx/conf.d/cors-map.conf b/backend/nginx/conf.d/cors-map.conf new file mode 100644 index 00000000..c71c13f1 --- /dev/null +++ b/backend/nginx/conf.d/cors-map.conf @@ -0,0 +1,32 @@ +# CORS Origin Map - Whitelist các domain được phép +# Include trong http {} context nên đặt ở conf.d/ +# NOTE: map_hash_bucket_size 128 đã set ở nginx.conf (đủ cho domain dài) + +map $http_origin $cors_origin { + default ""; + + # ============================================= + # PRODUCTION DOMAINS - Thêm domain mới ở đây + # ============================================= + + "https://www.menugreen.food" "https://www.menugreen.food"; + "https://menugreen.food" "https://menugreen.food"; + "https://admin.menugreen.food" "https://admin.menugreen.food"; + + # ============================================= + # VERCEL / STAGING DOMAINS + # ============================================= + + "https://menu-green-system-ldw5frytu-johnny-dangs-projects.vercel.app" + "https://menu-green-system-ldw5frytu-johnny-dangs-projects.vercel.app"; + + # ============================================= + # LOCALHOST - Development + # ============================================= + + "http://localhost:3000" "http://localhost:3000"; + "http://localhost:3001" "http://localhost:3001"; + "http://localhost:5173" "http://localhost:5173"; + "http://127.0.0.1:3000" "http://127.0.0.1:3000"; + "http://127.0.0.1:5173" "http://127.0.0.1:5173"; +} diff --git a/backend/nginx/deploy/INTEGRATION.md b/backend/nginx/deploy/INTEGRATION.md new file mode 100644 index 00000000..9957a58b --- /dev/null +++ b/backend/nginx/deploy/INTEGRATION.md @@ -0,0 +1,202 @@ +# Nginx + CI/CD Integrated Workflow + +> **Last updated:** 2026-07-11 — Workflow mới: Nginx config apply TỰ ĐỘNG qua CD workflow. + +--- + +## Tổng quan + +Trước đây: Sửa `backend/nginx/conf.d/cors-map.conf` → push git → **không có gì xảy ra** → phải SSH lên server chạy `deploy-nginx.sh` thủ công. + +Bây giờ: Sửa file nginx → push git → **CI/CD tự động apply lên server** (cùng lúc deploy Docker image). + +``` +git push origin main + ↓ +┌──────────────────────────────────────────────────┐ +│ GitHub Actions │ +│ │ +│ backend-ci.yml │ +│ └─ Build + Push Docker image │ +│ │ +│ backend-cd.yml │ +│ ├─ 1. SCP file nginx lên server │ +│ ├─ 2. Apply nginx config (auto rollback) │ ← MỚI +│ ├─ 3. Download Doppler secrets │ +│ ├─ 4. Backup RDS │ +│ ├─ 5. Pull + tag Docker image │ +│ ├─ 6. Restart container │ +│ ├─ 7. Health check (auto rollback nếu fail) │ +│ └─ 8. Cleanup │ +└──────────────────────────────────────────────────┘ +``` + +--- + +## Những gì đã fix (để CI/CD + Nginx hoạt động cùng nhau) + +### Fix 1: `nginx.conf` — Upstream trỏ vào host thay vì Docker DNS + +**Trước (SAI):** +```nginx +upstream api_backend { + server api:5000; # Docker DNS - chỉ work trong docker network + keepalive 32; +} +``` + +Nginx chạy trên HOST, không trong Docker → không resolve được `api`. + +**Sau (ĐÚNG):** +```nginx +upstream api_backend { + server 127.0.0.1:5000; # API container publish port ra host + keepalive 32; +} +``` + +File: `backend/nginx/nginx.conf` line 47-52. + +### Fix 2: `backend-cd.yml` — Thêm 2 step mới + +**Step mới 1:** SCP file nginx lên server +```yaml +- name: Upload nginx config to server + uses: appleboy/scp-action@v0.1.7 + with: + host: ${{ secrets.LIGHTSAIL_HOST }} + username: ${{ secrets.LIGHTSAIL_USER }} + key: ${{ secrets.LIGHTSAIL_SSH_KEY }} + source: "backend/nginx/nginx.conf,backend/nginx/conf.d/cors-map.conf" + target: "/tmp/nginx-deploy" +``` + +**Step mới 2:** Apply nginx (TRƯỚC khi restart container) +- Backup config hiện tại (`.bak.YYYYMMDD_HHMMSS`) +- Copy file mới vào `/etc/nginx/` +- Test syntax (`nginx -t`) +- Fail → auto rollback → abort deploy +- Pass → reload nginx (zero downtime) + +--- + +## Deploy Flow chi tiết (sau khi tích hợp) + +```bash +# Dev sửa CORS trên local +code backend/nginx/conf.d/cors-map.conf + +# Commit + push +git add backend/nginx/ +git commit -m "feat(nginx): add staging.menugreen.food to CORS" +git push origin main +``` + +**GitHub Actions tự động:** + +``` +backend-ci.yml + └─ Build image → push Docker Hub (3-5 phút) + +backend-cd.yml (trigger tự động) + ├─ Checkout code + ├─ Check disk space + ├─ SCP file nginx → /tmp/nginx-deploy/ ← MỚI + ├─ SSH vào server: + │ ├─ [0/10] Apply nginx config ← MỚI (auto rollback nếu fail) + │ ├─ [1/10] Cleanup Docker + │ ├─ [2/10] Decode docker-compose.prod.yml + │ ├─ [3/10] Install Doppler CLI + │ ├─ [4/10] Download Doppler secrets → build .env + │ ├─ [5/10] Backup RDS (pg_dump) + │ ├─ [6/10] Pull + tag image + │ ├─ [7/10] Stop old container → start new + │ ├─ [8/10] Verify DB tables + │ ├─ [9/10] Health check /health/ready (30 retries) + │ └─ [10/10] Prune old Docker images + └─ Done (5-8 phút) +``` + +--- + +## Khi nào cần chạy `deploy-nginx.sh` thủ công? + +Script `backend/nginx/deploy/deploy-nginx.sh` **vẫn còn trong repo** cho 2 trường hợp: + +1. **Test nginx config trên server local** (không cần trigger CD workflow) +2. **Sửa nginx NHANH khi CI/CD đang fail** (emergency fix) + +**Cách dùng:** +```bash +ssh -i ~/LightsailDefaultKeyPair.pem ubuntu@52.77.218.100 +cd ~/apps/MenuGreenSystem # cần clone repo (CD workflow không clone) +sudo ./backend/nginx/deploy/deploy-nginx.sh +``` + +> **Lưu ý:** Server hiện tại **KHÔNG clone repo** vì CD workflow tự quản lý image. Nếu muốn dùng `deploy-nginx.sh`, phải clone repo trước (xem `lightsail-setup.md`). + +--- + +## Rollback + +### Auto rollback nginx (trong workflow) + +Khi `nginx -t` fail → workflow tự restore `.bak.YYYYMMDD_HHMMSS` → abort deploy. + +### Auto rollback container (trong workflow) + +Khi health check fail 30 lần (60s) → workflow tự restore image `:previous`. + +### Manual rollback (nếu cần) + +```bash +ssh ubuntu@52.77.218.100 + +# Xem các backup +ls -t /etc/nginx/conf.d/cors-map.conf.bak.* + +# Restore nginx config +sudo cp /etc/nginx/conf.d/cors-map.conf.bak.20260711_143000 \ + /etc/nginx/conf.d/cors-map.conf +sudo nginx -t && sudo systemctl reload nginx + +# Restore Docker image (nếu cần) +sudo docker pull anhtuan21112004/menugreensystem:previous +sudo docker tag anhtuan21112004/menugreensystem:previous menugreen_api +cd /home/ubuntu/apps/menugreen +docker compose -f docker-compose.prod.yml up -d +``` + +--- + +## Verify sau deploy + +```bash +# Test Nginx serve đúng config mới +curl -I -X OPTIONS https://api.menugreen.food/api/Auth/login \ + -H "Origin: https://staging.menugreen.food" \ + -H "Access-Control-Request-Method: POST" +# → access-control-allow-origin: https://staging.menugreen.food + +# Test API vẫn hoạt động +curl https://api.menugreen.food/health/live + +# Verify nginx config hiện tại trên server +ssh ubuntu@52.77.218.100 "sudo cat /etc/nginx/conf.d/cors-map.conf" +``` + +--- + +## Files liên quan + +| File | Vai trò | +|---|---| +| `backend/nginx/nginx.conf` | Main nginx config (đã fix upstream → 127.0.0.1) | +| `backend/nginx/conf.d/cors-map.conf` | CORS whitelist | +| `.github/workflows/backend-cd.yml` | CD workflow (đã thêm 2 step nginx) | +| `backend/nginx/deploy/deploy-nginx.sh` | Manual deploy script (fallback) | +| `backend/nginx/deploy/setup-server.sh` | Setup ban đầu (chạy 1 lần) | + +--- + +*Last updated: 2026-07-11 — CI/CD tự động apply nginx config* \ No newline at end of file diff --git a/backend/nginx/deploy/README.md b/backend/nginx/deploy/README.md new file mode 100644 index 00000000..b45de260 --- /dev/null +++ b/backend/nginx/deploy/README.md @@ -0,0 +1,157 @@ +# MenuGreen Nginx - Deployment Scripts + +Nginx chạy trực tiếp trên host (không Docker) để tiết kiệm RAM trên server 2GB. + +> **Last updated:** 2026-07-11 — Workflow hiện tại đã tự động hóa hoàn toàn qua CI/CD. Các script trong folder này chỉ dùng cho **manual fallback** khi cần debug. + +--- + +## Tại sao nginx không chạy trong Docker? + +- Server chỉ có 2GB RAM +- API container: ~800MB | Redis: ~256MB | OS: ~700MB +- Còn ~240MB, đủ cho nginx trên host + +--- + +## Cấu trúc folder + +``` +backend/nginx/ +├── nginx.conf # Main config (include các file dưới) +├── conf.d/ +│ └── cors-map.conf # Whitelist CORS origins +└── deploy/ # Folder này (scripts + docs) + ├── setup-server.sh # Cài nginx lần đầu (chạy 1 lần) + ├── deploy-nginx.sh # Apply config mới từ git (manual) + ├── INTEGRATION.md # Tài liệu tích hợp với CI/CD + └── README.md # File này +``` + +--- + +## Workflow hiện tại (2026-07-11) — TỰ ĐỘNG qua CI/CD + +### Khi sửa `cors-map.conf` hoặc `nginx.conf`: + +```bash +# 1. Sửa file trên local +code backend/nginx/conf.d/cors-map.conf + +# 2. Commit + push +git add backend/nginx/ +git commit -m "feat(nginx): add staging domain to CORS" +git push origin main +``` + +**CI/CD sẽ TỰ ĐỘNG:** +1. SCP file nginx lên server +2. Backup config hiện tại (timestamped) +3. Apply config mới +4. Test syntax (`nginx -t`) → rollback nếu fail +5. Reload nginx (zero downtime) + +**Không cần SSH lên server.** Không cần chạy script thủ công. + +Chi tiết workflow: xem [`INTEGRATION.md`](./INTEGRATION.md) và [`../../docs/01-deployment/CI_CD.md`](../../docs/01-deployment/CI_CD.md). + +--- + +## Manual fallback (chỉ dùng khi cần debug) + +### Setup nginx lần đầu (chạy 1 lần duy nhất trên server mới) + +```bash +ssh ubuntu@52.77.218.100 + +# Cài nginx + certbot +sudo apt update +sudo apt install -y nginx certbot python3-certbot-nginx +sudo mkdir -p /etc/nginx/snippets + +# Apply config lần đầu +cd ~/apps/MenuGreenSystem +sudo ./backend/nginx/deploy/setup-server.sh +``` + +### Apply config mới (manual, không qua CI/CD) + +Dùng khi: +- CD workflow fail +- Cần test nhanh không muốn đợi CI/CD +- Debug trên server trực tiếp + +```bash +ssh ubuntu@52.77.218.100 +cd ~/apps/MenuGreenSystem +git pull origin main +sudo ./backend/nginx/deploy/deploy-nginx.sh +``` + +Script sẽ tự động: +1. Backup config hiện tại (timestamped) +2. Copy file mới vào `/etc/nginx/` +3. Test syntax (`nginx -t`) → rollback nếu fail +4. Reload nginx (zero downtime) +5. Cleanup backup cũ (giữ 10 file mới nhất) + +### Rollback khi lỗi + +**Cách 1: Restore từ backup có sẵn** + +```bash +ssh ubuntu@52.77.218.100 +ls -la /etc/nginx/conf.d/cors-map.conf.bak.* +# Chọn backup muốn restore, ví dụ: +sudo cp /etc/nginx/conf.d/cors-map.conf.bak.20260711_143000 \ + /etc/nginx/conf.d/cors-map.conf +sudo nginx -t && sudo systemctl reload nginx +``` + +**Cách 2: Rollback về version cũ trong git** + +```bash +ssh ubuntu@52.77.218.100 +cd ~/apps/MenuGreenSystem +git log --oneline backend/nginx/ +git checkout -- backend/nginx/ +sudo ./backend/nginx/deploy/deploy-nginx.sh +``` + +--- + +## Verify sau khi deploy + +```bash +# Test CORS preflight +curl -I -X OPTIONS https://api.menugreen.food/api/Auth/login \ + -H "Origin: https://www.menugreen.food" \ + -H "Access-Control-Request-Method: POST" + +# Test health check +curl https://api.menugreen.food/health/live + +# Xem logs nginx (real-time) +sudo tail -f /var/log/nginx/access.log +sudo tail -f /var/log/nginx/error.log +``` + +--- + +## Các lệnh hay dùng + +```bash +sudo nginx -t # Test config không reload +sudo systemctl reload nginx # Reload nginx (zero downtime) +sudo systemctl restart nginx # Restart nginx (downtime ngắn) +sudo systemctl status nginx # Xem status +sudo cat /etc/nginx/conf.d/cors-map.conf # Xem config hiện tại +``` + +--- + +## Related docs + +- [`INTEGRATION.md`](./INTEGRATION.md) — Cách CI/CD workflow apply Nginx tự động +- [`../../docs/01-deployment/CI_CD.md`](../../docs/01-deployment/CI_CD.md) — CI/CD pipeline chi tiết +- [`../../docs/01-deployment/NGINX_AND_CORS.md`](../../docs/01-deployment/NGINX_AND_CORS.md) — CORS config + Nginx architecture \ No newline at end of file diff --git a/backend/nginx/deploy/deploy-nginx.sh b/backend/nginx/deploy/deploy-nginx.sh new file mode 100644 index 00000000..cbd7a464 --- /dev/null +++ b/backend/nginx/deploy/deploy-nginx.sh @@ -0,0 +1,189 @@ +#!/bin/bash +# ============================================================= +# Apply nginx config mới từ git lên server +# Chạy mỗi khi sửa file trong backend/nginx/ +# +# Usage: +# sudo ./deploy-nginx.sh +# +# Workflow (phải làm trước khi chạy script này): +# 1. Sửa file trong backend/nginx/ trên local +# 2. git add . && git commit -m "..." && git push +# 3. Trên server: cd ~/apps/MenuGreenSystem && git pull +# 4. sudo ./backend/nginx/deploy/deploy-nginx.sh +# ============================================================= + +set -euo pipefail + +NGINX_SOURCE_DIR="$(cd "$(dirname "$0")/.." && pwd)" +TIMESTAMP=$(date +"%Y%m%d_%H%M%S") + +# Màu sắc cho dễ đọc +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +log_info() { echo -e "${GREEN}[$(date +'%H:%M:%S')]${NC} $*"; } +log_warn() { echo -e "${YELLOW}[$(date +'%H:%M:%S')] WARNING:${NC} $*"; } +log_error() { echo -e "${RED}[$(date +'%H:%M:%S')] ERROR:${NC} $*"; } + +# Check quyền root +if [ "$EUID" -ne 0 ]; then + log_error "Script cần chạy với sudo!" + echo "Usage: sudo $0" + exit 1 +fi + +log_info "==========================================" +log_info "MenuGreen Nginx Deploy" +log_info "==========================================" +log_info "Source: $NGINX_SOURCE_DIR" +log_info "Timestamp: $TIMESTAMP" +echo "" + +# ===================================================== +# BƯỚC 1: Verify file nguồn tồn tại +# ===================================================== +log_info "[1/5] Verifying source files..." + +REQUIRED_FILES=( + "$NGINX_SOURCE_DIR/nginx.conf" + "$NGINX_SOURCE_DIR/conf.d/cors-map.conf" +) + +for file in "${REQUIRED_FILES[@]}"; do + if [ ! -f "$file" ]; then + log_error "Source file không tồn tại: $file" + exit 1 + fi + log_info " ✓ Found: $(basename "$file")" +done + +# ===================================================== +# BƯỚC 2: Backup config hiện tại +# ===================================================== +log_info "" +log_info "[2/5] Backing up current config..." + +if [ -f /etc/nginx/conf.d/cors-map.conf ]; then + sudo cp /etc/nginx/conf.d/cors-map.conf \ + /etc/nginx/conf.d/cors-map.conf.bak.$TIMESTAMP + log_info " ✓ Backed up cors-map.conf → .bak.$TIMESTAMP" +fi + +if [ -f /etc/nginx/nginx.conf ]; then + # Backup main config (nếu chưa backup lần đầu) + if [ ! -f /etc/nginx/nginx.conf.original.backup ]; then + sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.original.backup + log_info " ✓ Saved original nginx.conf backup (first time)" + fi + sudo cp /etc/nginx/nginx.conf \ + /etc/nginx/nginx.conf.bak.$TIMESTAMP + log_info " ✓ Backed up nginx.conf → .bak.$TIMESTAMP" +fi + +# ===================================================== +# BƯỚC 3: Copy file mới vào /etc/nginx/ +# ===================================================== +log_info "" +log_info "[3/5] Copying new config..." + +sudo cp "$NGINX_SOURCE_DIR/nginx.conf" /etc/nginx/nginx.conf +log_info " ✓ Copied nginx.conf" + +sudo cp "$NGINX_SOURCE_DIR/conf.d/cors-map.conf" /etc/nginx/conf.d/cors-map.conf +log_info " ✓ Copied cors-map.conf" + +# ===================================================== +# BƯỚC 4: Test config (QUAN TRỌNG - phải pass trước khi reload) +# ===================================================== +log_info "" +log_info "[4/5] Testing config syntax..." + +if sudo nginx -t 2>&1 | tee /tmp/nginx-test.log; then + log_info " ✓ Config syntax OK" +else + log_error "Config syntax FAILED - rolling back!" + log_error "" + log_error "Test output:" + cat /tmp/nginx-test.log | sed 's/^/ /' + log_error "" + log_warn "Restoring from backup..." + + if [ -f /etc/nginx/conf.d/cors-map.conf.bak.$TIMESTAMP ]; then + sudo cp /etc/nginx/conf.d/cors-map.conf.bak.$TIMESTAMP \ + /etc/nginx/conf.d/cors-map.conf + fi + + if [ -f /etc/nginx/nginx.conf.bak.$TIMESTAMP ]; then + sudo cp /etc/nginx/nginx.conf.bak.$TIMESTAMP \ + /etc/nginx/nginx.conf + fi + + log_warn "Rolled back. Nginx config unchanged." + exit 1 +fi + +# ===================================================== +# BƯỚC 5: Reload nginx (zero-downtime) +# ===================================================== +log_info "" +log_info "[5/5] Reloading nginx..." + +if sudo systemctl reload nginx; then + log_info " ✓ Nginx reloaded successfully" +else + log_error "Failed to reload nginx!" + exit 1 +fi + +# ===================================================== +# Verify sau khi reload +# ===================================================== +sleep 2 + +# Check nginx đang chạy +if systemctl is-active --quiet nginx; then + log_info " ✓ Nginx is running" +else + log_error "Nginx is not running!" + exit 1 +fi + +# Check ports +if ss -tlnp | grep -q ':80 '; then + log_info " ✓ Listening on port 80" +else + log_warn "Not listening on port 80" +fi + +# ===================================================== +# Cleanup old backups (giữ lại 10 file mới nhất) +# ===================================================== +log_info "" +log_info "🧹 Cleaning up old backups (keeping 10 most recent)..." + +# Cleanup cors-map backups +ls -t /etc/nginx/conf.d/cors-map.conf.bak.* 2>/dev/null | tail -n +11 | xargs -r rm -f + +# Cleanup nginx.conf backups (giữ original.backup) +ls -t /etc/nginx/nginx.conf.bak.* 2>/dev/null | tail -n +11 | xargs -r rm -f + +log_info " ✓ Old backups removed" + +# ===================================================== +# Done +# ===================================================== +echo "" +log_info "==========================================" +log_info "✅ Deploy complete!" +log_info "==========================================" +echo "" +echo "📊 Quick verify:" +echo " curl -I https://api.menugreen.food/health/live" +echo "" +echo "📜 Backup location:" +echo " /etc/nginx/conf.d/cors-map.conf.bak.$TIMESTAMP" +echo " /etc/nginx/nginx.conf.bak.$TIMESTAMP" +echo "" diff --git a/backend/nginx/deploy/setup-server.sh b/backend/nginx/deploy/setup-server.sh new file mode 100644 index 00000000..d84f3e09 --- /dev/null +++ b/backend/nginx/deploy/setup-server.sh @@ -0,0 +1,70 @@ +#!/bin/bash +# ============================================================= +# Setup nginx trên server (chạy 1 LẦN DUY NHẤT) +# Sau khi chạy xong, không cần chạy lại - dùng deploy-nginx.sh +# ============================================================= + +set -e + +NGINX_SOURCE_DIR="$(cd "$(dirname "$0")/.." && pwd)" + +echo "==========================================" +echo "Setup nginx for MenuGreen" +echo "==========================================" +echo "Source: $NGINX_SOURCE_DIR" +echo "" + +# 1. Backup config hiện tại của nginx (nếu có) +echo "📦 [1/5] Backing up existing nginx config..." +if [ -f /etc/nginx/nginx.conf ] && [ ! -f /etc/nginx/nginx.conf.original.backup ]; then + sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.original.backup + echo " ✓ Backup saved to /etc/nginx/nginx.conf.original.backup" +else + echo " ⊘ No existing nginx.conf or already backed up, skipping" +fi + +# 2. Copy main config +echo "" +echo "📋 [2/5] Installing main nginx config..." +sudo cp "$NGINX_SOURCE_DIR/nginx.conf" /etc/nginx/nginx.conf +echo " ✓ Installed nginx.conf" + +# 3. Setup conf.d folder + copy cors-map +echo "" +echo "📋 [3/5] Setting up conf.d/..." +sudo mkdir -p /etc/nginx/conf.d +sudo cp "$NGINX_SOURCE_DIR/conf.d/cors-map.conf" /etc/nginx/conf.d/cors-map.conf +echo " ✓ Installed cors-map.conf" + +# 4. Setup snippets folder +echo "" +echo "📋 [4/5] Setting up snippets/ (empty for now)..." +sudo mkdir -p /etc/nginx/snippets +echo " ✓ Snippets folder ready" + +# 5. Test config + enable nginx +echo "" +echo "🧪 [5/5] Testing config..." +sudo nginx -t + +echo "" +echo "🚀 Enabling and starting nginx..." +sudo systemctl enable nginx +sudo systemctl restart nginx + +echo "" +echo "==========================================" +echo "✅ Setup complete!" +echo "==========================================" +echo "" +echo "Next steps:" +echo " 1. Setup SSL (Let's Encrypt):" +echo " sudo apt install -y certbot python3-certbot-nginx" +echo " sudo certbot --nginx -d api.menugreen.food" +echo "" +echo " 2. Verify nginx is running:" +echo " sudo systemctl status nginx" +echo "" +echo " 3. Test:" +echo " curl http://api.menugreen.food/health/live" +echo "" diff --git a/backend/nginx/nginx.conf b/backend/nginx/nginx.conf new file mode 100644 index 00000000..09840923 --- /dev/null +++ b/backend/nginx/nginx.conf @@ -0,0 +1,172 @@ +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for" ' + 'origin=$http_origin'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + + # Important: must be larger than longest domain in cors-map.conf + map_hash_bucket_size 128; + + # CORS origin map (whitelist-based for security) + include /etc/nginx/conf.d/cors-map.conf; + + # Rate limiting zones + limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; + limit_req_zone $binary_remote_addr zone=auth:1r/m; + limit_conn_zone $binary_remote_addr zone=addr:10m; + + # Upstream backend API + # NOTE: Nginx chạy trên HOST (không trong Docker), nên trỏ vào localhost:5000 + # (API container publish port 5000 ra host qua docker-compose.prod.yml) + upstream api_backend { + server 127.0.0.1:5000; + keepalive 32; + } + + # HTTP server - redirect to HTTPS + # server { + # listen 80; + # server_name _; + # return 301 https://$host$request_uri; + # } + + # HTTPS server + server { + listen 80; + server_name api.menugreen.food; + + # SSL Certificate + # ssl_certificate /etc/nginx/ssl/fullchain.pem; + # ssl_certificate_key /etc/nginx/ssl/privkey.pem; + # ssl_protocols TLSv1.2 TLSv1.3; + # ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; + # ssl_prefer_server_ciphers off; + # ssl_session_cache shared:SSL:10m; + # ssl_session_timeout 1d; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # CORS headers using dynamic map + add_header Access-Control-Allow-Origin "$cors_origin" always; + add_header Access-Control-Allow-Credentials "true" always; + add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS, PATCH" always; + add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,X-Api-Key" always; + add_header Access-Control-Expose-Headers "Content-Length,Content-Range,X-Request-Id" always; + add_header Access-Control-Max-Age "86400" always; + + # Rate limiting + limit_req zone=api burst=20 nodelay; + limit_conn addr 10; + + # Proxy to backend API + location / { + proxy_pass http://api_backend; + 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; + proxy_set_header X-Origin $http_origin; + proxy_set_header Connection ""; + + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + + # For large file uploads + client_max_body_size 50m; + + # Disable buffering for streaming responses + proxy_buffering off; + } + + # Health check endpoints (no CORS needed) + location = /health { + proxy_pass http://api_backend/health; + proxy_http_version 1.1; + proxy_set_header Host $host; + add_header Content-Type application/json; + add_header Access-Control-Allow-Origin "*"; + return 200 '{"status":"Healthy"}'; + } + + location = /health/ready { + proxy_pass http://api_backend/health/ready; + proxy_http_version 1.1; + proxy_set_header Host $host; + add_header Content-Type application/json; + add_header Access-Control-Allow-Origin "*"; + } + + location = /health/live { + add_header Content-Type application/json; + add_header Access-Control-Allow-Origin "*"; + return 200 '{"status":"Live"}'; + } + + # Preflight CORS (OPTIONS) + if ($request_method = 'OPTIONS') { + return 204; + } + + # Prometheus metrics + location = /metrics { + proxy_pass http://api_backend/metrics; + proxy_http_version 1.1; + proxy_set_header Host $host; + add_header Content-Type text/plain; + } + + # Swagger UI + location /swagger { + proxy_pass http://api_backend; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + add_header Content-Type text/plain; + return 500 '{"error":"Internal Server Error","message":"Please try again later"}'; + } + } + + # Catch-all HTTP -> HTTPS redirect (uncomment when SSL is configured) + # server { + # listen 443 ssl; + # server_name _; + # return 301 https://$host$request_uri; + # } +} diff --git a/backend/nginx/ssl/.gitkeep b/backend/nginx/ssl/.gitkeep new file mode 100644 index 00000000..f8904454 --- /dev/null +++ b/backend/nginx/ssl/.gitkeep @@ -0,0 +1,12 @@ +# Place SSL certificates here +# For production, you can use Let's Encrypt: +# certbot --nginx -d api.menugreen.food -d admin.menugreen.food -d www.menugreen.food + +# Required files: +# - fullchain.pem (certificate + intermediates) +# - privkey.pem (private key) + +# To generate self-signed certs for testing: +# openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ +# -keyout privkey.pem -out fullchain.pem \ +# -subj "/CN=api.menugreen.food" diff --git a/docs/00-overview/PROJECT_STATUS.md b/docs/00-overview/PROJECT_STATUS.md index 4106c234..9c451093 100644 --- a/docs/00-overview/PROJECT_STATUS.md +++ b/docs/00-overview/PROJECT_STATUS.md @@ -184,7 +184,7 @@ docs/ │ ├── SPEC.md │ ├── README.md │ └── PROJECT_STATUS.md -├── 01-deployment/ # Deployment guides (DEPLOY, DEPLOY_FIX_PLAN, DEPLOY_REVIEW, DOPPLER_SETUP, CI_CD, lightsail-setup) +├── 01-deployment/ # Deployment guides (ARCHITECTURE, CI_CD, NGINX_AND_CORS, SECRETS_MANAGEMENT, SERVER_SETUP) ├── 02-backend/ # Backend models documentation, BE overview ├── 03-features-overview/ # features/README, AI report │ ├── README.md @@ -214,7 +214,7 @@ docs/ │ ├── SPEC.md │ ├── README.md │ └── PROJECT_STATUS.md -├── 01-deployment/ # Deployment guides (DEPLOY, DEPLOY_FIX_PLAN, DEPLOY_REVIEW, DOPPLER_SETUP, CI_CD, lightsail-setup) +├── 01-deployment/ # Deployment guides (ARCHITECTURE, CI_CD, NGINX_AND_CORS, SECRETS_MANAGEMENT, SERVER_SETUP) ├── 02-backend/ # Backend models documentation, BE overview ├── 03-features-overview/ # features/README, AI report │ ├── README.md diff --git a/docs/00-overview/README.md b/docs/00-overview/README.md index 7c6a1d83..9fbe08b3 100644 --- a/docs/00-overview/README.md +++ b/docs/00-overview/README.md @@ -14,11 +14,12 @@ || Document | Description | ||----------|-------------| +|| **[ARCHITECTURE.md](../01-deployment/ARCHITECTURE.md)** | Kiến trúc tổng quan, GitHub Secrets, Server Info | || **[CI_CD.md](../01-deployment/CI_CD.md)** | CI/CD pipeline, deploy, rollback | -|| **[lightsail-setup.md](../01-deployment/lightsail-setup.md)** | Server setup guide | +|| **[SERVER_SETUP.md](../01-deployment/SERVER_SETUP.md)** | Server setup guide | +|| **[SECRETS_MANAGEMENT.md](../01-deployment/SECRETS_MANAGEMENT.md)** | Quản lý secrets (Doppler) | +|| **[NGINX_AND_CORS.md](../01-deployment/NGINX_AND_CORS.md)** | CORS & Nginx configuration | || **[GITHUB_SECRETS_SETUP.md](../GITHUB_SECRETS_SETUP.md)** | GitHub secrets configuration | -|| **[cors-config.md](../01-deployment/cors-config.md)** | CORS configuration guide | -|| **[01-deployment/](../01-deployment/)** | Deployment guides (DEPLOY, DEPLOY_FIX_PLAN, DEPLOY_REVIEW, DOPPLER_SETUP) | --- @@ -88,4 +89,4 @@ cd ~/apps/MenuGreenSystem --- -*Last updated: 2026-07-09 — Restructured docs: 6 folders, 35 files.* +*Last updated: 2026-07-11 — Reorganized 01-deployment/ docs (8 → 6 files, archive removed).* diff --git a/docs/01-deployment/ARCHITECTURE.md b/docs/01-deployment/ARCHITECTURE.md new file mode 100644 index 00000000..5dc00463 --- /dev/null +++ b/docs/01-deployment/ARCHITECTURE.md @@ -0,0 +1,247 @@ +# MenuGreen System - Architecture Overview + +> **Last updated:** 2026-07-11 — Phản ánh đúng trạng thái hiện tại của hệ thống. + +--- + +## Mục tiêu + +- Deploy backend .NET API lên **AWS Lightsail Ubuntu** sử dụng **GitHub Actions CI/CD**. +- **PostgreSQL** chạy trên **AWS RDS** (bên ngoài server). +- **Redis** chạy trên **AWS** (ElastiCache hoặc managed) — không phải Docker container local. +- API chạy trong **Docker container** duy nhất (`menugreen_api`). +- Nginx chạy **trong Docker** local (`docker-compose.yml`) cho dev — production dùng **Nginx trên host** (đã cài ở `/etc/nginx/`) và được **deploy tự động qua CI/CD** từ source trong git. +- **Tự động hóa hoàn toàn**: push code lên `main` → CI/CD tự deploy (cả API + Nginx config). + +--- + +## Kiến trúc Production + +``` +GitHub Repository (main branch) + │ + ▼ +┌─────────────────────────────────────────┐ +│ GitHub Actions CI/CD │ +│ ┌──────────────┐ ┌────────────────┐ │ +│ │ backend-ci │→ │ backend-cd │ │ +│ │ Build + Push │ │ Deploy to │ │ +│ │ Docker image │ │ Lightsail │ │ +│ └──────────────┘ │ (API + Nginx) │ │ +│ └────────────────┘ │ +└─────────────────────────────────────────┘ + │ │ + │ Docker Hub │ SSH (appleboy/scp-action + ssh-action) + ▼ ▼ +Docker Hub: AWS Lightsail (Ubuntu 22.04) +anhtuan21112004/ 52.77.218.100 +menugreensystem:main /home/ubuntu/apps/menugreen + │ + ├─ Docker container: menugreen_api (port 5000) + ├─ Nginx (host, port 80/443) → proxy → 5000 + │ └─ Source từ backend/nginx/ (deploy tự động) + │ + ▼ + AWS RDS PostgreSQL + menugreen-db.cr4uo6sksium.ap-southeast-1.rds.amazonaws.com:5432 + Database: menugreendb + │ + ▼ + Redis (managed) + REDIS_URL từ Doppler secrets +``` + +--- + +## Services + +| Service | Location | Port | Notes | +|---------------|-----------------------|------|----------------------------------------------------------------| +| **API** | Docker container | 5000 | `menugreen_api`, image từ Docker Hub | +| **Nginx** | Host (Ubuntu) | 80, 443 | Reverse proxy + SSL termination + CORS. Config trong `/etc/nginx/` | +| **PostgreSQL** | AWS RDS | 5432 | Bên ngoài, security group allow IP Lightsail | +| **Redis** | Managed (AWS/Doppler) | 6379 | Connection string từ `REDIS_URL` env, không phải Docker local | + +--- + +## App directory trên server + +``` +/home/ubuntu/apps/menugreen/ +├── .env # Tạo tự động bởi CI/CD từ Doppler +├── docker-compose.prod.yml # Base64 embedded trong workflow (NO REDIS) +└── (không cần clone repo trên server — CD tự quản lý image) + +/etc/nginx/ # Nginx trên host (KHÔNG trong Docker) +├── nginx.conf # Source: backend/nginx/nginx.conf (apply qua CI/CD) +├── conf.d/ +│ ├── cors-map.conf # Source: backend/nginx/conf.d/cors-map.conf +│ └── *.bak.YYYYMMDD_HHMMSS # Auto backup mỗi lần apply +└── ssl/ # SSL certs (Let's Encrypt) +``` + +> **Lưu ý:** Server **KHÔNG cần clone repo**. Image pull thẳng từ Docker Hub. CD workflow tự tạo `.env` từ Doppler VÀ tự apply nginx config từ source trong git. + +--- + +## CI/CD Workflows + +| File | Mục đích | Trigger | +|-----------------------------|----------------------------------------------|--------------------------| +| `backend-ci.yml` | Build + Test + Push Docker image | Push/PR vào `main` | +| `backend-cd.yml` | Deploy lên AWS Lightsail (API + Nginx) | Sau khi CI pass + manual | + +Chi tiết pipeline: xem [CI_CD.md](./CI_CD.md). + +### Trigger deploy + +- **Tự động:** CI pass trên nhánh `main` → CD trigger +- **Manual:** Workflow Dispatch với 2 option `production` / `staging` +- **Skip:** Commit message chứa `#skipdeploy` + +--- + +## GitHub Secrets (bắt buộc) + +| Secret Name | Giá trị | Ghi chú | +|------------------------|--------------------------------------------------|------------------------------------------| +| `DOPPLER_TOKEN` | `dp.prd.xxx...` | Service token Doppler config `prd` | +| `LIGHTSAIL_HOST` | `52.77.218.100` | IP server | +| `LIGHTSAIL_USER` | `ubuntu` | SSH username | +| `LIGHTSAIL_SSH_KEY` | (paste nội dung file `.pem`) | Toàn bộ file, bao gồm `BEGIN`/`END` | +| `DOCKERHUB_USERNAME` | `anhtuan21112004` | Docker Hub account | +| `DOCKERHUB_TOKEN` | (Docker Hub access token) | Read + Write để push image | + +Vào **GitHub** → Repository → **Settings** → **Secrets and variables** → **Actions** → **New repository secret**. + +### Doppler secrets (config `prd`) + +App đọc các biến từ Doppler (đã chuẩn hóa dạng `Foo__Bar=value` cho .NET): + +| Secret | Mục đích | +|-----------------------------------------------------|-------------------------------------------| +| `CONNECTIONSTRINGS__DEFAULTCONNECTION` | Full connection string PostgreSQL | +| `REDIS_HOST`, `REDIS_PORT`, `REDIS_PASSWORD` | Redis connection (sẽ ghép thành REDIS_URL)| +| `JWT_SECRET`, `JWT_ISSUER`, `JWT_AUDIENCE` | JWT config | +| `JWTSETTINGS__SECRETKEY` (alternative) | Alternative JWT secret key | +| `ALLOWEDORIGINS` | Domain CORS cho phép | +| `RESEND__APIKEY`, `RESEND__FROMEMAIL`, `RESEND__FROMNAME` | Email service | +| `SEPAY__*` | Payment gateway (SePay VN) | +| `FIREBASE__CREDENTIALPATH` | Firebase FCM credential | +| `CVSERVICE__BASEURL`, `CVSERVICE__APISECRETKEY` | Computer Vision microservice | +| `NUTRITIONASSISTANT__WORKERURL` | Nutrition AI worker | +| `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD`, `DB_SSL_MODE` | RDS info (cho backup script) | + +Chi tiết xem: [SECRETS_MANAGEMENT.md](./SECRETS_MANAGEMENT.md) + +--- + +## Server Information + +| Property | Value | +|--------------------|-------------------------------------------------------------| +| **Provider** | AWS Lightsail | +| **Plan** | Small ($10/mo) - 2GB RAM | +| **Public IP** | `52.77.218.100` | +| **OS** | Ubuntu 22.04 LTS | +| **Domain** | `https://api.menugreen.food` | +| **App directory** | `/home/ubuntu/apps/menugreen` | +| **Container** | `menugreen_api` (port 5000) | +| **Docker Image** | `docker.io/anhtuan21112004/menugreensystem:main` | +| **API Port** | 5000 (chỉ internal) | +| **Database** | AWS RDS PostgreSQL | +| **Redis** | Managed (kết nối qua REDIS_URL) | +| **Nginx** | Trên host (`/etc/nginx/`) | +| **SSL** | Let's Encrypt (auto-renew) | + +--- + +## Docker Compose Production (embedded base64) + +`docker-compose.prod.yml` được **embedded base64 trong workflow**, không nằm trong repo. Decode ra: + +```yaml +services: + api: + image: docker.io/anhtuan21112004/menugreensystem:latest + container_name: menugreen_api + pull_policy: always + env_file: + - .env + environment: + - ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT} + - ASPNETCORE_URLS=http://+:5000 + ports: + - "5000:5000" + networks: + - menugreen-net + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:5000/health/live"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + deploy: + resources: + limits: + memory: 800M + cpus: '1.0' + volumes: [] + +networks: + menugreen-net: + external: true +``` + +> **Lưu ý:** Compose này **CHỈ** có service `api` — không có Redis (Redis là managed service, connection string từ `REDIS_URL` env). + +--- + +## Redis Configuration + +- **Loại:** Managed (AWS ElastiCache hoặc Upstash), connection string dạng `host:port,password=xxx` +- **Config:** Truyền qua env `REDIS_URL` (CI/CD build từ Doppler `REDIS_HOST` + `REDIS_PORT` + `REDIS_PASSWORD`) +- **Trong code:** `Program.cs` đọc `Redis:ConnectionString` → fallback `REDIS_URL` env + +--- + +## Local Development (docker-compose.yml) + +Cho dev local, dùng file `MenuGreenSystem/backend/docker-compose.yml`: + +- **postgres**: PostgreSQL 16-alpine, port 5432, seed data từ `database/` +- **api**: .NET API, port 5000, depends_on postgres +- **nginx**: Reverse proxy, ports 80/443, depends_on api + +```bash +cd MenuGreenSystem/backend +docker compose up -d + +# Verify +curl http://localhost/health/live +curl http://localhost/api/... +``` + +--- + +## Lưu ý quan trọng + +- **Không commit** `.env` vào git (đã có `.gitignore`). +- **PostgreSQL KHÔNG chạy trong Docker production** (dùng RDS). +- **Redis KHÔNG chạy trong Docker production** (managed). +- **Nginx KHÔNG nằm trong Docker Image** — nó chạy trên host, source config trong `backend/nginx/`. Khi sửa nginx chỉ cần `git push` → CI/CD tự SCP + apply (5-8 phút, không cần build lại Docker image). +- **Server KHÔNG cần clone repo** — GitHub Actions runner (cloud) làm trung gian. +- **CI/CD tự động deploy khi push lên `main`**. Nhánh `Tuan` đã bỏ. +- **`docker-compose.prod.yml` được nhúng base64 trong workflow**, không cần file riêng trong repo. +- **App tự chạy migration khi startup** (không cần efbundle bên ngoài). +- **Auto-rollback** nếu health check fail 30 lần (~60s). + +--- + +## Tài liệu liên quan + +- [CI_CD.md](./CI_CD.md) — Chi tiết về CI/CD pipeline (13 bước deploy) +- [NGINX_AND_CORS.md](./NGINX_AND_CORS.md) — CORS & Nginx config +- [SECRETS_MANAGEMENT.md](./SECRETS_MANAGEMENT.md) — Quản lý secrets qua Doppler +- [SERVER_SETUP.md](./SERVER_SETUP.md) — Hướng dẫn setup server từ đầu \ No newline at end of file diff --git a/docs/01-deployment/CI_CD.md b/docs/01-deployment/CI_CD.md index 46c79091..db2c48f0 100644 --- a/docs/01-deployment/CI_CD.md +++ b/docs/01-deployment/CI_CD.md @@ -1,408 +1,421 @@ # CI/CD Pipeline Guide - MenuGreen System -**Last updated:** 2026-07-09 +> **Last updated:** 2026-07-11 — Phản ánh workflow hiện tại (backend-ci.yml + backend-cd.yml). +> +> **Kiến trúc tổng quan + GitHub Secrets + Server Info + docker-compose.prod.yml:** xem [ARCHITECTURE.md](./ARCHITECTURE.md). --- -## Overview - -MenuGreen sử dụng GitHub Actions để tự động hóa CI/CD pipeline: -- Build & Test .NET -- Build Docker image -- Deploy lên AWS Lightsail - ---- - -## Pipeline Flow +## Pipeline Flow (hiện tại) ``` -Git Push → GitHub Actions → Docker Hub → Server Deploy - │ - ▼ - ┌─────────────────┐ - │ 1. SSH to Server│ - │ 2. Pull .env │ - │ from Doppler │ - └────────┬────────┘ - │ - ▼ - ┌─────────────────┐ - │ 3. Backup DB │ ← pg_dump - └────────┬────────┘ - │ - ▼ - ┌─────────────────┐ - │ 4. Pull Image │ - └────────┬────────┘ - │ - ▼ - ┌─────────────────┐ - │ 5. EF Migration │ - └────────┬────────┘ - │ - ▼ - ┌─────────────────┐ - │ 6. Health Check │ - └─────────────────┘ +┌──────────────────────┐ +│ Developer │ +│ git push origin main│ +└──────────┬───────────┘ + │ + ▼ +┌──────────────────────────────────────────┐ +│ backend-ci.yml (Build & Push) │ +│ ┌────────────────────────────────────┐ │ +│ │ 1. Checkout │ │ +│ │ 2. Setup .NET 9.0 │ │ +│ │ 3. Restore + Build │ │ +│ │ 4. (Optional) Run tests │ │ +│ │ 5. Docker login (Docker Hub) │ │ +│ │ 6. Build image → push :main + :sha │ │ +│ └────────────────────────────────────┘ │ +└──────────┬───────────────────────────────┘ + │ workflow_run completed + ▼ +┌──────────────────────────────────────────┐ +│ backend-cd.yml (Deploy) │ +│ ┌────────────────────────────────────┐ │ +│ │ 1. Checkout │ │ +│ │ 2. Check disk space │ │ +│ │ 3. SCP nginx files → server │ │ +│ │ (nginx.conf + cors-map.conf) │ │ +│ │ 4. SSH → Lightsail │ │ +│ │ 5. Apply nginx config (FIRST!) │ │ +│ │ ├─ Backup → Copy → nginx -t │ │ +│ │ ├─ PASS: reload nginx │ │ +│ │ └─ FAIL: restore + abort │ │ +│ │ 6. Cleanup old Docker resources │ │ +│ │ 7. Decode docker-compose.prod.yml │ │ ← base64 embedded +│ │ 8. Install Doppler CLI (if needed) │ │ +│ │ 9. Doppler secrets → .env │ │ +│ │10. Backup RDS (pg_dump) │ │ +│ │11. Tag :main → :previous │ │ +│ │12. Pull :main │ │ +│ │13. Stop old container │ │ +│ │14. Up new container │ │ +│ │15. Verify tables exist in DB │ │ +│ │16. Health check /health/ready │ │ +│ │ └─ FAIL → Auto rollback │ │ +│ │17. Prune old Docker images │ │ +│ └────────────────────────────────────┘ │ +└──────────────────────────────────────────┘ + │ + ▼ + API live at: + https://api.menugreen.food ``` ---- - -## GitHub Secrets +> **Lưu ý quan trọng:** Nginx được apply **TRƯỚC** khi restart container. Nếu nginx syntax fail → restore backup + abort toàn bộ deploy → container KHÔNG bị restart → zero downtime. -### Required - -| Secret | Description | Example | -|--------|-------------|---------| -| `DOPPLER_TOKEN` | Doppler production config token | `dp.prd.xxx` | -| `LIGHTSAIL_HOST` | Server IP | `52.77.218.100` | -| `LIGHTSAIL_USER` | SSH user | `ubuntu` | -| `LIGHTSAIL_SSH_KEY` | SSH private key | `-----BEGIN...` | +--- -### Optional +## GitHub Secrets (bắt buộc) -| Secret | Description | -|--------|-------------| -| `DOCKERHUB_USERNAME` | `anhtuan21112004` | -| `DOCKERHUB_TOKEN` | Docker Hub access token | +| Secret | Description | Example | +|-----------------------|--------------------------------------------|---------------------------------| +| `DOPPLER_TOKEN` | Doppler service token (config `prd`) | `dp.prd.xxx...` | +| `LIGHTSAIL_HOST` | Server IP | `52.77.218.100` | +| `LIGHTSAIL_USER` | SSH username | `ubuntu` | +| `LIGHTSAIL_SSH_KEY` | SSH private key (.pem full content) | `-----BEGIN...` | +| `DOCKERHUB_USERNAME` | Docker Hub account | `anhtuan21112004` | +| `DOCKERHUB_TOKEN` | Docker Hub access token (Read+Write) | `dckr_pat_xxx...` | -### Setup Guide +Vào **GitHub** → Repository → **Settings** → **Secrets and variables** → **Actions** → **New repository secret**. -See: [GITHUB_SECRETS_SETUP.md](./GITHUB_SECRETS_SETUP.md) +> **Chi tiết secrets (kèm Doppler config `prd`):** xem [ARCHITECTURE.md](./ARCHITECTURE.md#github-secrets-bắt-buộc). --- -## Server Information +## Workflow files chi tiết -| Property | Value | -|----------|-------| -| **SSH** | `ssh -i ~/LightsailDefaultKeyPair.pem ubuntu@52.77.218.100` | -| **App Location** | `~/apps/MenuGreenSystem` | -| **Docker Image** | `anhtuan21112004/menugreensystem:latest` | -| **API Port** | 5000 | -| **OS** | Ubuntu 22.04 LTS | +### `.github/workflows/backend-ci.yml` ---- +**Triggers:** +- `push` to `main` +- `pull_request` to `main` +- Manual `workflow_dispatch` -## Deployment Commands +**Jobs:** -```bash -# SSH to server -ssh -i ~/LightsailDefaultKeyPair.pem ubuntu@52.77.218.100 +1. **Checkout code** +2. **Setup .NET 9.0.x** +3. **Restore dependencies** +4. **Build** (Release config) +5. **Run tests** (nếu có) +6. **Docker login** với Docker Hub credentials +7. **Build & tag**: + - `:main` (latest trên main branch) + - `:${{ github.sha }}` (commit SHA cụ thể) + - `:latest` +8. **Push** to Docker Hub + +**Outputs:** +- Image available at: `docker.io/anhtuan21112004/menugreensystem:main` -# Check container status -docker ps +--- -# View API logs -docker logs menugreen_api --tail 50 -f +### `.github/workflows/backend-cd.yml` -# Restart API -docker restart menugreen_api +**Triggers:** +- `workflow_run` từ `backend-ci.yml` với conclusion = `success` (chỉ trên nhánh `main`, không phải PR) +- Manual `workflow_dispatch` (option `production` hoặc `staging`) -# Pull latest image manually -docker pull anhtuan21112004/menugreensystem:latest -docker compose -f docker-compose.prod.yml up -d api +**Skip deploy** nếu: +- CI failed/cancelled +- Commit message chứa `#skipdeploy` +- Trigger là `pull_request` -# Database backup -pg_dump -h menugreen-db.cr4uo6sksium.ap-southeast-1.rds.amazonaws.com \ - -U postgres -d menugreendb -F p -f backup.sql +**Steps (deploy via SCP + SSH):** -# Navigate to app -cd ~/apps/MenuGreenSystem +```bash +# === Pre-SSH: SCP nginx files === +# GitHub Actions dùng appleboy/scp-action copy file từ repo lên server +scp backend/nginx/nginx.conf ubuntu@server:/tmp/nginx-deploy/ +scp backend/nginx/conf.d/cors-map.conf ubuntu@server:/tmp/nginx-deploy/ + +# === SSH vào server === + +# 1. Apply nginx config (FIRST - trước khi restart container) +# Nếu fail → restore backup + abort toàn bộ deploy (zero downtime) +NGINX_TS=$(date +"%Y%m%d_%H%M%S") +sudo cp /etc/nginx/conf.d/cors-map.conf \ + /etc/nginx/conf.d/cors-map.conf.bak.$NGINX_TS +sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak.$NGINX_TS +sudo cp /tmp/nginx-deploy/nginx.conf /etc/nginx/nginx.conf +sudo cp /tmp/nginx-deploy/conf.d/cors-map.conf /etc/nginx/conf.d/cors-map.conf +if sudo nginx -t 2>&1; then + sudo systemctl reload nginx +else + sudo cp /etc/nginx/conf.d/cors-map.conf.bak.$NGINX_TS \ + /etc/nginx/conf.d/cors-map.conf + sudo cp /etc/nginx/nginx.conf.bak.$NGINX_TS /etc/nginx/nginx.conf + exit 1 +fi +rm -rf /tmp/nginx-deploy + +# 2. Cleanup disk +sudo docker system prune -af --volumes + +# 3. Decode embedded docker-compose.prod.yml +echo "$COMPOSE_B64" | base64 -d > "$APP_DIR/docker-compose.prod.yml" + +# 4. Install Doppler CLI (if missing) +curl -fsSL https://github.com/DopplerHQ/cli/releases/... + +# 5. Download Doppler secrets +doppler secrets download --token $DOPPLER_TOKEN \ + --no-file --project menugreen --config prd --format env \ + > /tmp/doppler_raw.env + +# 6. Build .env from secrets +# Format: Foo__Bar=value (convert : to __ for nested keys) +# Special handling for: ConnectionStrings__DefaultConnection, JwtSettings__*, REDIS_URL + +# 7. Backup RDS (FAIL = ABORT DEPLOY) +PGPASSWORD=$DB_PASSWORD pg_dump -h $DB_HOST -U $DB_USER \ + -d $DB_NAME -F p -f /tmp/menugreen_backup_*.sql + +# 8. Tag previous image +docker tag $IMAGE:main $IMAGE:previous +docker push $IMAGE:previous + +# 9. Pull latest +docker pull $IMAGE:main + +# 10. Stop + remove old container +docker compose -f $APP_DIR/docker-compose.prod.yml down --remove-orphans + +# 11. Start new container +docker compose -f $APP_DIR/docker-compose.prod.yml up -d + +# 12. Wait + health check +for i in {1..30}; do + curl -sf http://localhost:5000/health/ready && break + sleep 2 +done + +# 13. Auto-rollback if health check fails +# - Logs failed container +# - Down compose +# - Pull $IMAGE:previous +# - Re-fetch Doppler secrets +# - Up with previous image ``` +> **Server info (SSH, app dir, image, port, domain):** xem [ARCHITECTURE.md](./ARCHITECTURE.md#server-information). + --- ## Database Migration -### Automatic (via CI/CD) +### Automatic (trên app startup) -CI/CD pipeline tự động chạy migration khi deploy: -1. Backup database với `pg_dump` -2. Run `dotnet ef database update` -3. Nếu fail → rollback, không start API +App tự chạy EF Core migration khi khởi động (xem `Program.cs` / `DbContext`). Không cần efbundle hay chạy `dotnet ef` trong container. -### Manual +### Backup trước khi deploy -```bash -# SSH vào server -ssh -i ~/LightsailDefaultKeyPair.pem ubuntu@52.77.218.100 +CD workflow tự động `pg_dump` trước khi deploy: -# Backup trước -pg_dump -h menugreen-db.cr4uo6sksium.ap-southeast-1.rds.amazonaws.com \ - -U postgres -d menugreendb -F p -f backup_$(date +%Y%m%d_%H%M%S).sql +```bash +BACKUP_FILE="/tmp/menugreen_backup_$(date +%Y%m%d_%H%M%S).sql" +PGPASSWORD="$DB_PASSWORD" pg_dump -h "$DB_HOST" \ + -U "$DB_USER" -d "$DB_NAME" -F p -f "$BACKUP_FILE" -# Run migration -docker exec menugreen_api dotnet ef database update \ - --project backend/MenuGreen.DataAccessLayer/MenuGreen.DataAccessLayer.csproj \ - --startup-project backend/MenuGreen.API/MenuGreen.API.csproj +# Backup fail → ABORT deployment ``` +Backup giữ lại 5 file gần nhất ở `/tmp/`. + --- ## Health Check -### Endpoints - -| Endpoint | Description | -|----------|-------------| -| `GET /health` | Full health check (DB + Redis) | -| `GET /health/ready` | Readiness check | -| `GET /health/live` | Liveness check | +| Endpoint | Description | Check trong CI | +|----------------------|----------------------------|----------------| +| `GET /health` | Full health (DB + Redis) | | +| `GET /health/ready` | Readiness (DB + Redis) | ✅ (30 lần, 2s/lần) | +| `GET /health/live` | Liveness (always OK) | (trong Docker healthcheck) | -### Test +### Test thủ công ```bash -curl https://api.menugreen.food/health +# Qua Nginx (public) +curl -I https://api.menugreen.food/health/live + +# Trực tiếp API (trên server) +curl -I http://localhost:5000/health/ready + +# Trên server từ máy local +ssh -i LightsailDefaultKeyPair.pem ubuntu@52.77.218.100 \ + "curl http://localhost:5000/health/ready" ``` --- ## Rollback Plan -### If Migration Fail +### Auto rollback (CD workflow tự làm) -1. SSH vào server -2. Restore từ backup: - ```bash - psql -h menugreen-db.cr4uo6sksium.ap-southeast-1.rds.amazonaws.com \ - -U postgres -d menugreendb < backup.sql - ``` -3. Pull image version cũ -4. Không start API cho đến khi fix xong +Nếu health check fail 30 lần (60s) sau khi deploy: -### If Deployment Fail +1. Log container lỗi +2. Stop container hiện tại +3. Pull image `anhtuan21112004/menugreensystem:previous` +4. Re-fetch Doppler secrets (đảm bảo `.env` đúng format) +5. Tag previous image +6. `docker compose up -d` với image cũ +7. `exit 1` để workflow fail -1. SSH vào server -2. Stop current: `docker compose -f docker-compose.prod.yml down` -3. Pull image cũ: `docker pull docker.io/anhtuan21112004/menugreensystem:` -4. Start: `docker compose -f docker-compose.prod.yml up -d` -5. Verify health check - ---- - -## Docker Compose Production +### Manual rollback ```bash -# Build & Start -docker-compose -f docker-compose.prod.yml build -docker-compose -f docker-compose.prod.yml up -d +ssh -i ~/LightsailDefaultKeyPair.pem ubuntu@52.77.218.100 -# View logs -docker-compose -f docker-compose.prod.yml logs -f +cd /home/ubuntu/apps/menugreen -# Stop -docker-compose -f docker-compose.prod.yml down -``` +# Stop current +docker compose -f docker-compose.prod.yml down ---- +# Pull previous image +sudo docker pull anhtuan21112004/menugreensystem:previous -## Nginx Configuration (Snippets Approach) +# Tag cho compose +sudo docker tag anhtuan21112004/menugreensystem:previous menugreen_api -Tách config thành các snippets để tái sử dụng và dễ maintain. +# Start +docker compose -f docker-compose.prod.yml up -d -### Folder Structure - -``` -/etc/nginx/ -├── snippets/ -│ ├── proxy-params.conf ← Proxy settings -│ └── cors-headers.conf ← CORS headers -├── sites-available/ -│ └── api.menugreen.food ← API config -└── sites-enabled/ - └── api.menugreen.food ← Symlink +# Verify +docker logs menugreen_api --tail 50 +curl http://localhost:5000/health/ready ``` -### Step 1: Create snippets folder +### Rollback DB từ backup ```bash -sudo mkdir -p /etc/nginx/snippets -``` - -### Step 2: Create proxy-params.conf +ls -t /tmp/menugreen_backup_*.sql | head -1 -```bash -sudo nano /etc/nginx/snippets/proxy-params.conf +# Restore +PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST \ + -U $DB_USER -d $DB_NAME < /tmp/menugreen_backup_20260711_143000.sql ``` -```nginx -# Proxy parameters - tái sử dụng cho tất cả backend services - -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; -proxy_set_header Connection ""; - -# Timeouts -proxy_connect_timeout 60s; -proxy_send_timeout 60s; -proxy_read_timeout 60s; - -# Buffers -proxy_buffering on; -proxy_buffer_size 4k; -proxy_buffers 4 4k; -``` +--- -### Step 3: Create cors-headers.conf +## Nginx Configuration (auto-deploy qua CI/CD) -```bash -sudo nano /etc/nginx/snippets/cors-headers.conf -``` +Cấu hình chi tiết: xem [NGINX_AND_CORS.md](./NGINX_AND_CORS.md) và `backend/nginx/`. -```nginx -# CORS Headers - tái sử dụng - -# Preflight OPTIONS -if ($request_method = 'OPTIONS') { - add_header 'Access-Control-Allow-Origin' 'https://www.menugreen.food' always; - add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS, PATCH' always; - add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, Accept, Origin, X-Requested-With' always; - add_header 'Access-Control-Allow-Credentials' 'true' always; - add_header 'Access-Control-Max-Age' 86400 always; - add_header 'Content-Type' 'text/plain; charset=utf-8'; - add_header 'Content-Length' 0; - return 204; -} - -# Normal responses -add_header 'Access-Control-Allow-Origin' 'https://www.menugreen.food' always; -add_header 'Access-Control-Allow-Credentials' 'true' always; -add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS, PATCH' always; -add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, Accept, Origin, X-Requested-With' always; -``` +### Tóm tắt -### Step 4: Update API config +- Nginx chạy **trên host** (không trong Docker) +- Config trong `/etc/nginx/nginx.conf` + `/etc/nginx/conf.d/cors-map.conf` +- Source config trong git: `backend/nginx/` +- **Deploy tự động qua CI/CD**: sửa file → commit → push → GitHub Actions tự SCP + apply +- Proxy: `https://api.menugreen.food` → `http://localhost:5000` +- CORS dùng **map** trong `cors-map.conf` (whitelist origins) +- SSL Let's Encrypt auto-renew -```bash -sudo nano /etc/nginx/sites-available/api.menugreen.food -``` +### Workflow apply Nginx -```nginx -server { - listen 443 ssl http2; - server_name api.menugreen.food; - - # SSL certificates - ssl_certificate /etc/letsencrypt/live/api.menugreen.food/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/api.menugreen.food/privkey.pem; - - # SSL optimization - ssl_session_cache shared:SSL:10m; - ssl_session_timeout 10m; - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers HIGH:!aNULL:!MD5; - - location / { - # Include CORS headers - include snippets/cors-headers.conf; - - # Proxy to backend - proxy_pass http://localhost:5000; - include snippets/proxy-params.conf; - } -} ``` - -### Step 5: Reload Nginx - -```bash -# Test config -sudo nginx -t - -# Reload nginx -sudo systemctl reload nginx - -# Verify -curl -I https://api.menugreen.food/health +Developer sửa backend/nginx/conf.d/cors-map.conf + ↓ +git push origin main + ↓ +backend-ci.yml — build Docker image (3-5 phút) + ↓ +backend-cd.yml: + ├─ SCP file nginx lên /tmp/nginx-deploy/ + └─ SSH apply: + ├─ Backup config (.bak.YYYYMMDD_HHMMSS) + ├─ Copy file mới → /etc/nginx/ + ├─ nginx -t → PASS → systemctl reload nginx + └─ nginx -t → FAIL → restore backup + abort ``` -### Verification Result +### Zero downtime guarantee -``` -HTTP/2 200 -access-control-allow-origin: https://www.menugreen.food -access-control-allow-credentials: true -access-control-allow-methods: GET, POST, PUT, DELETE, OPTIONS, PATCH -access-control-allow-headers: Content-Type, Authorization, Accept, Origin, X-Requested-With -``` +Khi nginx apply fail: +- ✅ Container KHÔNG bị restart (vẫn chạy image cũ) +- ✅ Nginx vẫn chạy với config cũ +- ✅ Không có downtime cho user +- ❌ Workflow fail → Dev nhận alert qua GitHub Actions -### Monitoring Logs +--- -```bash -# Access log -sudo tail -20 /var/log/nginx/access.log +## Monitoring (hiện tại) -# Error log -sudo tail -20 /var/log/nginx/error.log +- Health check `curl /health/ready` trong CD workflow +- Application logs: `docker logs menugreen_api -f` +- Nginx access logs: `/var/log/nginx/access.log` +- Nginx error logs: `/var/log/nginx/error.log` -# Real-time monitoring -sudo tail -f /var/log/nginx/access.log -``` +Có thể tích hợp thêm: +- UptimeRobot: ping `/health/live` mỗi 5 phút +- CloudWatch: collect Docker metrics +- Prometheus + Grafana: (chưa setup, có thể thêm sau) --- ## Troubleshooting -### Build Failures +### Build failures ```bash -# Check network connectivity +# Check network curl -s https://api.nuget.org/v3/index.json | head # Clear NuGet cache dotnet nuget locals all --clear ``` -### Deployment Failures +### Deployment failures ```bash -# Check logs -docker logs menugreen_api +# Container logs +docker logs menugreen_api --tail 100 -# Check environment variables +# Environment variables trong container docker exec menugreen_api env | sort -# Check port availability -netstat -tlnp | grep 5000 +# Port availability +sudo netstat -tlnp | grep 5000 + +# Container status +docker ps -a | grep menugreen ``` -### CORS Issues +### Doppler issues ```bash -# Test preflight -curl -I -X OPTIONS https://api.menugreen.food/api/Auth/login \ - -H "Origin: https://www.menugreen.food" \ - -H "Access-Control-Request-Method: POST" - -# Verify CORS headers -curl -I https://api.menugreen.food/health +# Test download thủ công +DOPPLER_TOKEN=dp.prd.xxx doppler secrets download \ + --no-file --project menugreen --config prd --format env ``` ---- - -## Monitoring Stack (Future) +### Health check fails -See: [monitoring/uptimerobot-setup.md](./monitoring/uptimerobot-setup.md) +```bash +# Trên server +curl -v http://localhost:5000/health/ready +docker logs menugreen_api --tail 50 -Planned: -- Prometheus metrics: `/metrics` -- Grafana dashboards -- UptimeRobot alerts +# Qua Nginx +curl -v https://api.menugreen.food/health/ready +sudo nginx -t +sudo tail -20 /var/log/nginx/error.log +``` --- ## Related Documents -| Document | Description | -|----------|-------------| -| [GITHUB_SECRETS_SETUP.md](./GITHUB_SECRETS_SETUP.md) | GitHub secrets setup guide | -| [lightsail-setup.md](./lightsail-setup.md) | Server setup guide | -| [issues.md](./issues.md) | Issue tracker | +| Document | Description | +|---------------------------------------------|--------------------------------------| +| [ARCHITECTURE.md](./ARCHITECTURE.md) | Kiến trúc tổng quan + GitHub Secrets + Server Info | +| [NGINX_AND_CORS.md](./NGINX_AND_CORS.md) | CORS & Nginx configuration | +| [SECRETS_MANAGEMENT.md](./SECRETS_MANAGEMENT.md) | Quản lý secrets qua Doppler | +| [SERVER_SETUP.md](./SERVER_SETUP.md) | Setup server từ đầu | +| [archive/](./archive/) | Lịch sử fix + review (đã xong) | +| [GITHUB_SECRETS_SETUP.md](../GITHUB_SECRETS_SETUP.md) | Setup GitHub Secrets | --- -*Last updated: 2026-07-05* +*Last updated: 2026-07-11* diff --git a/docs/01-deployment/DEPLOY.md b/docs/01-deployment/DEPLOY.md deleted file mode 100644 index f0e5aec9..00000000 --- a/docs/01-deployment/DEPLOY.md +++ /dev/null @@ -1,303 +0,0 @@ -# MenuGreen System - Production Deployment Plan - -## Mục tiêu - -- Deploy backend .NET API lên AWS Lightsail Ubuntu sử dụng **GitHub Actions CI/CD**. -- PostgreSQL dùng **AWS RDS** (không chạy trong Docker). -- Redis chạy trong **Docker container** trên server. -- **Tự động hóa hoàn toàn**: push code → CI/CD tự deploy. - ---- - -## Kiến trúc Production - -``` -GitHub Repository - │ - ▼ -┌──────────────────────────────┐ -│ GitHub Actions CI/CD │ -│ (GitHub Secrets) │ -└──────────────────────────────┘ - │ - │ SSH + Docker - ▼ -AWS Lightsail (Ubuntu 22.04) -├── Docker -│ ├── menugreen-api (port 5000) -│ └── menugreen-redis (port 6379) -└── AWS RDS - └── PostgreSQL 15 (port 5432) -``` - ---- - -## Services - -| Service | Location | Port | Notes | -|---------|----------|------|-------| -| API | Docker container | 5000 | Kết nối RDS + Redis | -| Redis | Docker container | 6379 | Giới hạn 256MB RAM | -| PostgreSQL | AWS RDS | 5432 | `` | - ---- - -## CI/CD Pipeline - -### Jobs Flow - -``` -┌─────────────────┐ -│ build-and-test │ (Unit Tests) -└────────┬────────┘ - │ pass - ▼ -┌─────────────────┐ -│ build-docker │ (Build & Push to GHCR) -└────────┬────────┘ - │ pass - ▼ -┌─────────────────┐ -│ deploy │ (SSH → Pull → Deploy) -│ │ 1. Create .env -│ │ 2. Pull Docker image -│ │ 3. Stop old container -│ │ 4. Start new container -│ │ 5. Health check -│ │ 6. Run EF Migration -└─────────────────┘ -``` - -### Trigger Conditions - -- Push lên nhánh `main` hoặc `Tuan` -- Pull Request vào nhánh `main` - ---- - -## PHASE 1: Chuẩn bị (GitHub Secrets) - -### Danh sách Secrets cần thêm - -Vào **GitHub** → Repository → **Settings** → **Secrets and variables** → **Actions** → **New repository secret**: - -| Secret Name | Giá trị | Ghi chú | -|-------------|---------|---------| -| `LIGHTSAIL_HOST` | `` | IP/DNS server | -| `LIGHTSAIL_USER` | `ubuntu` | SSH username | -| `LIGHTSAIL_SSH_KEY` | (paste file .pem) | Toàn bộ nội dung | -| `DB_HOST` | `` | Ví dụ: | -| `DB_PORT` | `5432` | | -| `DB_NAME` | `MenuGreenDb` | | -| `DB_USER` | `postgres` | | -| `DB_PASSWORD` | `` | | -| `REDIS_PASSWORD` | `` | | -| `JWT_SECRET` | `` | Tạo: `openssl rand -base64 48` | - -### Cách tạo JWT_SECRET - -```bash -# Trên terminal -openssl rand -base64 48 -``` - -Copy kết quả và paste vào GitHub Secret `JWT_SECRET`. - ---- - -## PHASE 2: Chuẩn bị Server - -### Checklist - -- [ ] Server Ubuntu đã cài Docker + Docker Compose -- [ ] Security group RDS đã mở port 5432 cho IP server -- [ ] Database `MenuGreenDb` đã tạo trên RDS -- [ ] File `.pem` SSH key đã tải về (để add vào GitHub Secret) - -### Kiểm tra Docker trên server - -```bash -ssh -i your-key.pem ubuntu@ - -# Kiểm tra Docker -docker --version -docker compose version - -# Kiểm tra Redis container (nếu chưa có, sẽ được tạo tự động) -docker ps -a | grep redis || echo "Redis chưa có, sẽ được tạo" -``` - -### Tạo database trên RDS (nếu chưa có) - -```bash -psql -h \ - -p 5432 -U postgres -d postgres - -# Nhập password khi được hỏi - -CREATE DATABASE "MenuGreenDb"; -\q -``` - ---- - -## PHASE 3: Deploy tự động - -### Cách deploy - -**Chỉ cần push code lên nhánh `main` hoặc `Tuan`:** - -```bash -# Trên local -git add . -git commit -m "feat: mô tả thay đổi" -git push origin main -# hoặc -git push origin Tuan -``` - -GitHub Actions sẽ tự động: -1. Chạy unit tests -2. Build Docker image -3. Push lên GHCR -4. SSH vào server -5. Pull image mới -6. Deploy container mới -7. Chạy EF Migration -8. Health check - -### Theo dõi tiến trình - -Vào **GitHub** → Repository → **Actions** tab → Click vào workflow đang chạy. - ---- - -## PHASE 4: Kiểm tra sau deploy - -### Checklist - -- [ ] Workflow status: ✅ green -- [ ] API health check: `curl http://:5000/health` -- [ ] Swagger UI: `http://:5000/swagger/index.html` - -### Thao tác kiểm tra - -```bash -# SSH vào server -ssh -i your-key.pem ubuntu@ - -# Kiểm tra container đang chạy -docker ps - -# Kiểm tra logs API -docker logs menugreen-api --tail 50 - -# Kiểm tra health endpoint -curl http://localhost:5000/health - -# Kiểm tra Redis -docker exec menugreen-redis redis-cli -a $REDIS_PASSWORD ping -``` - ---- - -## Xử lý lỗi thường gặp - -### Lỗi Secrets - -| Triệu chứng | Nguyên nhân | Cách fix | -|-------------|-------------|----------| -| SSH connection failed | `LIGHTSAIL_SSH_KEY` sai | Kiểm tra lại file .pem, đảm bảo copy đúng | -| Cannot connect to RDS | Security group chưa mở | Mở port 5432 cho IP server trong AWS Console | -| Redis connection failed | Password sai | Kiểm tra `REDIS_PASSWORD` secret | - -### Lỗi Deployment - -| Triệu chứng | Cách fix | -|-------------|----------| -| Workflow failed at deploy | Xem logs trong Actions tab | -| Container không start | `docker logs menugreen-api` | -| Migration lỗi | SSH vào server, chạy lại: `docker exec menugreen-api dotnet ef database update` | - -### Re-run deployment - -Vào **Actions** → Click workflow failed → **Re-run all jobs** - ---- - -## QUICK REFERENCE - -### Sau khi thêm secrets - -Push code để trigger CI/CD: - -```bash -git add . -git commit -m "ci: trigger deployment" -git push origin main -``` - -### Kiểm tra trạng thái - -| Mục đích | Cách kiểm tra | -|----------|---------------| -| Xem workflow | GitHub → Actions tab | -| Logs CI/CD | GitHub → Actions → Click job → Xem logs | -| Logs API server | `docker logs menugreen-api -f` | -| Health API | `curl http://:5000/health` | - -### Restart container (nếu cần) - -```bash -ssh -i your-key.pem ubuntu@ - -# Restart API -docker restart menugreen-api - -# Restart Redis -docker restart menugreen-redis -``` - -### Stop tất cả - -```bash -docker stop menugreen-api menugreen-redis -docker rm menugreen-api menugreen-redis -``` - ---- - -## Redis Production Configuration - -### Resource Limits - -- Memory: 256MB -- Policy: allkeys-lru (xóa key cũ khi full) -- Persistence: AOF enabled - -### Backup (nếu cần) - -```bash -# Manual backup -docker exec menugreen-redis redis-cli -a $REDIS_PASSWORD BGSAVE -docker cp menugreen-redis:/data/dump.rdb ./backup-$(date +%Y%m%d).rdb -``` - ---- - -## Lưu ý quan trọng - -- **Không commit** `.env` hoặc secrets vào git. -- PostgreSQL **không chạy trong Docker** (dùng RDS). -- CI/CD tự động deploy khi push lên `main` hoặc `Tuan`. -- SSH key (.pem) cần được paste **toàn bộ** vào GitHub Secret. -- Health endpoint có thể cần cấu hình thêm trong API. - ---- - -## Liên hệ hỗ trợ - -Nếu gặp lỗi không xử lý được: -1. Xem logs trong GitHub Actions -2. SSH vào server kiểm tra logs -3. Kiểm tra GitHub Secrets đã đúng chưa diff --git a/docs/01-deployment/DEPLOY_FIX_PLAN.md b/docs/01-deployment/DEPLOY_FIX_PLAN.md deleted file mode 100644 index f2801736..00000000 --- a/docs/01-deployment/DEPLOY_FIX_PLAN.md +++ /dev/null @@ -1,696 +0,0 @@ -# MenuGreen System - Deployment Fix Plan (Final v2) - -## Mục tiêu -- Đưa app `MenuGreen.API` chạy ổn định trên **Lightsail Ubuntu VM** với Docker + Compose. -- Đảm bảo EF Core migration chạy **trước khi start API**, theo best practice production. -- Giữ nguyên CI/CD hiện tại (GitHub Actions + Doppler + GHCR) nhưng sắp xếp đúng thứ tự deploy. -- Phù hợp với server **$12/tháng** (2 vCPU, 2GB RAM, 60GB SSD). -- Thống nhất deploy theo **1 nhánh duy nhất** là `main` để tránh nhầm lẫn giữa `main` và `Tuan`. - ---- - -## 1. Vấn đề hiện tại - -||| # | Lỗi | Tác động | -|---|------|----------| -||| 1 | `.env` trên Lightsail lệch so với Doppler `prd` | API `menugreen_api` đang `unhealthy` do thiếu JWT + Redis connection string sai | -||| 1b | Migration đang chạy `dotnet ef database update` trong container runtime không có SDK | Migration fail im lặng hoặc không chạy đúng trong prod | -||| 2 | Redis chỉ có `REDIS_HOST`/`REDIS_PORT` riêng | `Program.cs` đọc `ConnectionStrings:Redis` → null | -||| 3 | Thiếu volume Firebase | FCM có thể không hoạt động nếu dùng | -||| 4 | CI chỉ check `/health/ready` | Không phân biệt được app dead vs DB/Redis unreachable | - -### Trạng thái hiện tại (01/07/2026) - -- `menugreen_api`: `unhealthy` -- `menugreen_redis`: `healthy` -- RDS: kết nối được từ Lightsail -- `.env` server: thiếu `JwtSettings__SecretKey`, `JwtSettings__Issuer`, `JwtSettings__Audience`; `ConnectionStrings__Redis` đang là `:,password=` -- CI/CD: đã sửa cách sinh `.env` từ Doppler `prd` đúng key backend đọc - ---- - -## 2. Kiểm tra rủi ro và tối ưu - -||| Điểm cũ | Rủi ro | Tối ưu | -|||----------|--------|--------| -||| Chạy `efbundle` từ API container sau `up -d` | API fail startup → không exec được → không migrate | **Chạy migration trên host, trước khi `up -d`** | -||| Build bundle trong Dockerfile | Image prod cồng kềnh, chứa tool thừa | **Bundle là artifact CI riêng**, không lưu trong runtime image | -||| Chưa verify config | `.env` thiếu/sai format → API crash im lặng | Thêm bước **verify config** sau khi upload `.env` | -||| Health check chỉ `/health/ready` | Timeout không phân biệt nguyên nhân | Dùng `/health/live` trước, `/health/ready` sau | - ---- - -## 3. Server cần chuẩn bị gì - -### 3.1 Yêu cầu cơ bản - -Dựa trên server bạn đã mua (**Lightsail $12/tháng**) + database hiện tại (**AWS RDS PostgreSQL**): - -||| Thành phần | Thực tế | Đánh giá | -|||------------|---------|----------| -||| OS | Ubuntu (Lightsail default) | ✅ Tương thích | -||| CPU | 2 vCPUs | ✅ Đủ cho Docker + API | -||| RAM | **2 GB** | ⚠️ Vừa đủ, cần tối ưu | -||| Disk | 60 GB SSD | ✅ Đủ | -||| Network | Static IP + 3TB transfer | ✅ Tốt | -||| Outbound | HTTPS 443 + HTTP 5000 + TCP 5432 (RDS) | Cần mở Lightsail Firewall | -||| Database | **AWS RDS PostgreSQL** | ✅ Đã có sẵn, không cần cài DB trên server | - -**Lưu ý quan trọng:** -- Database **không chạy trên Lightsail**, mà là **AWS RDS PostgreSQL** bên ngoài -- Do đó, **không cần cài PostgreSQL container** trong `docker-compose.prod.yml` -- Lightsail chỉ cần kết nối ra RDS endpoint qua port 5432 -- Nếu RDS đang chạy, plan này tập trung vào deploy **chỉ API + Redis** - -### 3.2 Tối ưu cho 2GB RAM - -Vì database đã ở AWS RDS bên ngoài, Lightsail chỉ cần chạy **2 services**: - -- **Redis** (~256MB) - cache local trên Lightsail -- **API** (~800MB) - .NET container - -**docker-compose.prod.yml - memory limits:** -```yaml -services: - redis: - image: redis:7-alpine - container_name: menugreen_redis - command: redis-server --appendonly yes --maxmemory 200mb --maxmemory-policy allkeys-lru - deploy: - resources: - limits: - memory: 256M - cpus: '0.5' - - api: - build: - context: . - dockerfile: Dockerfile - container_name: menugreen_api - deploy: - resources: - limits: - memory: 800M - cpus: '1.0' -``` - -**Tổng memory allocation:** -||| Container | Limit | Reserve | -|||-----------|-------|---------| -||| menugreen_redis | 256 MB | 128 MB | -||| menugreen_api | 800 MB | 512 MB | -||| OS + Docker | ~700 MB | ~600 MB | -||| **Total** | **~1.8 GB** | **~1.2 GB | - -Nếu thấy RAM không đủ, có thể nâng cấp lên $24/tháng (4GB) - chỉ cần vài cú click trên Lightsail console. - ---- - -### 3.3 `efbundle` có cần tải/cài trên Lightsail không? - -- **Không cần cài gì thêm trên Lightsail.** -- `efbundle` là **artifact CI** được build trên GitHub Actions runner từ `dotnet ef migrations bundle --self-contained -r linux-x64`. -- CI sẽ **upload file `efbundle` lên Lightsail**, chạy migrate, rồi **xóa file đi**. -- Trên Lightsail chỉ cần chạy file binary đó như một chương trình thường; không cần `.NET SDK`, không cần `dotnet-ef`, không cần giữ `efbundle` lâu dài. -- Ưu điểm phù hợp Lightsail 2GB RAM: nhẹ (~5–10MB), không phụ thuộc runtime image, migrate xong là xóa. - -### 3.4 Server Pre-flight Check (KIỂM TRA TRƯỚC KHI CÀI) - -**Chạy script này trên Lightsail instance trước khi cài đặt** - -```bash -# Copy script lên server -scp scripts/server_preflight_check.sh ubuntu@:/home/ubuntu/ - -# SSH vào và chạy -ssh ubuntu@ -chmod +x /home/ubuntu/server_preflight_check.sh -/home/ubuntu/server_preflight_check.sh -``` - -**Script sẽ báo bạn:** -- OS hiện tại là gì, có đúng Ubuntu không -- CPU/RAM/Disk có đủ không -- Docker đã có chưa, version bao nhiêu -- Docker Compose đã có chưa -- Docker daemon đang chạy không -- User có trong group `docker` không -- Network `menugreen-net` đã tồn tại chưa -- Thư mục app, `.env` và Git repo đã có chưa -- Kết nối outbound đến AWS RDS port 5432 có được không -- SSH key của GitHub Actions đã thêm vào `authorized_keys` chưa - -**Sau khi chạy xong, script sẽ liệt kê chính xác những gì CẦN làm, không cần cài gì đó nếu đã có sẵn.** - ---- - -### 3.5 Cài đặt phần mềm trên server (chỉ cài những gì thiếu) - -Dựa trên kết quả pre-flight check, chỉ cài những gì còn thiếu: - -```bash -# 1. Update hệ thống (chỉ cần chạy 1 lần đầu) -sudo apt update && sudo apt upgrade -y - -# 2. Cài Docker (CHỈ NẾU chưa có) -if ! command -v docker &> /dev/null; then - curl -fsSL https://get.docker.com -o get-docker.sh - sudo sh get-docker.sh - sudo usermod -aG docker ubuntu - echo "Docker installed. Logout and login again to apply group changes." -fi - -# 3. Cài Docker Compose plugin (CHỈ NẾU chưa có) -if ! docker compose version &> /dev/null; then - sudo apt install -y docker-compose-plugin -fi - -# 4. Cài công cụ hữu ích (chỉ cài 1 lần) -sudo apt install -y curl jq git ufw -``` - -### 3.6 Cấu hình Firewall (Lightsail + UFW) - -**Lightsail Firewall** (qua console): -- Allow: TCP 22 (SSH) - restrict to your IP if possible -- Allow: TCP 5000 (API) - hoặc 80/443 nếu có Nginx -- Allow: TCP 5432 (PostgreSQL RDS) - từ IP Lightsail nếu cần test trực tiếp - -**UFW trên Ubuntu** (tùy chọn, layer 2): -```bash -sudo ufw allow 22/tcp -sudo ufw allow 5000/tcp -sudo ufw allow 80/tcp -sudo ufw allow 443/tcp -sudo ufw enable -``` - -### 3.7 Tạo Docker Network (nếu chưa có) - -```bash -docker network create menugreen-net -``` - -### 3.8 Tạo file `.env` ban đầu - -Tạo `/home/ubuntu/apps/MenuGreenSystem/.env` với nội dung tối thiểu: - -```env -ASPNETCORE_ENVIRONMENT=Production -ASPNETCORE_URLS=http://+:5000 -ConnectionStrings__DefaultConnection=Host=;Port=5432;Database=MenuGreenDb;Username=postgres;Password=;SSL Mode=Require;Trust Server Certificate=true -ConnectionStrings__Redis=menugreen_redis:6379 -JwtSettings__SecretKey= -AllowedOrigins=https://menugreen.vn,https://app.menugreen.vn -``` - -> **Lưu ý:** CI sau này sẽ overwrite `.env` bằng Doppler secrets, nhưng file này dùng cho deploy thủ công hoặc emergency. - -### 3.9 SSH Key setup cho GitHub Actions - -```bash -# Trên Lightsail instance -mkdir -p ~/.ssh -chmod 700 ~/.ssh - -# Thêm public key của GitHub Actions runner vào ~/.ssh/authorized_keys -# Hoặc dùng Lightsail default key + thêm vào GitHub Secrets -``` - -**GitHub Secrets cần có:** -- `LIGHTSAIL_HOST`: IP Lightsail -- `LIGHTSAIL_USER`: `ubuntu` -- `LIGHTSAIL_SSH_KEY`: Private key SSH -- `DOPPLER_TOKEN`: Token Doppler project `menugreen` config `prd` - -### 3.11 Cấu hình Git (tùy chọn) - -Nếu muốn server có thể `git pull`: -```bash -cd /home/ubuntu/apps/MenuGreenSystem -git init -git remote add origin -git fetch origin -git checkout main # hoặc branch deploy -``` - -Hoặc để CI tự quản lý qua `ssh-action` như hiện tại. - ---- - -## 4. Plan thực hiện chi tiết - -### Bước 1: Cập nhật `.github/workflows/ci-cd.yml` - -**Tại sao dùng `efbundle` thay vì `dotnet ef database update` trong container?** - -||| Vấn đề | Giải thích | -|||--------|-----------| -||| Container prod chỉ có runtime | `aspnet:9.0` image không có .NET SDK, không chạy được `dotnet ef` | -||| Cài SDK vào container làm image nặng | Thêm ~1GB vào image, vi phạm best practice production | -||| Migration phải chạy trước API | Đảm bảo DB schema sẵn sàng trước khi app start, tránh crash | - -**`efbundle` là native binary** (~5-10MB) được build từ `dotnet ef migrations bundle --self-contained`, chạy trực tiếp trên Linux mà **không cần cài .NET SDK hay dotnet-ef tool**. - -**Cập nhật job `deploy` trong `.github/workflows/ci-cd.yml` theo flow tối ưu:** - -- Không chạy `dotnet ef database update` trong container runtime. -- Thêm bước build `efbundle` trên CI, upload lên Lightsail và chạy migrate **trước khi `docker compose up -d`**. -- Thêm health check `/health/live` trước, `/health/ready` sau. -- Thống nhất deploy chỉ nhánh `main`. - -Chi tiết đã đồng bộ trong file `.github/workflows/ci-cd.yml`: -```yaml - - name: Upload .env to server - uses: appleboy/scp-action@v0.1.7 - with: - host: ${{ secrets.LIGHTSAIL_HOST }} - username: ${{ secrets.LIGHTSAIL_USER }} - key: ${{ secrets.LIGHTSAIL_SSH_KEY }} - source: "${{ github.workspace }}/.env" - target: "/home/ubuntu/apps/MenuGreenSystem" - strip_components: 0 - overwrite: true - - - name: Build migration bundle - run: | - dotnet tool install --global dotnet-ef --version 9.0.0 - export PATH="$PATH:~/.dotnet/tools" - dotnet ef migrations bundle \ - --self-contained -r linux-x64 \ - --project backend/MenuGreen.DataAccessLayer/MenuGreen.DataAccessLayer.csproj \ - --startup-project backend/MenuGreen.API/MenuGreen.API.csproj \ - -o ./efbundle - - - name: Upload and run migration bundle - run: | - scp -i "${{ secrets.LIGHTSAIL_SSH_KEY }}" -o StrictHostKeyChecking=no \ - ./efbundle ${{ secrets.LIGHTSAIL_USER }}@${{ secrets.LIGHTSAIL_HOST }}:/home/ubuntu/apps/MenuGreenSystem/efbundle - - ssh -i "${{ secrets.LIGHTSAIL_SSH_KEY }}" -o StrictHostKeyChecking=no \ - "${{ secrets.LIGHTSAIL_USER }}@${{ secrets.LIGHTSAIL_HOST }}" \ - "cd /home/ubuntu/apps/MenuGreenSystem && \ - chmod +x ./efbundle && \ - CONNECTION_STRING=\$(grep '^ConnectionStrings__DefaultConnection=' .env | cut -d'=' -f2-) && \ - ./efbundle --connection \"\$CONNECTION_STRING\" || echo 'Migration may have already been applied' && \ - rm -f ./efbundle" - - - name: Deploy application - run: | - ssh -i "${{ secrets.LIGHTSAIL_SSH_KEY }}" -o StrictHostKeyChecking=no \ - "${{ secrets.LIGHTSAIL_USER }}@${{ secrets.LIGHTSAIL_HOST }}" \ - "cd /home/ubuntu/apps/MenuGreenSystem && \ - docker compose -f docker-compose.prod.yml down --remove-orphans 2>/dev/null || true && \ - docker compose -f docker-compose.prod.yml up -d --force-recreate && \ - sleep 15" - - - name: Verify containers - run: | - ssh -i "${{ secrets.LIGHTSAIL_SSH_KEY }}" -o StrictHostKeyChecking=no \ - "${{ secrets.LIGHTSAIL_USER }}@${{ secrets.LIGHTSAIL_HOST }}" \ - "docker compose -f /home/ubuntu/apps/MenuGreenSystem/docker-compose.prod.yml ps" - - - name: Wait for API to be alive - run: | - echo "Waiting for API..." - for i in {1..90}; do - STATUS=$(curl -sf "http://${{ secrets.LIGHTSAIL_HOST }}:5000/health/live" -w "\n%{http_code}" || true) - CODE=$(echo "$STATUS" | tail -n1 || true) - if [ "$CODE" = "200" ]; then - echo "API is alive!" - break - fi - echo "Waiting... ($i/90) status=${CODE:-no_response}" - sleep 5 - done - - - name: Check API readiness - run: | - echo "Checking API readiness (best-effort)..." - curl -fsSL "http://${{ secrets.LIGHTSAIL_HOST }}:5000/health/ready" \ - || echo "Ready check failed - check DB/Redis connectivity" -``` - -### Bước 2: Cập nhật `docker-compose.prod.yml` - -File hiện tại đã khá sát production. Mình chỉ bổ sung nhỏ cho khớp plan và giới hạn rõ ràng cho Lightsail 2GB: - -```yaml -services: - redis: - image: redis:7-alpine - container_name: menugreen_redis - volumes: - - redis_data:/data - networks: - - menugreen-net - restart: unless-stopped - command: > - sh -c 'if [ -n "$$REDIS_PASSWORD" ]; then - redis-server --appendonly yes --requirepass "$$REDIS_PASSWORD" --maxmemory 200mb --maxmemory-policy allkeys-lru; - else - redis-server --appendonly yes --maxmemory 200mb --maxmemory-policy allkeys-lru; - fi' - healthcheck: - test: > - sh -c 'if [ -n "$$REDIS_PASSWORD" ]; then - redis-cli -a "$$REDIS_PASSWORD" ping; - else - redis-cli ping; - fi' - interval: 30s - timeout: 10s - retries: 3 - deploy: - resources: - limits: - memory: 256M - cpus: '0.5' - - api: - build: - context: . - dockerfile: Dockerfile - container_name: menugreen_api - env_file: - - .env - environment: - - ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT} - - ASPNETCORE_URLS=http://+:5000 - ports: - - "5000:5000" - depends_on: - redis: - condition: service_healthy - networks: - - menugreen-net - restart: unless-stopped - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:5000/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - deploy: - resources: - limits: - memory: 800M - cpus: '1.0' - volumes: - # Uncomment nếu dùng Firebase - # - /etc/secrets/firebase-adminsdk.json:/etc/secrets/firebase-adminsdk.json:ro - -volumes: - redis_data: - -networks: - menugreen-net: - external: true -``` - -**Lưu ý:** -- `docker-compose.prod.yml` hiện đã đủ dùng cho deploy. -- Phần Firebase volume giữ dạng **comment** vì backend hiện chỉ đọc `Firebase:CredentialPath` từ config; chỉ cần mount thật khi app dùng FCM. - -### Bước 3: Cập nhật `Dockerfile` (tối giản) - -`Dockerfile` hiện tại đã đủ nhẹ, chỉ cần build + publish app runtime. Không cần chèn tool migrate vào image prod. - -```dockerfile -# Sử dụng base image .NET 9.0 ASP.NET (dùng cho chạy ứng dụng) -FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base -WORKDIR /app -# Render gán PORT lúc runtime (thường 10000); không set ASPNETCORE_URLS trong image. -EXPOSE 10000 -EXPOSE 5000 - -# Install curl for healthchecks -RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/* - -# Sử dụng base image .NET 9.0 SDK (dùng cho build) -FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build -WORKDIR /src - -# Copy file .csproj và restore các packages -COPY ["backend/MenuGreen.API/MenuGreen.API.csproj", "backend/MenuGreen.API/"] -COPY ["backend/MenuGreen.BusinessLogicLayer/MenuGreen.BusinessLogicLayer.csproj", "backend/MenuGreen.BusinessLogicLayer/"] -COPY ["backend/MenuGreen.DataAccessLayer/MenuGreen.DataAccessLayer.csproj", "backend/MenuGreen.DataAccessLayer/"] -RUN dotnet restore "backend/MenuGreen.API/MenuGreen.API.csproj" - -# Copy toàn bộ mã nguồn -COPY . . -WORKDIR "/src/backend/MenuGreen.API" - -# Build ứng dụng -RUN dotnet build "MenuGreen.API.csproj" -c Release -o /app/build - -# Publish ứng dụng (tối ưu hóa) -FROM build AS publish -RUN dotnet publish "MenuGreen.API.csproj" -c Release -o /app/publish /p:UseAppHost=false - -# Cấu hình container cuối cùng (Chỉ chứa code đã publish để giảm dung lượng) -FROM base AS final -WORKDIR /app -COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "MenuGreen.API.dll"] -``` - -### Bước 4: Cập nhật `Program.cs` (nếu cần) - -`Program.cs` hiện tại đã có đủ health checks và đọc đúng Redis connection string. Nếu sau này bạn thêm package thiếu thì mới cần bổ sung; hiện tại phần này không bắt buộc sửa. - -```csharp -// Redis -var redisConnection = - builder.Configuration["Redis:ConnectionString"] - ?? Environment.GetEnvironmentVariable("REDIS_URL"); - -if (!string.IsNullOrWhiteSpace(redisConnection)) -{ - builder.Services.AddStackExchangeRedisCache(options => - { - options.Configuration = redisConnection; - options.InstanceName = "MenuGreen:"; - }); -} -else -{ - builder.Services.AddDistributedMemoryCache(); -} - -// Health checks -builder.Services.AddHealthChecks() - .AddCheck("self", () => HealthCheckResult.Healthy(), tags: new[] { "ready" }) - .AddNpgSql( - builder.Configuration["ConnectionStrings:DefaultConnection"] - ?? "Host=localhost;Port=5432;Database=MenuGreenDb;Username=postgres;Password=12345", - name: "postgresql", - tags: new[] { "db", "ready" }) - .AddRedis( - builder.Configuration["ConnectionStrings:Redis"] - ?? Environment.GetEnvironmentVariable("REDIS_URL") - ?? "localhost:6379", - name: "redis", - tags: new[] { "cache", "ready" }); -``` - -### Bước 5: `scripts/server_preflight_check.sh` - -Đã thêm script vào repo tại `scripts/server_preflight_check.sh`. - -Cách dùng: - -```bash -# Copy script lên server -scp scripts/server_preflight_check.sh ubuntu@:/home/ubuntu/ - -# SSH vào và chạy -ssh ubuntu@ -chmod +x /home/ubuntu/server_preflight_check.sh -/home/ubuntu/server_preflight_check.sh -``` - -Script sẽ kiểm tra: -- OS, CPU/RAM/Disk -- Docker + Docker Compose + daemon -- Group quyền `docker` -- Network `menugreen-net` -- Thư mục app, `.env`, git repo -- Kết nối outbound RDS -- SSH `authorized_keys` cho CI - -### Bước 6: `scripts/deploy.sh` (nếu vẫn dùng deploy thủ công) - -```bash -#!/bin/bash -set -euo pipefail - -APP_DIR="/home/ubuntu/apps/MenuGreenSystem" -ENV_FILE="$APP_DIR/.env" -REGISTRY="ghcr.io/exe201-menugreen/menugreen-api" - -log_info() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*"; } -log_error() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $*" >&2; } -log_warning() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] WARNING: $*"; } - -# 1. Verify .env exists -if [ ! -f "$ENV_FILE" ]; then - log_error ".env file not found at $ENV_FILE" - exit 1 -fi - -# 2. Extract connection string -CONNECTION_STRING=$(grep '^ConnectionStrings__DefaultConnection=' "$ENV_FILE" | cut -d'=' -f2-) -if [ -z "$CONNECTION_STRING" ]; then - log_error "ConnectionStrings__DefaultConnection not found in .env" - exit 1 -fi - -# 3. Pull latest image -log_info "Pulling latest image..." -docker pull $REGISTRY:latest - -# 4. Download and run efbundle for migration -log_info "Running database migration with efbundle..." -EFBUNDLE_URL="https://github.com/your-org/menugreen/releases/latest/download/efbundle" -# Alternative: build efbundle locally if you have dotnet-ef installed -if command -v dotnet-ef &> /dev/null; then - log_info "Building efbundle locally..." - cd "$APP_DIR/backend" - dotnet ef migrations bundle \ - --self-contained -r linux-x64 \ - --project MenuGreen.DataAccessLayer/MenuGreen.DataAccessLayer.csproj \ - --startup-project MenuGreen.API/MenuGreen.API.csproj \ - -o /tmp/efbundle - chmod +x /tmp/efbundle - /tmp/efbundle --connection "$CONNECTION_STRING" || log_warning "Migration may have already been applied" -else - log_error "dotnet-ef not found. Please install dotnet-ef or download efbundle from CI." - exit 1 -fi - -# 5. Start containers -log_info "Starting containers..." -cd "$APP_DIR" -docker compose -f docker-compose.prod.yml down --remove-orphans 2>/dev/null || true -docker compose -f docker-compose.prod.yml up -d --force-recreate - -# 6. Wait and verify -sleep 15 -docker compose -f docker-compose.prod.yml ps - -log_info "Deployment completed!" -``` - -> **Lưu ý:** Script này giả định bạn đã build `efbundle` trong CI và upload lên server, hoặc cài `dotnet-ef` locally. Trong production, nên dùng CI để build efbundle và upload lên server trước khi chạy script này. - ---- - -## 5. Thứ tự thực hiện - -``` -SERVER PREPARATION -├── 1. Provision Lightsail Ubuntu VM (2 vCPU, 4GB RAM, 40GB SSD) -├── 2. SSH vào server, chạy script cài Docker + dependencies -├── 3. Tạo Docker network menugreen-net -├── 4. Tạo thư mục /home/ubuntu/apps/MenuGreenSystem -├── 5. Setup SSH key cho GitHub Actions -├── 6. Config Lightsail Firewall (22, 5000, 80, 443) -└── 7. Tạo .env ban đầu (tạm thời, CI sẽ overwrite) - -CI/CD UPDATE -├── 8. Update ci-cd.yml theo Bước 1 -├── 9. Update docker-compose.prod.yml theo Bước 2 -├── 10. Update Dockerfile theo Bước 3 -└── 11. Commit và push lên `main` - -FIRST DEPLOY -├── 12. CI tự chạy: build image → build efbundle → deploy -├── 13. Verify: docker compose ps + logs -├── 14. Test: curl http://:5000/health/live -└── 15. Test: curl http://:5000/health/ready -``` - ---- - -## 6. Checklist server preparation - -- [ ] **Chạy `server_preflight_check.sh` để biết server đã có gì, cần cài gì** -- [ ] Lightsail instance **$12/tháng** đã tạo (2 vCPU, **2GB RAM**, 60GB SSD) -- [ ] SSH key đã add vào GitHub Secrets (`LIGHTSAIL_SSH_KEY`) -- [ ] `LIGHTSAIL_HOST` và `LIGHTSAIL_USER` đã set trong GitHub Secrets -- [ ] Docker đã cài (nếu chưa có) -- [ ] Docker Compose plugin đã cài (nếu chưa có) -- [ ] User `ubuntu` đã trong group `docker` -- [ ] `menugreen-net` network đã tạo (nếu chưa có) -- [ ] Firewall Lightsail cho phép port 22, 5000 -- [ ] `/home/ubuntu/apps/MenuGreenSystem` đã tồn tại -- [ ] `.env` ban đầu đã tạo (CI sẽ overwrite) -- [ ] RDS PostgreSQL đã tạo và cho phép IP Lightsail kết nối -- [ ] `DB_*` và `REDIS_*` secrets đã có trong Doppler config `prd` -- [ ] **Đã cấu hình memory limits cho containers** (Redis 256MB, API 800MB) để phù hợp 2GB RAM - ---- - -## 7. Troubleshooting - -### API vẫn không start -```bash -# Check logs -docker logs menugreen_api --tail 100 - -# Check env trong container -docker exec menugreen_api env | grep -E 'ASPNETCORE|ConnectionStrings|Redis' - -# Test port -docker exec menugreen_api netstat -tlnp -``` - -### Migration fail -```bash -# 1. Preferred: dùng efbundle từ CI (đã upload lên Lightsail) -# - Không cần cài .NET SDK trên Lightsail -# - Chạy trên host, trước khi up -d -# - Xem log CI để lấy lệnh ssh/scp chạy efbundle - -# 2. Manual fallback: chạy từ SDK container nếu cần debug -docker run --rm --env-file .env -v "$PWD/backend:/src/backend" \ - -w /src/backend mcr.microsoft.com/dotnet/sdk:9.0 \ - dotnet ef database update \ - --project MenuGreen.DataAccessLayer/MenuGreen.DataAccessLayer.csproj \ - --startup-project MenuGreen.API/MenuGreen.API.csproj - -# 3. Nếu lỗi liên quan connection string, kiểm tra lại: -# - ConnectionStrings__DefaultConnection -# - ConnectionStrings__Redis -# - JwtSettings__SecretKey / Issuer / Audience -``` - -### Redis không kết nối -```bash -docker exec menugreen_redis redis-cli ping -docker exec menugreen_api ping -c 1 menugreen_redis -``` - -### DB không kết nối từ container -```bash -# Test từ host -nc -zv 5432 - -# Test từ container -docker run --rm postgres:18-alpine pg_isready -h -p 5432 -``` - ---- - -## 8. Tài liệu tham khảo - -- [EF Core Applying Migrations - Microsoft Docs](https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/applying) -- [Stop Running dotnet ef database update in Production - ByteCrafted](https://bytecrafted.dev/ef-core-migrations-cicd-production/) -- [Running Migrations in EF Core 10 - codewithmukesh](https://codewithmukesh.com/blog/running-migrations-efcore/) -- [How to run EF Core migrations from Docker - anuraj.dev](https://anuraj.dev/blog/how-to-run-ef-core-migrations-from-docker/) -- [AWS Lightsail Container Services](https://docs.aws.amazon.com/lightsail/latest/userguide/amazon-lightsail-container-services-deployments.html) - ---- - -*Cập nhật: 01/07/2026* diff --git a/docs/01-deployment/DEPLOY_REVIEW.md b/docs/01-deployment/DEPLOY_REVIEW.md deleted file mode 100644 index 96dae9e1..00000000 --- a/docs/01-deployment/DEPLOY_REVIEW.md +++ /dev/null @@ -1,262 +0,0 @@ -# MenuGreen Deployment - Review & Gap Analysis - -## Tổng quan -Mình đã review toàn bộ codebase so với plan deploy. Kết quả: **plan có hướng đúng, nhưng có một số điểm đã được implement sẵn, và có bug thực tế cần fix ngay**. - ---- - -## 1. Current State Analysis (Code thực tế) - -### 1.1 CI/CD (`.github/workflows/ci-cd.yml`) -**Đã có sẵn:** -- Doppler secrets download + .env build logic ✅ -- Upload .env to server ✅ -- docker compose down/up flow ✅ -- Container cleanup before deploy ✅ -- Health check after deploy ✅ - -**Vấn đề thực tế:** -```yaml -# Line 294 - MIGRATION CHẠY TRONG CONTAINER API -docker compose -f docker-compose.prod.yml exec api dotnet ef database update \ - --project backend/MenuGreen.DataAccessLayer/MenuGreen.DataAccessLayer.csproj \ - --startup-project backend/MenuGreen.API/MenuGreen.API.csproj -``` -- **Container API không có dotnet-ef tool** → lệnh này sẽ fail -- Đây có thể là **root cause** của deployment failure bạn đã gặp - -### 1.2 `docker-compose.prod.yml` -**Đã có sẵn:** -- Redis với memory limit 256M ✅ -- Redis maxmemory policy ✅ -- Healthcheck cho Redis ✅ -- Healthcheck cho API ✅ -- External network menugreen-net ✅ - -**Thiếu:** -- Memory limit cho API container -- API mem_limit cần thêm ~600-800M cho 2GB RAM server - -### 1.3 `Dockerfile` -**Đã tối ưu:** -- Multi-stage build (sdk → publish → aspnet) ✅ -- Không có migration bundle trong image ✅ -- curl được cài cho healthcheck ✅ -- Chỉ ~200-300MB image size ✅ - -**Không cần thay đổi** theo plan. - -### 1.4 `Program.cs` -**Vấn đề CRITICAL - Redis config mismatch:** - -```csharp -// Line 45-47 - Program.cs ĐANG ĐỌC: -var redisConnection = - builder.Configuration["Redis:ConnectionString"] - ?? Environment.GetEnvironmentVariable("REDIS_URL"); - -// Nhưng CI đang TẠO .env với key: -ConnectionStrings__Redis=menugreen_redis:6379 - -// KẾT QUẢ: Redis connection = null → fallback to DistributedMemoryCache -// KHÔNG dùng Redis thật! Cache không hoạt động! -``` - -**Health checks đã đúng:** -- `/health` - tất cả checks ✅ -- `/health/ready` - chỉ ready tags ✅ -- `/health/live` - always healthy ✅ - -**Lưu ý khác:** -- Line 143: JWT có fallback secret key hardcoded - **security risk**, nên fix -- Line 270-278: HTTPS redirect + RateLimiter chỉ chạy non-development - -### 1.5 `scripts/deploy.sh` -**Có điểm tốt hơn CI hiện tại:** -- Database backup trước khi migrate ✅ -- Migration chạy bằng SDK container tạm (đúng best practice) ✅ -- Health check đầy đủ ✅ - -**Nhưng có vấn đề:** -- Dùng `docker run` thay vì `docker compose` → không dùng compose file -- Không giống CI flow → gây confusion -- Monitoring stack chỉ có trong deploy.sh, không trong CI - ---- - -## 2. So sánh Plan vs Reality - -| Điểm | Plan đề xuất | Code hiện tại | Trạng thái | -|------|-------------|---------------|------------| -| Migration bundle | Build efbundle trong CI, chạy trên host | Chạy `dotnet ef` trong container API | ❌ Sai - cần fix | -| Dockerfile tối giản | Xóa stage migration | Đã tối giản | ✅ OK | -| Redis connection | `ConnectionStrings:Redis` | CI tạo `ConnectionStrings__Redis` | ❌ Mismatch | -| Health check | `/health/live` trước, `/health/ready` sau | Chỉ check `/health/ready` | ⚠️ Cần cải thiện | -| Memory limits | Redis 256M, API 800M | Redis 256M, API không có | ⚠️ Cần thêm API limit | -| Config validation | Verify .env sau upload | Có verify Doppler secrets | ✅ OK | -| Pre-flight check | Script kiểm tra server | Chưa có | ⏳ Cần thêm | - ---- - -## 3. Critical Fixes Cần Làm Ngay - -### Fix 1: Redis Connection String Mismatch (CRITICAL) -**Vấn đề:** CI tạo `ConnectionStrings__Redis` nhưng Program.cs đọc `Redis:ConnectionString` - -**Giải pháp A (Khuyến nghị - sửa Program.cs):** -```csharp -// Thêm fallback cho ConnectionStrings:Redis -var redisConnection = - builder.Configuration["Redis:ConnectionString"] - ?? builder.Configuration["ConnectionStrings:Redis"] // Thêm dòng này - ?? Environment.GetEnvironmentVariable("REDIS_URL"); -``` - -**Giải pháp B (Đổi CI):** -```bash -# Trong ci-cd.yml, thay: -echo "ConnectionStrings__Redis=${REDIS_URL}" >> .env - -# Bằng: -echo "Redis__ConnectionString=${REDIS_URL}" >> .env -``` - -### Fix 2: Migration Strategy (CRITICAL) -**Vấn đề:** Migration chạy trong container không có SDK - -**Giải pháp:** Dùng efbundle như plan đề xuất, hoặc ít nhất là SDK container tạm: -```bash -# Trong CI, thay: -docker compose exec api dotnet ef database update ... - -# Bằng: -docker run --rm \ - --env-file "$APP_DIR/.env" \ - -v "$APP_DIR/backend:/src/backend" \ - -w /src/backend \ - mcr.microsoft.com/dotnet/sdk:9.0 \ - dotnet ef database update \ - --project MenuGreen.DataAccessLayer/MenuGreen.DataAccessLayer.csproj \ - --startup-project MenuGreen.API/MenuGreen.API.csproj -``` - -Hoặc tốt hơn: build efbundle trong CI như plan. - -### Fix 3: Memory Limit cho API -**Cần thêm vào `docker-compose.prod.yml`:** -```yaml - api: - # ... - deploy: - resources: - limits: - memory: 800M - cpus: '1.0' -``` - -### Fix 4: Health Check Strategy -**Cải thiện CI health check:** -```yaml -- name: Wait for API to be alive - run: | - echo "Waiting for API..." - for i in {1..90}; do - STATUS=$(curl -sf "http://${{ secrets.LIGHTSAIL_HOST }}:5000/health/live" -w "\n%{http_code}" || true) - CODE=$(echo "$STATUS" | tail -n1 || true) - if [ "$CODE" = "200" ]; then - echo "API is alive!" - break - fi - echo "Waiting... ($i/90) status=${CODE:-no_response}" - sleep 5 - done - -- name: Check API readiness - run: | - echo "Checking API readiness (best-effort)..." - curl -fsSL "http://${{ secrets.LIGHTSAIL_HOST }}:5000/health/ready" \ - || echo "Ready check failed - check DB/Redis connectivity" -``` - -### Fix 5: JWT Secret Fallback (Security) -**Program.cs line 143:** -```csharp -// ❌ HIỆN TẠI - có fallback hardcoded -var secretKey = builder.Configuration["JwtSettings:SecretKey"] ?? "super_secret_key_menu_green_1234567890_super_long"; - -// ✅ NÊN THAY - fail nếu không có secret -var secretKey = builder.Configuration["JwtSettings:SecretKey"]; -if (string.IsNullOrEmpty(secretKey)) -{ - throw new InvalidOperationException("JWT SecretKey is not configured"); -} -``` - ---- - -## 4. Plan Optimization Recommendations - -### Những gì đã tốt, giữ nguyên: -- Dockerfile đã tối giản, không cần sửa -- Redis memory limit đã có -- Health check endpoints đã đúng cấu trúc -- Doppler integration đã tốt -- docker-compose structure đã tốt - -### Những gì cần cập nhật trong plan: - -1. **Bỏ phần Dockerfile** - đã tối ưu sẵn -2. **Cập nhật Program.cs** - thêm Redis fallback -3. **Thay đổi migration strategy** - dùng efbundle hoặc SDK container -4. **Thêm memory limit cho API** trong compose -5. **Cải thiện health check** trong CI -6. **Thêm pre-flight script** vào repo -7. **Fix JWT secret** - bỏ fallback - ---- - -## 5. Execution Order (Revised) - -``` -IMMEDIATE FIXES (Trước khi deploy) -├── 1. Fix Redis connection trong Program.cs (thêm fallback) -├── 2. Thêm mem_limit cho API trong docker-compose.prod.yml -├── 3. Cải thiện health check trong ci-cd.yml -└── 4. Commit và push - -SERVER PREPARATION -├── 5. Provision Lightsail $12/tháng -├── 6. Chạy server_preflight_check.sh -├── 7. Cài Docker + Compose (nếu cần) -├── 8. Tạo network menugreen-net -├── 9. Config Firewall -└── 10. Tạo .env ban đầu - -CI/CD UPDATE -├── 11. Cập nhật migration strategy trong ci-cd.yml -├── 12. Thêm verify config trên server step -└── 13. Push lại - -FIRST DEPLOY -├── 14. CI tự deploy -├── 15. Verify containers + logs -├── 16. Test health endpoints -└── 17. Test API thực tế -``` - ---- - -## 6. Conclusion - -**Plan hiện tại có hướng đúng** nhưng cần điều chỉnh: -- **Đã tốt:** Dockerfile, Redis config, Doppler integration -- **Cần fix ngay:** Redis connection mismatch, migration trong container -- **Cần thêm:** API memory limit, pre-flight script, health check improvement - -**Ưu tiên hành động:** -1. Fix Redis config (bắt API thực sự dùng Redis) -2. Fix migration strategy (đừng chạy ef trong container) -3. Thêm memory limit cho API -4. Thêm pre-flight script - -Sau khi fix 4 điểm trên, deploy sẽ ổn định hơn rất nhiều. diff --git a/docs/01-deployment/DOPPLER_SETUP.md b/docs/01-deployment/DOPPLER_SETUP.md deleted file mode 100644 index b962eefd..00000000 --- a/docs/01-deployment/DOPPLER_SETUP.md +++ /dev/null @@ -1,203 +0,0 @@ -# Doppler Setup — MenuGreen System - -Log tiến độ và hướng dẫn dùng Doppler trong dự án này. - ---- - -## Trạng thái hiện tại - -| # | Hành động | Trạng thái | -|---|-----------|------------| -| 1 | Tạo project Doppler `menugreen` | ✅ | -| 2 | Thêm secrets vào config `prd` (Production) | ✅ | -| 3 | Thêm secrets vào config `dev` (Local development) | ✅ | -| 4 | Tạo Service Token cho config `prd` (Read-only) | ✅ | -| 5 | Thêm `DOPPLER_TOKEN` vào GitHub Secrets | ✅ | -| 6 | Cập nhật `.github/workflows/ci-cd.yml` để dùng Doppler | ✅ | -| 7 | Push code lên GitHub để chạy workflow thật | ⬜ | -| 8 | Xác minh deploy thành công | ⬜ | - ---- - -## Cấu trúc Doppler Project - -``` -Project: menugreen -├── Config: prd (Production) — secrets dùng cho CI/CD + server Lightsail -└── Config: dev (Development) — secrets dùng cho chạy local -``` - -### Secrets trong config `prd` - -| Secret | Mục đích | -|--------|----------| -| `DB_HOST` | Endpoint RDS PostgreSQL | -| `DB_PORT` | `5432` | -| `DB_NAME` | `MenuGreenDb` | -| `DB_USER` | `postgres` | -| `DB_PASSWORD` | Password RDS | -| `DB_SSL_MODE` | `Require` | -| `REDIS_HOST` | Host Redis | -| `REDIS_PORT` | `6379` | -| `REDIS_PASSWORD` | Password Redis | -| `JWT_SECRET` | Random key cho JWT | -| `JWT_ISSUER` | `MenuGreenAPI` | -| `JWT_AUDIENCE` | `MenuGreenApp` | -| `LIGHTSAIL_HOST` | IP Lightsail server | -| `LIGHTSAIL_USER` | `ubuntu` | -| `LIGHTSAIL_SSH_KEY` | Nội dung file `.pem` | - -### Secrets trong config `dev` - -| Secret | Giá trị gợi ý | -|--------|---------------| -| `DB_HOST` | `localhost` | -| `DB_PORT` | `5432` | -| `DB_NAME` | `MenuGreenDb` | -| `DB_USER` | `postgres` | -| `DB_PASSWORD` | `12345` | -| `DB_SSL_MODE` | `Prefer` | -| `REDIS_HOST` | `localhost` | -| `REDIS_PORT` | `6379` | -| `REDIS_PASSWORD` | (để trống nếu Redis local không có password) | -| `JWT_SECRET` | (random key) | -| `JWT_ISSUER` | `MenuGreenAPI` | -| `JWT_AUDIENCE` | `MenuGreenApp` | -| `LIGHTSAIL_HOST` | `localhost` (placeholder) | -| `LIGHTSAIL_USER` | `ubuntu` (placeholder) | -| `LIGHTSAIL_SSH_KEY` | `dummy` (placeholder) | - ---- - -## Luồng hoạt động CI/CD - -``` -GitHub Actions - │ - ├─ Đọc DOPPLER_TOKEN từ GitHub Secrets - │ - ▼ -Doppler API (project menugreen, config prd) - │ - └─ Trả về secrets: DB_HOST, JWT_SECRET, ... - │ - ▼ -appleboy/ssh-action inject env vars → Server Lightsail - │ - └─ Script tạo .env từ Doppler secrets - └─ docker run --env-file .env menugreen-api -``` - ---- - -## Dùng Doppler cho Local Development - -### Cài Doppler CLI - -```bash -# macOS -brew install dopplerhq/cli/doppler - -# Windows (PowerShell) -scoop install doppler - -# Linux -curl -Ls https://cli.doppler.com/install.sh | sh -``` - -### Liên kết project - -```bash -cd d:\University\Term8\EXE201\MenuGreenSystem\backend -doppler login -doppler setup -# Chọn project: menugreen -# Chọn config: dev -``` - -### Chạy API với Doppler - -```bash -doppler run -- dotnet run --project MenuGreen.API -``` - -### Kiểm tra secrets đã inject - -```bash -doppler run -- printenv | grep DB_ -``` - ---- - -## File đã sửa - -| File | Thay đổi | -|------|----------| -| `.github/workflows/ci-cd.yml` | Thêm bước `Setup Doppler CLI`, cập nhật `.env` từ Doppler secrets | - -### Thay đổi chính trong `ci-cd.yml` - -- Thêm step `Setup Doppler CLI` trước khi deploy -- `envs:` giờ bao gồm tất cả secrets từ Doppler: `DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD, DB_SSL_MODE, REDIS_HOST, REDIS_PORT, REDIS_PASSWORD, JWT_SECRET, JWT_ISSUER, JWT_AUDIENCE` -- Script SSH tạo `.env` với format `ConnectionStrings__DefaultConnection` và `JwtSettings__*` khớp với `appsettings.json` - ---- - -## Lưu ý bảo mật - -- **Không commit file `.env`** vào Git -- **GitHub Secrets** chỉ chứa `DOPPLER_TOKEN` và `LIGHTSAIL_*` (SSH) -- **Doppler dashboard** là nơi quản lý tất cả secrets ứng dụng -- Service Token chỉ có quyền **Read** config `prd` (least privilege) - ---- - -## Troubleshooting - -| Lỗi | Nguyên nhân | Cách fix | -|-----|-------------|----------| -| Doppler token expired/invalid | Secret `DOPPLER_TOKEN` sai | Tạo lại Service Token trong Doppler | -| Missing Doppler secret | Config `prd` thiếu key | Thêm key còn thiếu vào Doppler config `prd` | -| SSH fail | `LIGHTSAIL_*` secrets sai | Kiểm tra lại GitHub Secrets | -| RDS SSL error | `DB_SSL_MODE` sai | Đảm bảo config `prd` có `DB_SSL_MODE=Require` | -| JWT không nhận diện | `JWT_SECRET` thiếu hoặc sai key | Kiểm tra secret trong Doppler config `prd` | - ---- - -## Tiến độ tiếp theo - -- [ ] Push code và chạy GitHub Actions workflow -- [ ] Kiểm tra health endpoint sau deploy -- [ ] (Tuỳ chọn) Xóa các GitHub Secrets cũ: `DB_*`, `JWT_SECRET` sau khi Doppler chạy ổn - ---- - -_Cập nhật lần cuối: 30/06/2026_ - ---- - -## Log công việc đã làm - -### 2026-06-30: Import secrets vào Doppler config `prd` - -| Hành động | Trạng thái | -|-----------|-----------| -| Tạo file `.env` tổng hợp tất cả secrets Production | ✅ | -| Import secrets vào Doppler config `prd` qua Web UI | ✅ | -| Kiểm tra keys đã import đúng (JWT, Resend, SePay, Firebase, Redis, DB, CVService, NutritionAssistant) | ✅ | - -**Secrets đã import gồm:** -- `JWTSETTINGS__SECRETKEY`, `JWTSETTINGS__ISSUER`, `JWTSETTINGS__AUDIENCE`, `JWTSETTINGS__EXPIRYMINUTES` -- `RESEND__APIKEY`, `RESEND__FROMEMAIL`, `RESEND__FROMNAME` -- `SEPAY__WEBHOOKSECRET`, `SEPAY__WEBHOOKAUTHMODE`, `SEPAY__WEBHOOKTIMESTAMPTOLERANCESECONDS`, `SEPAY__PAYMENTCODEPREFIX`, `SEPAY__PAYMENTCODESUFFIXLENGTH`, `SEPAY__PAYMENTCODESUFFIXMINLENGTH`, `SEPAY__PAYMENTCODESUFFIXMAXLENGTH`, `SEPAY__ORDEREXPIRYMINUTES`, `SEPAY__QRIMAGEBASEURL` -- `SEPAY__BANKACCOUNT__ACCOUNTNUMBER`, `SEPAY__BANKACCOUNT__BANKNAME`, `SEPAY__BANKACCOUNT__ACCOUNTHOLDERNAME`, `SEPAY__BANKACCOUNT__TRANSFERDESCRIPTIONPREFIX` -- `FIREBASE__CREDENTIALPATH` -- `REDIS__CONNECTIONSTRING` -- `ALLOWEDORIGINS` -- `CONNECTIONSTRINGS__DEFAULTCONNECTION` -- `CVSERVICE__BASEURL`, `CVSERVICE__APISECRETKEY` -- `NUTRITIONASSISTANT__WORKERURL` - -**Format key đã chuẩn hóa:** -- Dùng `__` thay `:` cho nested keys (tương thích .NET + Doppler) -- Tất cả keys đều uppercase snake_case diff --git a/docs/01-deployment/NGINX_AND_CORS.md b/docs/01-deployment/NGINX_AND_CORS.md new file mode 100644 index 00000000..f7497a61 --- /dev/null +++ b/docs/01-deployment/NGINX_AND_CORS.md @@ -0,0 +1,309 @@ +# CORS & Nginx Configuration Guide + +> **Last updated:** 2026-07-11 — Workflow tự động qua CI/CD (không cần SSH để apply nginx). + +--- + +## Tổng quan + +MenuGreen API dùng **Nginx chạy trên host** (không Docker) để: +- **SSL termination** (Let's Encrypt) +- **Reverse proxy** → port 5000 (API trong Docker) +- **CORS handling** (whitelist origins qua dynamic map) + +Có 2 cách CORS được xử lý song song: + +1. **Nginx** (chính, whitelist qua `cors-map.conf`) +2. **.NET API** (fallback, qua `Program.cs` đọc `ALLOWEDORIGINS` env) + +--- + +## Kiến trúc Nginx (hiện tại) + +``` +Internet (HTTPS:443) + ↓ +[Nginx trên host] ← Cài qua apt install, chạy như systemd service + ├─ SSL terminate (Let's Encrypt) + ├─ CORS check (map $http_origin $cors_origin) + ├─ Rate limiting + └─ proxy_pass → http://localhost:5000 + ↓ + [Docker container: menugreen_api] + └─ .NET API + CORS middleware (fallback) +``` + +### Vị trí Nginx trong hệ thống + +| Thành phần | Vị trí | Quản lý bởi | +|------------|--------|--------------| +| Nginx binary | `/usr/sbin/nginx` (apt install) | System package | +| Nginx service | `systemctl status nginx` | systemd | +| Config source | `MenuGreenSystem/backend/nginx/` | Git repository | +| Config runtime | `/etc/nginx/nginx.conf` + `/etc/nginx/conf.d/` | CI/CD apply | +| Docker image `menugreensystem:main` | ❌ KHÔNG chứa nginx | — | + +> **Lưu ý:** Nginx là service ĐỘC LẬP trên host, tách biệt hoàn toàn khỏi Docker Image. Sửa nginx → chỉ cần push git → CI/CD apply (không build lại image). + +--- + +## File cấu hình trên server + +``` +/etc/nginx/ +├── nginx.conf # Main config +├── conf.d/ +│ └── cors-map.conf # ⭐ WHITELIST origins (thường sửa file này) +├── snippets/ # (placeholder, hiện chưa dùng) +├── sites-enabled/ +└── ssl/ # SSL certs (Let's Encrypt) +``` + +**Các file nguồn trong repo:** `MenuGreenSystem/backend/nginx/` + +``` +MenuGreenSystem/backend/nginx/ +├── nginx.conf # Source nginx.conf (copy về /etc/nginx/) +├── conf.d/ +│ └── cors-map.conf # Source CORS map (copy về /etc/nginx/conf.d/) +└── deploy/ + ├── deploy-nginx.sh # Script apply config + ├── setup-server.sh # Script setup lần đầu + └── README.md # Hướng dẫn chi tiết +``` + +--- + +## CORS Map — Hiện tại + +File `/etc/nginx/conf.d/cors-map.conf` (source: `MenuGreenSystem/backend/nginx/conf.d/cors-map.conf`): + +```nginx +map $http_origin $cors_origin { + default ""; + + # Production domains + "https://www.menugreen.food" "https://www.menugreen.food"; + "https://menugreen.food" "https://menugreen.food"; + "https://admin.menugreen.food" "https://admin.menugreen.food"; + + # Vercel preview + "https://menu-green-system-ldw5frytu-johnny-dangs-projects.vercel.app" + "https://menu-green-system-ldw5frytu-johnny-dangs-projects.vercel.app"; + + # Localhost dev + "http://localhost:3000" "http://localhost:3000"; + "http://localhost:3001" "http://localhost:3001"; + "http://localhost:5173" "http://localhost:5173"; + "http://127.0.0.1:3000" "http://127.0.0.1:3000"; + "http://127.0.0.1:5173" "http://127.0.0.1:5173"; +} +``` + +> **Cơ chế:** Nginx map `Origin` header → `$cors_origin` variable. Nếu origin KHÔNG trong whitelist → `$cors_origin = ""` → KHÔNG gửi `Access-Control-Allow-Origin` → browser block request. + +--- + +## Cách thêm domain mới vào CORS + +### Bước 1: Sửa file trên local + +Mở `MenuGreenSystem/backend/nginx/conf.d/cors-map.conf` bằng VS Code, thêm domain: + +```nginx +map $http_origin $cors_origin { + default ""; + + "https://www.menugreen.food" "https://www.menugreen.food"; + "https://menugreen.food" "https://menugreen.food"; + "https://admin.menugreen.food" "https://admin.menugreen.food"; + "https://staging.menugreen.food" "https://staging.menugreen.food"; ← MỚI THÊM + ... +} +``` + +### Bước 2: Commit + push + +```bash +git add backend/nginx/conf.d/cors-map.conf +git commit -m "feat(nginx): add staging.menugreen.food to CORS" +git push origin main +``` + +### Bước 3: CI/CD tự động apply lên server + +**Không cần SSH vào server.** GitHub Actions runner tự động: + +``` +1. backend-ci.yml — Build & push Docker image (3-5 phút) +2. backend-cd.yml — Deploy: + ├─ SCP file nginx: /tmp/nginx-deploy/ (vài giây) + ├─ SSH vào server, chạy script apply: + │ ├─ Backup config hiện tại (.bak.YYYYMMDD_HHMMSS) + │ ├─ Copy file mới → /etc/nginx/conf.d/cors-map.conf + │ ├─ nginx -t (syntax check) + │ │ ├─ PASS → systemctl reload nginx (zero downtime) + │ │ └─ FAIL → restore backup → exit 1 (abort toàn bộ deploy) + │ └─ Cleanup /tmp/nginx-deploy/ + └─ Pull image mới + restart container +``` + +> **Tổng thời gian:** 5-8 phút từ lúc push → nginx mới có hiệu lực. +> **Không cần build lại Docker image** khi chỉ sửa nginx (CD vẫn chạy nhưng image giữ nguyên). + +### Bước 4: Verify + +```bash +# Test preflight +curl -I -X OPTIONS https://api.menugreen.food/api/Auth/login \ + -H "Origin: https://staging.menugreen.food" \ + -H "Access-Control-Request-Method: POST" + +# Response phải có: +# Access-Control-Allow-Origin: https://staging.menugreen.food +``` + +### 4 trường hợp cập nhật Nginx + +| Trường hợp | Cách làm | Thời gian | +|------------|----------|-----------| +| **Thêm domain CORS** (phổ biến nhất) | Sửa `cors-map.conf` → push | 5-8 phút | +| **Sửa upstream / rate limit / security header** | Sửa `nginx.conf` → push | 5-8 phút | +| **Sửa code API + Nginx cùng lúc** | Sửa cả `.cs` + nginx → push | 5-8 phút (cả 2 update) | +| **Chỉ sửa nginx, không sửa code** | Sửa nginx → push | 5-8 phút (image giữ nguyên) | + +> ⚠️ **Lưu ý:** Sửa `nginx.conf` (không phải `cors-map.conf`) cần cẩn thận — workflow có auto rollback nếu syntax fail, nhưng nên test kỹ trước khi push. + +--- + +## Thay đổi trong Program.cs (CORS fallback) + +`Program.cs` cũng có CORS middleware đọc từ config `AllowedOrigins`: + +```csharp +builder.Services.AddCors(options => +{ + options.AddPolicy("AllowSpecificOrigins", policy => + { + policy.WithOrigins( + builder.Configuration["AllowedOrigins"] + ?.Split(',', StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty()) + .AllowAnyHeader() + .AllowAnyMethod() + .AllowCredentials(); + }); +}); +``` + +### Config qua env `ALLOWED_ORIGINS` + +Set trong Doppler config `prd`: + +``` +ALLOWEDORIGINS=https://www.menugreen.food,https://menugreen.food,https://admin.menugreen.food +``` + +CD workflow sẽ tự convert thành `ALLOWED_ORIGINS=...` trong file `.env` trên server. + +--- + +## Rate Limiting (trong nginx.conf) + +```nginx +# Rate limiting zones +limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; +limit_req_zone $binary_remote_addr zone=auth:1r/m; +limit_conn_zone $binary_remote_addr zone=addr:10m; + +# Apply +limit_req zone=api burst=20 nodelay; +limit_conn addr 10; +``` + +- API general: 10 req/s, burst 20 +- Auth endpoints: 1 req/min (chống brute force) +- Max 10 connections/IP + +--- + +## CORS Test Commands + +### Test preflight (OPTIONS) + +```bash +curl -I -X OPTIONS https://api.menugreen.food/api/Auth/login \ + -H "Origin: https://www.menugreen.food" \ + -H "Access-Control-Request-Method: POST" +``` + +**Response mong đợi:** +``` +HTTP/2 204 +access-control-allow-origin: https://www.menugreen.food +access-control-allow-methods: GET, POST, PUT, DELETE, OPTIONS, PATCH +access-control-allow-credentials: true +access-control-allow-headers: ... +access-control-max-age: 86400 +``` + +### Test actual request (GET) + +```bash +curl -I https://api.menugreen.food/health/live \ + -H "Origin: https://www.menugreen.food" +``` + +### Test domain KHÔNG có trong whitelist (expect fail) + +```bash +curl -I https://api.menugreen.food/api/Auth/login \ + -H "Origin: https://evil.com" +# → KHÔNG có access-control-allow-origin header +``` + +--- + +## Troubleshooting CORS + +### "No 'Access-Control-Allow-Origin' header" + +1. Check domain đã thêm vào `cors-map.conf` chưa +2. Verify exact match (kể cả `https://` vs `http://`) +3. Reload nginx: `sudo systemctl reload nginx` + +### Preflight (OPTIONS) failing + +1. `sudo nginx -t` để check syntax +2. Xem nginx logs: `sudo tail -20 /var/log/nginx/error.log` +3. Test bằng curl preflight (xem trên) + +### Domain Vercel preview + +Vercel preview URL thay đổi mỗi lần deploy. Có 2 options: + +**Option A:** Thêm URL cụ thể vào `cors-map.conf` sau mỗi Vercel deploy. + +**Option B:** Dùng `ALLOWEDORIGINS` env trong Doppler với pattern (không khuyến nghị vì CORS spec không support wildcard). + +Hiện tại: dùng Option A cho domain Vercel chính (`menu-green-system-ldw5frytu-johnny-dangs-projects.vercel.app`). + +--- + +## Security Notes + +- **Không dùng `*` cho `Access-Control-Allow-Origin`** khi `AllowCredentials=true` (browser sẽ reject). +- **Whitelist chỉ những domain thật sự dùng**, xóa các domain dev cũ không dùng nữa. +- **CORS ở Nginx** (chính) + **CORS ở .NET** (fallback) = double protection. +- **Luôn dùng HTTPS** cho production origins (không `http://`). + +--- + +## Liên quan + +- [ARCHITECTURE.md](./ARCHITECTURE.md) — Tổng quan deployment (workflow Nginx CI/CD) +- [CI_CD.md](./CI_CD.md) — CI/CD pipeline chi tiết +- `backend/nginx/deploy/setup-server.sh` — Setup lần đầu (1 lần duy nhất) + +--- + +*Last updated: 2026-07-11 — Workflow tự động qua CI/CD* diff --git a/docs/01-deployment/README.md b/docs/01-deployment/README.md new file mode 100644 index 00000000..456955fc --- /dev/null +++ b/docs/01-deployment/README.md @@ -0,0 +1,106 @@ +# MenuGreen — Deployment Documentation + +> **Cập nhật lần cuối:** 2026-07-11 + +Tài liệu về việc deploy backend MenuGreen API lên AWS Lightsail. + +--- + +## Mục lục + +Đọc theo thứ tự nếu bạn mới: + +| # | File | Mục đích | Đối tượng | +|---|-----------------------------------------------|-------------------------------------------------------------------|------------------------| +| 1 | [ARCHITECTURE.md](./ARCHITECTURE.md) | Kiến trúc tổng quan, GitHub Secrets, server info | Tất cả | +| 2 | [SERVER_SETUP.md](./SERVER_SETUP.md) | Setup server từ đầu (tạo VM, cài Docker, Nginx) | DevOps mới | +| 3 | [SECRETS_MANAGEMENT.md](./SECRETS_MANAGEMENT.md) | Quản lý secrets qua Doppler | DevOps | +| 4 | [CI_CD.md](./CI_CD.md) | Chi tiết CI/CD pipeline (13 bước deploy) | DevOps | +| 5 | [NGINX_AND_CORS.md](./NGINX_AND_CORS.md) | CORS & Nginx config (deploy tự động qua CI/CD) | Backend Dev + DevOps | + +--- + +## Quick Start (cho người mới) + +### Bạn muốn... + +**Setup server mới từ đầu?** +→ Đọc [SERVER_SETUP.md](./SERVER_SETUP.md) + +**Hiểu kiến trúc hệ thống + các services?** +→ Đọc [ARCHITECTURE.md](./ARCHITECTURE.md) + +**Hiểu cách deploy tự động hoạt động?** +→ Đọc [CI_CD.md](./CI_CD.md) + +**Thêm domain vào CORS whitelist?** +→ Đọc [NGINX_AND_CORS.md](./NGINX_AND_CORS.md) + +**Quản lý secrets (DB password, JWT, ...)?** +→ Đọc [SECRETS_MANAGEMENT.md](./SECRETS_MANAGEMENT.md) + +**Trigger deploy thủ công?** +→ GitHub → Actions → `Backend CD - Deploy` → `Run workflow` + +--- + +## Kiến trúc tổng quan + +``` +Internet + ↓ +[Nginx trên host:443] ← SSL + CORS + rate limit + ↓ +[Docker: menugreen_api:5000] ← .NET API + ↓ ↓ +[AWS RDS PostgreSQL] [Managed Redis] +``` + +**Workflow:** +1. Dev push code lên `main` +2. `backend-ci.yml`: build + push Docker image lên Docker Hub +3. `backend-cd.yml`: SCP nginx files + SSH vào Lightsail, pull Doppler secrets, deploy +4. Health check pass → live. Fail → auto-rollback. + +Xem chi tiết: [ARCHITECTURE.md](./ARCHITECTURE.md) + [CI_CD.md](./CI_CD.md) + +--- + +## Thông tin server hiện tại + +| Property | Value | +|-------------------|------------------------------------| +| **Provider** | AWS Lightsail | +| **Plan** | Small ($10/mo) - 2GB RAM | +| **Public IP** | `52.77.218.100` | +| **Domain** | `https://api.menugreen.food` | +| **App directory** | `/home/ubuntu/apps/menugreen` | +| **Container** | `menugreen_api` (port 5000) | +| **Database** | AWS RDS PostgreSQL | +| **Redis** | Managed (kết nối qua REDIS_URL) | +| **Nginx** | Trên host (`/etc/nginx/`) | +| **SSL** | Let's Encrypt (auto-renew) | + +--- + +## Files liên quan ngoài folder này + +| File | Mục đích | +|------------------------------------------------------------|-------------------------------| +| [`../GITHUB_SECRETS_SETUP.md`](../GITHUB_SECRETS_SETUP.md) | Setup GitHub Secrets chi tiết | +| [`../../backend/nginx/deploy/README.md`](../../backend/nginx/deploy/README.md) | Nginx deployment workflow | +| [`../../.github/workflows/backend-ci.yml`](../../.github/workflows/backend-ci.yml) | CI workflow (build image) | +| [`../../.github/workflows/backend-cd.yml`](../../.github/workflows/backend-cd.yml) | CD workflow (deploy) | + +--- + +## Thay đổi gần đây + +- **2026-07-11:** Tổ chức lại docs (8 → 6 file, archive removed). Merge DEPLOY.md + CI_CD.md overview thành ARCHITECTURE.md. Rename: cors-config → NGINX_AND_CORS, DOPPLER_SETUP → SECRETS_MANAGEMENT, lightsail-setup → SERVER_SETUP. +- **2026-07-11:** Cập nhật toàn bộ docs cho khớp với workflow thật (Nginx apply qua CI/CD, SCP + SSH, auto-migrate, managed Redis). +- **2026-07-01:** Fix Doppler secrets flow, JWT throw, Redis connection string. +- **2026-06-30:** Import secrets vào Doppler config `prd`. + +--- + +*Last updated: 2026-07-11* \ No newline at end of file diff --git a/docs/01-deployment/SECRETS_MANAGEMENT.md b/docs/01-deployment/SECRETS_MANAGEMENT.md new file mode 100644 index 00000000..c4185401 --- /dev/null +++ b/docs/01-deployment/SECRETS_MANAGEMENT.md @@ -0,0 +1,251 @@ +# Doppler Setup — MenuGreen System + +> **Last updated:** 2026-07-11 — Phản ánh workflow thật trong `backend-cd.yml`. + +--- + +## Trạng thái hiện tại + +| # | Hành động | Trạng thái | +|---|-------------------------------------------------------------|------------| +| 1 | Tạo project Doppler `menugreen` | ✅ | +| 2 | Thêm secrets vào config `prd` (Production) | ✅ | +| 3 | Thêm secrets vào config `dev` (Local development) | ✅ | +| 4 | Tạo Service Token cho config `prd` (Read-only) | ✅ | +| 5 | Thêm `DOPPLER_TOKEN` vào GitHub Secrets | ✅ | +| 6 | Cập nhật `.github/workflows/backend-cd.yml` để dùng Doppler | ✅ | +| 7 | Push code + workflow chạy thật | ✅ | +| 8 | Deploy thành công từ CI/CD | ✅ | + +--- + +## Cấu trúc Doppler Project + +``` +Project: menugreen +├── Config: prd (Production) — secrets cho CI/CD + server Lightsail +└── Config: dev (Development) — secrets cho chạy local +``` + +--- + +## Secrets trong config `prd` + +### Database (cho backup script + backup dùng) + +| Secret | Mục đích | +|----------------|---------------------------| +| `DB_HOST` | Endpoint RDS PostgreSQL | +| `DB_PORT` | `5432` | +| `DB_NAME` | `menugreendb` | +| `DB_USER` | `postgres` | +| `DB_PASSWORD` | Password RDS | +| `DB_SSL_MODE` | `Require` | + +### Redis (sẽ ghép thành `REDIS_URL` cho Program.cs) + +| Secret | Mục đích | +|------------------|--------------------------------| +| `REDIS_HOST` | Host Redis (managed) | +| `REDIS_PORT` | `6379` | +| `REDIS_PASSWORD` | Password Redis | + +### JWT (cho JwtSettings__*) + +| Secret | Mục đích | +|-----------------|-------------------------| +| `JWT_SECRET` | Random key cho JWT | +| `JWT_ISSUER` | `MenuGreenAPI` | +| `JWT_AUDIENCE` | `MenuGreenApp` | + +### SSH (chỉ để backup, không inject vào app) + +| Secret | Mục đích | +|---------------------|------------------------| +| `LIGHTSAIL_HOST` | IP Lightsail server | +| `LIGHTSAIL_USER` | `ubuntu` | +| `LIGHTSAIL_SSH_KEY` | Nội dung file `.pem` | + +### Connection strings (cho app) + +| Secret | Mục đích | +|------------------------------------------|-----------------------------------| +| `CONNECTIONSTRINGS__DEFAULTCONNECTION` | Full PostgreSQL connection string | +| `REDIS__CONNECTIONSTRING` | Redis connection (alternative) | + +### Email (Resend) + +| Secret | Mục đích | +|------------------------|---------------------| +| `RESEND__APIKEY` | Resend API key | +| `RESEND__FROMEMAIL` | Sender email | +| `RESEND__FROMNAME` | Sender display name | + +### SePay (Payment) + +| Secret | Mục đích | +|---------------------------------------------------|---------------------------| +| `SEPAY__WEBHOOKSECRET` | Webhook signature secret | +| `SEPAY__WEBHOOKAUTHMODE` | Auth mode | +| `SEPAY__WEBHOOKTIMESTAMPTOLERANCESECONDS` | Timestamp tolerance | +| `SEPAY__PAYMENTCODEPREFIX` | Payment code prefix | +| `SEPAY__PAYMENTCODESUFFIXLENGTH` | Suffix length | +| `SEPAY__PAYMENTCODESUFFIXMINLENGTH` | Min suffix length | +| `SEPAY__PAYMENTCODESUFFIXMAXLENGTH` | Max suffix length | +| `SEPAY__ORDEREXPIRYMINUTES` | Order expiry | +| `SEPAY__QRIMAGEBASEURL` | QR image base URL | +| `SEPAY__BANKACCOUNT__ACCOUNTNUMBER` | Bank account number | +| `SEPAY__BANKACCOUNT__BANKNAME` | Bank name | +| `SEPAY__BANKACCOUNT__ACCOUNTHOLDERNAME` | Account holder | +| `SEPAY__BANKACCOUNT__TRANSFERDESCRIPTIONPREFIX` | Transfer description prefix | + +### Firebase (FCM) + +| Secret | Mục đích | +|-----------------------------|-----------------------------| +| `FIREBASE__CREDENTIALPATH` | Path tới Firebase cred file | + +### Other services + +| Secret | Mục đích | +|-------------------------------------|--------------------------------| +| `CVSERVICE__BASEURL` | Computer Vision microservice | +| `CVSERVICE__APISECRETKEY` | CV service API key | +| `NUTRITIONASSISTANT__WORKERURL` | Nutrition AI worker URL | +| `ALLOWEDORIGINS` | CORS whitelist origins | +| `JWTSETTINGS__SECRETKEY` | Alternative JWT secret key | +| `JWTSETTINGS__ISSUER` | Alternative issuer | +| `JWTSETTINGS__AUDIENCE` | Alternative audience | +| `JWTSETTINGS__EXPIRYMINUTES` | Token expiry | + +> **Luồng CI/CD + chi tiết script build `.env`:** xem [CI_CD.md](./CI_CD.md#workflow-files-chi-tiết). + +--- + +## Dùng Doppler cho Local Development + +### Cài Doppler CLI + +```bash +# macOS +brew install dopplerhq/cli/doppler + +# Windows (PowerShell) +scoop install doppler + +# Linux +curl -Ls https://cli.doppler.com/install.sh | sh +``` + +### Liên kết project + +```bash +cd MenuGreenSystem/backend +doppler login +doppler setup +# Chọn project: menugreen +# Chọn config: dev +``` + +### Chạy API với Doppler + +```bash +cd MenuGreenSystem/backend +doppler run -- dotnet run --project MenuGreen.API +``` + +### Chạy docker-compose với Doppler + +```bash +# Cách 1: inject env vào .env rồi docker compose up +doppler secrets download --no-file --format env > .env +docker compose up -d + +# Cách 2: dùng doppler run với env_file +# (cần custom script vì env_file đọc từ file, không nhận trực tiếp env vars) +``` + +### Kiểm tra secrets đã inject + +```bash +doppler run -- printenv | grep DB_ +``` + +--- + +## File đã sửa (chỉ để tham khảo) + +| File | Thay đổi | +|--------------------------------------------|----------------------------------------------------------------| +| `.github/workflows/backend-cd.yml` | Dùng Doppler CLI để download secrets → build `.env` trên server | +| `.github/workflows/backend-ci.yml` | Build + push Docker image, không cần Doppler | + +--- + +## Lưu ý bảo mật + +- **Không commit file `.env`** vào Git (đã có `.gitignore`). +- **GitHub Secrets chỉ chứa `DOPPLER_TOKEN`**, `LIGHTSAIL_*` (SSH), `DOCKERHUB_*`. +- **Doppler dashboard** là nơi quản lý tất cả secrets ứng dụng (single source of truth). +- Service Token Doppler chỉ có quyền **Read** config `prd` (least privilege). +- Khi rotate secret: chỉ cần update trong Doppler, CD workflow pull secret mới ở deploy kế tiếp. + +--- + +## Troubleshooting + +| Lỗi | Nguyên nhân | Cách fix | +|------------------------------|------------------------------------------|------------------------------------------------| +| Doppler token expired/invalid| Secret `DOPPLER_TOKEN` sai/expired | Tạo lại Service Token trong Doppler dashboard | +| Missing Doppler secret | Config `prd` thiếu key | Thêm key còn thiếu vào Doppler config `prd` | +| SSH fail | `LIGHTSAIL_*` secrets sai | Kiểm tra lại GitHub Secrets | +| RDS SSL error | `DB_SSL_MODE` sai | Đảm bảo config `prd` có `DB_SSL_MODE=Require` | +| JWT không nhận diện | `JWT_SECRET` thiếu hoặc sai key | Kiểm tra secret trong Doppler config `prd` | +| `REDIS_URL` format sai | Thiếu `REDIS_HOST`/`REDIS_PORT`/`REDIS_PASSWORD` | Verify cả 3 secrets có trong Doppler | + +### Test Doppler CLI thủ công + +```bash +# Test download secrets +DOPPLER_TOKEN=dp.prd.xxx \ + doppler secrets download \ + --no-file \ + --project menugreen \ + --config prd \ + --format env +``` + +--- + +## Tiến độ tiếp theo + +- [✅] Push code và chạy GitHub Actions workflow thật +- [✅] Health endpoint OK sau deploy +- [x] (Tùy chọn) Xóa GitHub Secrets cũ `DB_*`, `JWT_SECRET` (đã làm — chỉ giữ `DOPPLER_TOKEN`) +- [ ] (Tương lai) Thêm Doppler config `stg` cho staging environment + +--- + +## Log công việc đã làm + +### 2026-06-30: Import secrets vào Doppler config `prd` + +- Tạo file `.env` tổng hợp tất cả secrets Production ✅ +- Import vào Doppler config `prd` qua Web UI ✅ +- Verify các keys: JWT, Resend, SePay, Firebase, Redis, DB, CVService, NutritionAssistant ✅ + +**Format key đã chuẩn hóa:** +- Dùng `__` thay `:` cho nested keys (tương thích .NET + Doppler) +- Tất cả keys uppercase snake_case + +### 2026-07-01: CI/CD fix — Doppler secrets flow đúng + +- **Fix 1:** Trước đó `.env` server thiếu `JwtSettings__SecretKey`, `Issuer`, `Audience` → fix bằng cách explicit build từ Doppler secrets ✅ +- **Fix 2:** Trước đó `ConnectionStrings__Redis` sai format → đổi sang `REDIS_URL` env (đúng format Program.cs đọc) ✅ +- **Fix 3:** Đổi từ `ci-cd.yml` (cũ, chạy efbundle) sang `backend-ci.yml` + `backend-cd.yml` (tách CI/CD) ✅ + +### 2026-07-11: Workflow tự động hoạt động ổn định + +- Backup DB tự động trước khi deploy ✅ +- Auto-rollback nếu health check fail ✅ +- Doppler secrets tự động inject đúng format ✅ diff --git a/docs/01-deployment/SERVER_SETUP.md b/docs/01-deployment/SERVER_SETUP.md new file mode 100644 index 00000000..f63d486a --- /dev/null +++ b/docs/01-deployment/SERVER_SETUP.md @@ -0,0 +1,422 @@ +# AWS Lightsail Setup Guide - MenuGreen + +> **Last updated:** 2026-07-11 — Phản ánh server production hiện tại. + +## Mục lục + +1. [Tạo AWS Account](#1-tạo-aws-account) +2. [Tạo Lightsail Instance](#2-tạo-lightsail-instance) +3. [Cấu hình Firewall](#3-cấu-hình-firewall) +4. [Kết nối SSH](#4-kết-nối-ssh) +5. [Cài đặt Docker](#5-cài-đặt-docker) +6. [Cài đặt Nginx](#6-cài-đặt-nginx) +7. [Cấu hình Server](#7-cấu-hình-server) +8. [Cấu hình Domain (Optional)](#8-cấu-hình-domain-optional) +9. [Setup SSL (Optional)](#9-setup-ssl-optional) + +--- + +## 1. Tạo AWS Account + +### Bước 1.1: Đăng ký AWS + +1. Truy cập: https://aws.amazon.com +2. Click **"Create an AWS Account"** +3. Điền thông tin: email, password, AWS account name +4. Chọn **"Personal"** account type +5. Điền thông tin cá nhân +6. Thêm thông tin thanh toán (Visa/Mastercard quốc tế) + - AWS charge $1 để verify thẻ (sẽ hoàn lại) +7. Xác minh danh tính qua phone +8. Chọn Support plan: **Basic (Free)** + +### Free Tier (3 tháng đầu) + +| Service | Free Tier | +| -------------- | ------------------------------------------- | +| Lightsail | 3 tháng đầu ($3.50-$10/month instance free) | +| RDS PostgreSQL | 750 giờ/tháng (db.t3.micro) | +| S3 | 5GB storage | + +--- + +## 2. Tạo Lightsail Instance + +### Bước 2.1: Truy cập Lightsail Console + +1. Login AWS Console: https://console.aws.amazon.com +2. Search "Lightsail" hoặc truy cập: https://lightsail.aws.amazon.com +3. Click **"Create instance"** + +### Bước 2.2: Cấu hình Instance (đang dùng) + +``` +Region: Asia Pacific (Singapore) - ap-southeast-1 +Blueprint: Ubuntu 22.04 LTS +Instance plan: $10/mo - Small + ├─ 2 GB RAM, 1 vCPU, 60 GB SSD + └─ 3 TB Transfer +Instance name: menugreen-server +``` + +### Bước 2.3: Đợi Instance khởi tạo (~2-5 phút) + +Status chuyển từ **Pending** → **Running**. + +### Thông tin server hiện tại + +| Property | Value | +|-------------------|----------------------------------------| +| **Public IP** | `52.77.218.100` | +| **Domain** | `api.menugreen.food` (A record → IP) | +| **OS** | Ubuntu 22.04 LTS | +| **RAM** | 2 GB | +| **Disk** | 60 GB SSD | +| **App directory** | `/home/ubuntu/apps/menugreen` | + +--- + +## 3. Cấu hình Firewall + +### Mở ports qua Lightsail Console + +Vào instance → tab **"Networking"** → **IPv4 Firewall** → **+ Add rule**: + +| Protocol | Port | Source | Mục đích | +|----------|------|--------------------|----------------------| +| SSH | 22 | My IP / 0.0.0.0/0 | SSH | +| HTTP | 80 | Anywhere | Nginx (redirect HTTPS) | +| HTTPS | 443 | Anywhere | Nginx SSL | + +> **Không cần mở port 5000** ở firewall — Nginx reverse proxy từ 80/443 → localhost:5000 (API trong Docker). + +> **Không cần mở port 5432** ở firewall — RDS ở AWS bên ngoài, kết nối qua internal network. + +### Lightsail Firewall mặc định (sau khi setup) + +``` +┌─────────────────────────────────────────────┐ +│ FIREWALL │ +├─────────────────────────────────────────────┤ +│ Protocol Port Source │ +│ ──────── ──── ────── │ +│ SSH TCP 22 Anywhere (0.0.0.0/0) │ +│ HTTP TCP 80 Anywhere (0.0.0.0/0) │ +│ HTTPS TCP 443 Anywhere (0.0.0.0/0) │ +└─────────────────────────────────────────────┘ +``` + +--- + +## 4. Kết nối SSH + +### Phương pháp khuyến nghị: Git Bash / Windows Terminal + +1. Download SSH key từ Lightsail: + - Lightsail → Account → SSH keys + - Download default key `LightsailDefaultKey.pem` + +2. Set permissions (Git Bash): + ```bash + chmod 400 ~/Downloads/LightsailDefaultKey.pem + ``` + +3. Connect: + ```bash + ssh -i ~/Downloads/LightsailDefaultKey.pem ubuntu@52.77.218.100 + ``` + +> **Lưu ý:** Key có thể tên khác (`LightsailDefaultKeyPair.pem`). Cần thêm nội dung file này vào GitHub Secret `LIGHTSAIL_SSH_KEY`. + +### Verify connection thành công + +``` +Welcome to Ubuntu 22.04.3 LTS (GNU/Linux 5.15.0-1051-aws x86_64) + + * Documentation: https://help.ubuntu.com + * Management: https://landscape.canonical.com + * Support: https://ubuntu.com/pro + +Last login: ... +ubuntu@menugreen-server:~$ +``` + +--- + +## 5. Cài đặt Docker + +```bash +sudo apt update && sudo apt upgrade -y + +# Install Docker +curl -fsSL https://get.docker.com -o get-docker.sh +sudo sh get-docker.sh + +# Add user ubuntu to docker group (không cần sudo cho docker) +sudo usermod -aG docker ubuntu + +# Logout và login lại để áp dụng group +exit + +# Re-login +ssh -i ~/Downloads/LightsailDefaultKey.pem ubuntu@52.77.218.100 + +# Verify +docker --version # Docker version 26.x.x +docker compose version # Docker Compose version v2.x.x +docker ps # Không cần sudo +``` + +--- + +## 6. Cài đặt Nginx + +> Nginx chạy **trực tiếp trên host** (không phải Docker container) để tiết kiệm RAM. + +```bash +# Cài Nginx +sudo apt install -y nginx certbot python3-certbot-nginx + +# Tạo folder snippets +sudo mkdir -p /etc/nginx/snippets +sudo mkdir -p /etc/nginx/sites-enabled +``` + +> **Cấu hình chi tiết Nginx (CORS, proxy, SSL):** xem [NGINX_AND_CORS.md](./NGINX_AND_CORS.md) và `MenuGreenSystem/backend/nginx/deploy/README.md`. + +--- + +## 7. Cấu hình Server + +### 7.1 App directory + +CD workflow tự tạo folder này. Không cần clone repo trên server. + +```bash +sudo mkdir -p /home/ubuntu/apps/menugreen +sudo chown ubuntu:ubuntu /home/ubuntu/apps/menugreen +``` + +### 7.2 GitHub Actions SSH access + +Thêm public key của GitHub Actions runner vào `~/.ssh/authorized_keys`: + +```bash +mkdir -p ~/.ssh +chmod 700 ~/.ssh + +# Hoặc dùng Lightsail default key + paste vào GitHub Secret LIGHTSAIL_SSH_KEY +``` + +### 7.3 Outbound connections cần thiết + +Server cần kết nối ra ngoài đến: + +| Destination | Port | Mục đích | +|--------------------------------|------|----------------------| +| `registry-1.docker.io` | 443 | Pull Docker image | +| `api.doppler.com` | 443 | Download secrets | +| `.rds.amazonaws.com` | 5432 | Kết nối PostgreSQL | +| `` | 6379 | Kết nối Redis (nếu managed) | + +Không cần mở ports cho outbound — Lightsail mặc định cho phép tất cả outbound. + +### 7.4 RDS Security Group + +Vào **AWS Console → RDS → menugreen-db → Connectivity & security → Security group** → Edit inbound rules: + +| Type | Protocol | Port | Source | +|-----------------|----------|------|-------------------| +| PostgreSQL | TCP | 5432 | `52.77.218.100/32` (IP Lightsail) | + +### 7.5 Verify server + +```bash +# Docker OK? +docker ps + +# Network outbound +curl -fsSL https://api.doppler.com > /dev/null && echo "Doppler OK" +nc -zv 5432 + +# Disk space +df -h +``` + +--- + +## 8. Cấu hình Domain (Optional - đã có api.menugreen.food) + +### Bước 8.1: Tạo Static IP + +1. Lightsail → **"Networking"** → **"Create static IP"** +2. Attach vào instance `menugreen-server` +3. Ghi nhớ Static IP + +### Bước 8.2: Point DNS về Lightsail + +Vào domain registrar (Namecheap/Cloudflare/GoDaddy), thêm A record: + +``` +api.menugreen.food → A → 52.77.218.100 +``` + +Đợi 5-30 phút để DNS propagate. + +--- + +## 9. Setup SSL (Let's Encrypt) + +```bash +# Lấy SSL certificate cho domain +sudo certbot --nginx -d api.menugreen.food + +# Làm theo prompts: +# - Enter email: your-email@example.com +# - Accept terms: A +# - Share email: N +# - Redirect HTTP to HTTPS: 2 (Redirect) +``` + +### Verify SSL + +```bash +sudo certbot certificates + +# Test renewal +sudo certbot renew --dry-run + +# Auto-renewal đã setup sẵn bởi certbot +sudo systemctl status certbot.timer +``` + +--- + +## Checklist Setup Hoàn chỉnh + +``` +┌──────────────────────────────────────────────────────┐ +│ MENUGREEN SERVER SETUP CHECKLIST │ +├──────────────────────────────────────────────────────┤ +│ │ +│ AWS Account: │ +│ [✅] Đăng ký AWS account │ +│ [✅] Verify credit card │ +│ │ +│ Lightsail Instance: │ +│ [✅] Tạo instance Ubuntu 22.04, Small $10 │ +│ [✅] Mở firewall: 22, 80, 443 │ +│ [✅] Static IP: 52.77.218.100 │ +│ │ +│ SSH: │ +│ [✅] Download SSH key │ +│ [✅] Test connect thành công │ +│ [✅] LIGHTSAIL_SSH_KEY paste vào GitHub Secrets │ +│ │ +│ Software: │ +│ [✅] Docker installed │ +│ [✅] Docker Compose plugin │ +│ [✅] Nginx installed │ +│ [✅] Certbot installed │ +│ │ +│ Database (RDS): │ +│ [✅] PostgreSQL RDS created │ +│ [✅] Security group allow IP Lightsail │ +│ [✅] Database `menugreendb` exists │ +│ │ +│ App directory: │ +│ [✅] /home/ubuntu/apps/menugreen exists │ +│ [✅] Owned by ubuntu:ubuntu │ +│ │ +│ GitHub Secrets: │ +│ [✅] DOPPLER_TOKEN │ +│ [✅] LIGHTSAIL_HOST, USER, SSH_KEY │ +│ [✅] DOCKERHUB_USERNAME, TOKEN │ +│ │ +│ Domain & SSL: │ +│ [✅] api.menugreen.food A record → 52.77.218.100 │ +│ [✅] SSL Let's Encrypt (auto-renew) │ +│ │ +│ First deploy: │ +│ [✅] GitHub Actions backend-ci + backend-cd pass │ +│ [✅] Container `menugreen_api` running │ +│ [✅] Health check OK │ +│ │ +└──────────────────────────────────────────────────────┘ +``` + +--- + +## Chi phí + +| Item | Cost | +|---------------------------------|---------------------------------| +| AWS Lightsail Small (2GB) | $10/tháng | +| AWS RDS db.t3.micro | ~$15/tháng (free tier hết) | +| Domain `menugreen.food` | ~$10/năm | +| SSL Let's Encrypt | Miễn phí | +| **Total Month** | **~$25-30/tháng** | + +--- + +## Troubleshooting + +### Không SSH được? + +```bash +# Kiểm tra Security Group Lightsail +Lightsail → Instance → Networking → Firewall (đảm bảo có port 22) + +# Reset SSH keys +Lightsail → Account → SSH keys → Reset +``` + +### Docker pull fail? + +```bash +# Test outbound +curl -fsSL https://registry-1.docker.io/v2/ > /dev/null && echo "OK" + +# Login Docker Hub (nếu image private) +sudo docker login -u anhtuan21112004 +``` + +### Nginx không start? + +```bash +sudo nginx -t # Check syntax +sudo systemctl status nginx +sudo tail -50 /var/log/nginx/error.log +``` + +### RDS không kết nối? + +```bash +# Test từ server +nc -zv 5432 +PGPASSWORD=xxx psql -h -U postgres -d menugreendb +``` + +### Disk đầy? + +```bash +# Cleanup Docker +sudo docker system prune -af --volumes + +# Xem dung lượng lớn +sudo du -sh /var/log/* +sudo du -sh /tmp/* +``` + +--- + +## Support Links + +- AWS Lightsail Docs: https://docs.aws.amazon.com/lightsail/ +- Docker Docs: https://docs.docker.com/ +- Let's Encrypt: https://letsencrypt.org/docs/ +- Ubuntu Server Guide: https://ubuntu.com/server/docs + +--- + +**Tiếp theo:** Sau khi setup xong, xem [ARCHITECTURE.md](./ARCHITECTURE.md) để hiểu kiến trúc và [CI_CD.md](./CI_CD.md) để hiểu flow deploy. diff --git a/docs/01-deployment/cors-config.md b/docs/01-deployment/cors-config.md deleted file mode 100644 index b3cd331c..00000000 --- a/docs/01-deployment/cors-config.md +++ /dev/null @@ -1,117 +0,0 @@ -# CORS Configuration Guide - -**Last updated:** 2026-07-09 - -## Overview - -MenuGreen API uses CORS (Cross-Origin Resource Sharing) to allow frontend applications to access API endpoints. - -## Default Allowed Origins - -The following origins are allowed by default in production: - -| Environment | Origin | Description | -|-------------|--------|-------------| -| Production | `https://www.menugreen.food` | Main website | -| Production | `https://menugreen.food` | Website (non-www) | -| Production | `https://menu-green-system-ldw5frytu-johnny-dangs-projects.vercel.app` | Vercel preview | -| Development | `http://localhost:3000` | Local Next.js | -| Development | `http://localhost:3001` | Local alternative port | - -## How to Add New Origins - -### Option 1: Environment Variable (Recommended for Production) - -Set the `ALLOWED_ORIGINS` environment variable: - -```bash -# Single origin -ALLOWED_ORIGINS=https://newdomain.com - -# Multiple origins (comma-separated) -ALLOWED_ORIGINS=https://www.menugreen.food,https://newdomain.com,https://staging.menugreen.food -``` - -### Option 2: Configuration File - -In `appsettings.Production.json`: - -```json -{ - "AllowedOrigins": "https://www.menugreen.food,https://newdomain.com" -} -``` - -### Option 3: Code (Not Recommended) - -Edit `Program.cs` and add to the `defaultOrigins` array: - -```csharp -var defaultOrigins = new[] -{ - "https://www.menugreen.food", - "https://menugreen.food", - "https://your-new-origin.com" // Add here -}; -``` - -## Development vs Production - -### Development Environment -- CORS is set to `AllowAnyOrigin` -- All origins, headers, and methods are allowed -- Credentials are allowed - -### Production Environment -- Only specific origins are allowed -- All methods and headers are allowed -- Credentials are allowed - -## Troubleshooting - -### CORS Error: "No 'Access-Control-Allow-Origin' header" - -1. Check if the origin is in the allowed list -2. Verify environment variable is set correctly on the server -3. Restart the application after changing CORS config - -### Preflight (OPTIONS) Request Failing - -The API handles preflight requests automatically. If preflight fails: - -1. Check Nginx is not blocking OPTIONS requests -2. Verify the origin matches exactly (including https://) - -### Vercel Domain Issues - -When deploying to Vercel: -1. Add the Vercel preview domain to `ALLOWED_ORIGINS` -2. After custom domain is configured, add it to the list - -## Testing CORS - -### Test with curl - -```bash -# Test preflight request -curl -I -X OPTIONS https://api.menugreen.food/api/Auth/login \ - -H "Origin: https://www.menugreen.food" \ - -H "Access-Control-Request-Method: POST" - -# Expected response should include: -# Access-Control-Allow-Origin: https://www.menugreen.food -``` - -### Test in Browser - -1. Open DevTools (F12) -2. Go to Network tab -3. Make the API request -4. Check Response Headers for `Access-Control-Allow-Origin` - -## Security Notes - -- Never use `AllowAnyOrigin` in production -- Always use `https://` for production origins -- Include both `www` and non-www versions if needed -- Regularly review and remove unused origins diff --git a/docs/01-deployment/lightsail-setup.md b/docs/01-deployment/lightsail-setup.md deleted file mode 100644 index 9a3413bb..00000000 --- a/docs/01-deployment/lightsail-setup.md +++ /dev/null @@ -1,685 +0,0 @@ -# AWS Lightsail Setup Guide - MenuGreen - -## Mục lục - -1. [Tạo AWS Account](#1-tạo-aws-account) -2. [Tạo Lightsail Instance](#2-tạo-lightsail-instance) -3. [Cấu hình Firewall](#3-cấu-hình-firewall) -4. [Kết nối SSH](#4-kết-nối-ssh) -5. [Cài đặt Docker](#5-cài-đặt-docker) -6. [Deploy MenuGreen](#6-deploy-menugreen) -7. [Cấu hình Domain (Optional)](#7-cấu-hình-domain-optional) -8. [Setup SSL (Optional)](#8-setup-ssl-optional) - ---- - -## 1. Tạo AWS Account - -### Bước 1.1: Đăng ký AWS - -1. Truy cập: https://aws.amazon.com -2. Click **"Create an AWS Account"** -3. Điền thông tin: - - Email address - - Password - - AWS account name -4. Chọn **"Personal"** account type -5. Điền thông tin cá nhân -6. Thêm thông tin thanh toán (Credit card) - - **Lưu ý**: Cần có thẻ tín dụng/ghi nợ quốc tế (Visa, Mastercard) - - AWS sẽ charge $1 để verify thẻ (sẽ hoàn lại) -7. Xác minh danh tính qua phone -8. Chọn Support plan: **Basic (Free)** - -### Bước 1.2: Mặc định có gì miễn phí? - -| Service | Free Tier | -| -------------- | ------------------------------------------- | -| Lightsail | 3 tháng đầu ($3.50-$10/month instance free) | -| RDS PostgreSQL | 750 giờ/tháng (db.t3.micro) | -| S3 | 5GB storage | -| CloudWatch | 10 metrics | - ---- - -## 2. Tạo Lightsail Instance - -### Bước 2.1: Truy cập Lightsail Console - -1. Login AWS Console: https://console.aws.amazon.com -2. Search "Lightsail" hoặc truy cập: https://lightsail.aws.amazon.com -3. Click **"Create instance"** - -### Bước 2.2: Cấu hình Instance - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ CREATE AN INSTANCE │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ Location: │ -│ ├─ Region: Asia Pacific (Singapore) hoặc Asia Pacific (Tokyo) │ -│ └─ Availability Zone: Any (single zone OK for now) │ -│ │ -│ Instance image: │ -│ └─ Platform: Linux/Unix │ -│ └─ Blueprint: Ubuntu 22.04 LTS │ -│ └─ [✅] Include launch scripts (optional - skip for now) │ -│ │ -│ Instance plan: │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ $3.50/mo - Nano [CURRENTLY FREE 3 MO] │ │ -│ │ ├─ 512 MB RAM, 1 vCPU, 20 GB SSD │ │ -│ │ └─ 1 TB Transfer │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ $5/mo - Micro [CURRENTLY FREE 3 MO] │ │ -│ │ ├─ 1 GB RAM, 1 vCPU, 40 GB SSD │ │ -│ │ └─ 2 TB Transfer │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ $10/mo - Small ★ RECOMMENDED [CURRENTLY FREE 3 MO] │ │ -│ │ ├─ 2 GB RAM, 1 vCPU, 60 GB SSD │ │ -│ │ └─ 3 TB Transfer │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ │ -│ Identify your instance: │ -│ └─ Instance name: menugreen-server │ -│ │ -│ [Create instance] │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -### Bước 2.3: Giải thích các Plans - -| Plan | RAM | vCPU | SSD | Transfer | Phù hợp | -| ------------- | ------- | ----- | -------- | -------- | ----------------------------- | -| Nano $3.5 | 512MB | 1 | 20GB | 1TB | Demo, test | -| Micro $5 | 1GB | 1 | 40GB | 2TB | Light production | -| **Small $10** | **2GB** | **1** | **60GB** | **3TB** | **✅ Production (~5k users)** | - -**Khuyến nghị**: Chọn **Small $10** (hiện tại free 3 tháng đầu) - -### Bước 2.4: Đợi Instance khởi tạo - -- Thời gian: ~2-5 phút -- Status sẽ chuyển từ "Pending" → "Running" - ---- - -## 3. Cấu hình Firewall - -### Bước 3.1: Mở Firewall Ports - -1. Trong Lightsail console, click vào instance vừa tạo -2. Click tab **"Networking"** -3. Firewall hiện tại: - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ FIREWALL │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ IPv6 Firewall │ -│ ┌─────────────────┬────────────────┬─────────────────────────────────┐ │ -│ │ Protocol │ Port │ Source │ │ -│ ├─────────────────┼────────────────┼─────────────────────────────────┤ │ -│ │ SSH │ TCP 22 │ Anywhere (0.0.0.0/0) │ │ -│ │ HTTP │ TCP 80 │ Anywhere (0.0.0.0/0) │ │ -│ │ HTTPS │ TCP 443 │ Anywhere (0.0.0.0/0) │ │ -│ └─────────────────┴────────────────┴─────────────────────────────────┘ │ -│ │ -│ [ + Add rule ] [ + Another rule ] │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -### Bước 3.2: Thêm Rules cần thiết - -Click **"+ Add rule"** và thêm: - -| Protocol | Port | Source | -| -------- | -------- | ------------------------------------ | -| Custom | TCP 3000 | Anywhere | -| Custom | TCP 8080 | Anywhere | -| Custom | TCP 9090 | Anywhere | -| Custom | TCP 2375 | My IP (Docker management - tạm thời) | - -**Sau khi setup xong, nên restrict port 2375 về My IP** - ---- - -## 4. Kết nối SSH - -### Phương 1: Lightsail Browser SSH (Dễ nhất) - -1. Trong Lightsail console, click instance -2. Click **"Connect using SSH"** -3. Terminal sẽ mở trong browser - -### Phương 2: PuTTY (Windows) - -1. Download PuTTY: https://www.putty.org/ -2. Download private key: - - Click instance → **"Account"** → **"SSH keys"** - - Download default key hoặc create new -3. Convert .pem to .ppk (nếu cần): - - Mở PuTTYgen → Load .pem file → Save private key -4. Connect với PuTTY: - - Host: Public IP của instance - - Port: 22 - - Connection → SSH → Auth → Browse .ppk file - -### Phương 3: Windows Terminal / Git Bash (Khuyến nghị) - -1. Download SSH key: - - Lightsail → Account → SSH keys - - Download default key (e.g., `LightsailDefaultKey.pem`) - -2. Connect: - -```bash -# Set permissions cho key file -chmod 400 ~/Downloads/LightsailDefaultKey.pem - -# Connect -ssh -i ~/Downloads/LightsailDefaultKey.pem ubuntu@ -``` - -### Bước 4.1: Lấy Public IP - -1. Trong Lightsail console, click instance -2. Copy **Public IP** (ví dụ: `54.123.45.67`) - -### Bước 4.2: Test Connection - -```bash -ssh -i ~/Downloads/LightsailDefaultKey.pem ubuntu@54.123.45.67 - -# Nếu hỏi "Are you sure you want to continue connecting?" -# Gõ: yes -``` - -### Bước 4.3: Verify Connection - -``` -Welcome to Ubuntu 22.04.3 LTS (GNU/Linux 5.15.0-1051-aws x86_64) - - * Documentation: https://help.ubuntu.com - * Management: https://landscape.canonical.com - * Support: https://ubuntu.com/pro - - System information as of Mon Jun 29 10:00:00 UTC 2026 - - 0 updates can be applied immediately. - -Last login: Mon Jun 29 09:00:00 2026 from 203.0.113.1 -ubuntu@menugreen-server:~$ -``` - ---- - -## 5. Cài đặt Docker - -### Bước 5.1: Update System - -```bash -sudo apt update && sudo apt upgrade -y -``` - -### Bước 5.2: Install Docker - -```bash -# Install Docker -curl -fsSL https://get.docker.com -o get-docker.sh -sudo sh get-docker.sh - -# Add user to docker group (không cần sudo cho docker) -sudo usermod -aG docker ubuntu - -# Verify Docker installation -docker --version -# Output: Docker version 26.x.x -``` - -### Bước 5.3: Install Docker Compose - -```bash -# Install Docker Compose -sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose - -# Make executable -sudo chmod +x /usr/local/bin/docker-compose - -# Verify -docker-compose --version -# Output: Docker Compose version v2.x.x -``` - -### Bước 5.4: Logout và Login lại - -```bash -# Thoát SSH -exit - -# Login lại để áp dụng docker group -ssh -i ~/Downloads/LightsailDefaultKey.pem ubuntu@54.123.45.67 - -# Verify không cần sudo -docker ps -# Output: CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES -``` - ---- - -## 6. Deploy MenuGreen - -### Bước 6.1: Cài đặt Git - -```bash -sudo apt install -y git -``` - -### Bước 6.2: Clone Project - -```bash -# Tạo directory cho app -mkdir -p apps && cd apps - -# Clone project (thay URL bằng repo của bạn) -git clone https://github.com/your-username/MenuGreenSystem.git - -# Vào directory -cd MenuGreenSystem -``` - -### Bước 6.3: Tạo Docker Network - -```bash -docker network create menugreen-net -``` - -### Bước 6.4: Configure Environment Variables - -```bash -# Copy template .env.example -cp .env.example .env - -# Edit .env với giá trị của bạn -nano .env - -# Hoặc generate htpasswd password cho monitoring -cd monitoring/scripts -chmod +x generate-password.sh -./generate-password.sh admin -# Nhập password mới khi được yêu cầu - -# Copy output vào monitoring/nginx/.htpasswd -cd ../nginx -nano .htpasswd -# Paste nội dung đã generate - -# Quay lại root directory -cd ~/apps/MenuGreenSystem -``` - -**Lưu ý quan trọng:** -- File `.env` KHÔNG được commit lên Git (đã có trong `.gitignore`) -- Chỉ cần điền các biến trong `.env`, `docker-compose.yml` đã dùng `${VAR}` rồi -- Grafana password được lấy từ `GF_SECURITY_ADMIN_PASSWORD` trong `.env` -- Nginx basic auth password được lấy từ `NGINX_BASIC_AUTH_PASSWORD` trong `.env` - -### Bước 6.5: Sử dụng Docker Compose Production (Khuyến nghị) - -Dự án đã cung cấp file `docker-compose.prod.yml` để deploy production: - -**Tại sao dùng `docker-compose.prod.yml`?** -- Chỉ deploy services cần thiết (API + DB + Redis) -- Port DB/Redis chỉ bind localhost (`127.0.0.1`) để tăng security -- Không bao gồm monitoring stack trong deploy chính (monitoring sẽ setup sau) -- Giảm attack surface và tài nguyên server - -```bash -# Build image -docker-compose -f docker-compose.prod.yml build --no-cache - -# Start services -docker-compose -f docker-compose.prod.yml up -d - -# Xem logs -docker-compose -f docker-compose.prod.yml logs -f -``` - -### Bước 6.6: Deploy bằng Script (Tự động hóa) - -```bash -# Download deploy script (hoặc đã có sẵn trong repo) -cd ~/apps/MenuGreenSystem/scripts - -# Chmod +x -chmod +x deploy.sh - -# Chạy deploy (cần sudo) -sudo ./deploy.sh -``` - -Script sẽ tự động: -- Kiểm tra Docker + Docker Compose -- Tạo Docker network nếu chưa có -- Build images -- Start DB/Redis, chờ DB sẵn sàng -- Chạy EF migrations (nếu dotnet CLI có sẵn) -- Start API -- Chạy health check - -### Bước 6.7: Deploy thủ công (nếu cần) - -```bash -cd ~/apps/MenuGreenSystem - -# Build image -docker-compose -f docker-compose.prod.yml build - -# Start services theo thứ tự -docker-compose -f docker-compose.prod.yml up -d db redis - -# Chờ DB ready (kiểm tra) -docker-compose -f docker-compose.prod.yml exec db pg_isready -U postgres - -# Chạy EF migrations (nếu cần) -cd backend/MenuGreen.API -dotnet ef database update --no-build -cd ~/apps/MenuGreenSystem - -# Start API -docker-compose -f docker-compose.prod.yml up -d api -``` - -### Bước 6.8: Verify Services - -```bash -# Kiểm tra container status -docker-compose -f docker-compose.prod.yml ps - -# Output mong đợi: -# NAME IMAGE COMMAND SERVICE -# menugreen_db postgres:15-alpine "docker-entrypoint..." db -# menugreen_api menugreen_api "dotnet ..." api -# menugreen_redis redis:7-alpine "redis-server ..." redis - -# Test API -curl http://localhost:5000/health - -# Test qua Nginx (nếu đã setup domain) -curl http://your-domain.com/health -``` - ---- - -## 7. Cấu hình Domain (Optional) - -### Bước 7.1: Mua Domain (nếu chưa có) - -Mua domain tại: - -- Namecheap (~$10/năm) -- GoDaddy (~$12/năm) -- Google Domains (~$12/năm) -- Cloudflare Registrar (~$9/năm) - -### Bước 7.2: Tạo Static IP tĩnh - -1. Trong Lightsail console → **"Networking"** -2. Click **"Create static IP"** -3. Attach vào instance của bạn -4. **Quan trọng**: Ghi nhớ Static IP - -### Bước 7.3: Point DNS về Lightsail - -1. Vào domain registrar (nơi bạn mua domain) -2. Tìm DNS settings -3. Thêm records: - -| Type | Name | Value | -| ----- | ---- | ------------------------------------------ | -| A | @ | `` | -| A | www | `` | -| CNAME | @ | `.singapore.cloudapp.azure.com` | - -**Ví dụ với Cloudflare:** - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ DNS Settings - yourdomain.com │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ Type Name Content Proxy status TTL │ -│ ───── ──── ────────────────────── ──────────── ──── │ -│ A @ 54.123.45.67 DNS only Auto │ -│ A www 54.123.45.67 DNS only Auto │ -│ │ -│ [ + Add record ] │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -4. Đợi 5-30 phút để DNS propagate - -### Bước 7.4: Test Domain - -```bash -# Thay your-domain.com bằng domain thật -curl http://your-domain.com/health -``` - ---- - -## 8. Setup SSL (Let's Encrypt) - -### Bước 8.1: Cài đặt Certbot - -```bash -sudo apt install -y certbot python3-certbot-nginx -``` - -### Bước 8.2: Lấy SSL Certificate - -```bash -sudo certbot --nginx -d your-domain.com -d www.your-domain.com - -# Làm theo prompts: -# Enter email: your-email@example.com -# Accept terms: A -# Share email: N -# Redirect HTTP to HTTPS: 2 (Redirect) -``` - -### Bước 8.3: Verify SSL - -```bash -# Test SSL certificate -curl https://your-domain.com/health - -# Check certificate expiration -sudo certbot certificates - -# Output: -# Certificate name: your-domain.com -# Valid from: Mon Jun 29 10:00:00 2026 -# Valid until: Sun Sep 27 10:00:00 2026 -# SSL Grade: A+ -``` - -### Bước 8.4: Auto-renewal (Certbot tự làm) - -```bash -# Test renewal -sudo certbot renew --dry-run - -# Kiểm tra cron job đã được tạo -sudo systemctl status certbot.timer -``` - ---- - -## 9. Setup Alerts (Cuối cùng) - -### Bước 9.1: Cài đặt Alert Script Dependencies - -```bash -sudo apt install -y bc mailutils curl -``` - -### Bước 9.2: Configure Alerts - -```bash -cd ~/apps/MenuGreenSystem/monitoring/scripts -nano alert.sh - -# Sửa các dòng sau: -ALERT_EMAIL="your-email@example.com" -``` - -### Bước 9.3: Setup Cron Job - -```bash -# Edit crontab -crontab -e - -# Thêm dòng này (every 5 minutes): -*/5 * * * * /home/ubuntu/apps/MenuGreenSystem/monitoring/scripts/alert.sh >> /var/log/menugreen-alert.log 2>&1 - -# Save và exit -``` - ---- - -## Checklist Setup Hoàn chỉnh - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ MENUGREEN SETUP CHECKLIST │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ AWS Account: │ -│ [ ] Đăng ký AWS account │ -│ [ ] Verify credit card │ -│ │ -│ Lightsail Instance: │ -│ [ ] Tạo instance (Ubuntu 22.04, Small $10) │ -│ [ ] Mở firewall ports (80, 443, 3000, 8080, 9090) │ -│ [ ] Tạo static IP (optional nhưng khuyến nghị) │ -│ │ -│ SSH Connection: │ -│ [ ] Download SSH key │ -│ [ ] Connect thành công │ -│ │ -│ Docker: │ -│ [ ] Cài Docker │ -│ [ ] Cài Docker Compose │ -│ [ ] Test docker ps │ -│ │ -│ MenuGreen Deployment: │ -│ [ ] Clone project │ -│ [ ] Tạo Docker network │ -│ [ ] docker-compose up -d │ -│ [ ] Verify tất cả containers running │ -│ [ ] Test /health endpoint │ -│ │ -│ Domain & SSL: │ -│ [ ] Point DNS về Lightsail IP │ -│ [ ] Setup SSL với Let's Encrypt │ -│ [ ] Test HTTPS │ -│ │ -│ Monitoring: │ -│ [ ] Setup monitoring sau khi deploy production thành công │ -│ │ -│ Security: │ -│ [ ] Đổi all default passwords │ -│ [ ] Close port 2375 (Docker) │ -│ [ ] Setup firewall rules restrictively │ -│ [ ] Backup credentials │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Chi phí - -| Item | Cost | -| ----------------------------- | ------------------------------- | -| AWS Lightsail Small (2GB RAM) | **$0** (Free 3 tháng đầu) | -| Sau 3 tháng | $10/tháng | -| Domain | ~$10-15/năm | -| SSL | Miễn phí (Let's Encrypt) | -| **Total Year 1** | **~$20-30** (chủ yếu là domain) | - ---- - -## Troubleshooting - -### Không kết nối được SSH? - -```bash -# Kiểm tra Security Groups -Lightsail → Instance → Networking → Firewall - -# Kiểm tra instance status -Lightsail → Instances → Kiểm tra status (Running?) - -# Reset SSH keys -Lightsail → Account → SSH keys → Reset -``` - -### Docker containers không start? - -```bash -# Xem logs -docker-compose logs -f - -# Restart -docker-compose restart - -# Rebuild nếu cần -docker-compose down -docker-compose up -d --build -``` - -### Health check fails? - -```bash -# Check container status -docker-compose ps - -# Check logs của API -docker-compose logs api - -# Kiểm tra port đang listen -curl http://localhost:5000/health -``` - -### DNS không hoạt động? - -```bash -# Flush DNS cache (local) -# Windows: ipconfig /flushdns -# Mac: sudo dscacheutil -flushcache - -# Verify DNS propagation -dig your-domain.com -# hoặc -nslookup your-domain.com -``` - ---- - -## Support Links - -- AWS Lightsail Documentation: https://docs.aws.amazon.com/lightsail/ -- Docker Documentation: https://docs.docker.com/ -- Let's Encrypt: https://letsencrypt.org/docs/ -- UptimeRobot: https://uptimerobot.com/dashboard - ---- - -**Tiếp theo**: [Sau khi deploy xong, setup Monitoring](./monitoring/README.md) diff --git a/docs/02-playstore/PLAY_STORE_SUBMISSION_CHECKLIST.md b/docs/02-playstore/PLAY_STORE_SUBMISSION_CHECKLIST.md new file mode 100644 index 00000000..d3b7d44a --- /dev/null +++ b/docs/02-playstore/PLAY_STORE_SUBMISSION_CHECKLIST.md @@ -0,0 +1,481 @@ +# 🎯 Play Store Submission Checklist - MenuGreen + +**Created:** 2026-07-09 +**Target:** Google Play Store (CH Play) +**Package Name:** `com.menugreen.app` +**Current Version:** 1.0.0+1 + +--- + +## 📋 Mục lục + +1. [ ] Phase 1: Chuẩn bị Tài Khoản +2. [ ] Phase 2: Android Configuration +3. [ ] Phase 3: Chuẩn bị Assets +4. [ ] Phase 4: Build & Signing +5. [ ] Phase 5: Google Play Console Setup +6. [ ] Phase 6: Upload & Review +7. [ ] Phase 7: Post-Launch + +--- + +## ✅ Phase 1: Chuẩn Bị Tài Khoản + +| # | Task | Status | Notes | +|---|------|--------|-------| +| 1.1 | Tạo tài khoản Google Play Developer | ⬜ | | +| 1.2 | Thanh toán phí $25 | ⬜ | | +| 1.3 | Xác minh danh tính Developer | ⬜ | | +| 1.4 | Đăng nhập Play Console thành công | ⬜ | | + +### Actions: +```bash +# Truy cập: https://play.google.com/console +# Thanh toán phí đăng ký $25 (một lần) +``` + +--- + +## ✅ Phase 2: Android Configuration + +| # | Task | Status | Notes | +|---|------|--------|-------| +| 2.1 | Đổi package name thành `com.menugreen.app` | ⬜ | Hiện tại: `com.example.frontend` | +| 2.2 | Cập nhật `build.gradle.kts` | ⬜ | namespace + applicationId | +| 2.3 | Cập nhật `AndroidManifest.xml` | ⬜ | package attribute | +| 2.4 | Di chuyển/đổi package MainActivity.kt | ⬜ | Path: `com/menugreen/app/` | +| 2.5 | Cập nhật `google-services.json` | ⬜ | Package phải match | +| 2.6 | Kiểm tra minSdkVersion | ⬜ | Tối thiểu 21 (Android 5.0) | +| 2.7 | Enable ProGuard/R8 | ⬜ | | + +### Files cần sửa: + +#### `android/app/build.gradle.kts` +```kotlin +android { + namespace = "com.menugreen.app" // Đổi từ "com.example.frontend" + + defaultConfig { + applicationId = "com.menugreen.app" // Đổi từ "com.example.frontend" + } +} +``` + +#### `android/app/src/main/AndroidManifest.xml` +```xml + +``` + +#### Di chuyển file Kotlin +```bash +# Tạo thư mục mới +mkdir -p android/app/src/main/kotlin/com/menugreen/app + +# Di chuyển và cập nhật nội dung file +mv android/app/src/main/kotlin/com/example/frontend/MainActivity.kt \ + android/app/src/main/kotlin/com/menugreen/app/ + +# Cập nhật package trong file: +# package com.menugreen.app +``` + +--- + +## ✅ Phase 3: Chuẩn Bị Assets + +| # | Task | Status | Size | Notes | +|---|------|--------|------|-------| +| 3.1 | App Icon | ⬜ | 512x512 | PNG, không alpha | +| 3.2 | App Icon (adaptive) | ⬜ | 1024x1024 | PNG cho Play Store | +| 3.3 | Feature Graphic | ⬜ | 1024x500 | PNG/JPG | +| 3.4 | Screenshots Phone (6.7") | ⬜ | 1440x2560 | 2-8 images | +| 3.5 | Screenshots Phone (5.5") | ⬜ | 1080x1920 | Tùy chọn | +| 3.6 | Screenshots Tablet | ⬜ | 2048x2560 | Tùy chọn | +| 3.7 | App Logo Vector | ⬜ | 512x512 | SVG tốt hơn | + +### App Icon Sizes cho Android: +``` +mipmap-mdpi → 48x48 +mipmap-hdpi → 72x72 +mipmap-xhdpi → 96x96 +mipmap-xxhdpi → 144x144 +mipmap-xxxhdpi → 192x192 +mipmap-anydpi-v26 → adaptive icon +``` + +### Thư mục icons hiện tại: +``` +android/app/src/main/res/ +├── mipmap-hdpi/ +├── mipmap-mdpi/ +├── mipmap-xhdpi/ +├── mipmap-xxhdpi/ +├── mipmap-xxxhdpi/ +└── mipmap-anydpi-v26/ +``` + +### Gợi ý tạo Icon: +- Tool: [Android Asset Studio](https://romannurik.github.io/AndroidAssetStudio/) +- Tool: [App Icon Generator](https://appicon.co/) +- Thiết kế gốc: Figma/Canva + +--- + +## ✅ Phase 4: Build & Signing + +| # | Task | Status | Notes | +|---|------|--------|-------| +| 4.1 | Tạo Keystore | ⬜ | `menugreen_release.jks` | +| 4.2 | Tạo `key.properties` | ⬜ | File config signing | +| 4.3 | Cấu hình signing trong `build.gradle.kts` | ⬜ | | +| 4.4 | Build release AAB | ⬜ | | +| 4.5 | Verify AAB file | ⬜ | Kiểm tra signature | + +### 4.1 Tạo Keystore: +```bash +cd frontend + +# Tạo keystore +keytool -genkey -v -keystore menugreen_release.jks \ + -keyalg RSA \ + -keysize 2048 \ + -validity 10000 \ + -alias menugreen + +# Nhập các thông tin khi được hỏi: +# - Keystore password +# - Key password +# - First and Last Name: MenuGreen Team +# - Organization: MenuGreen +# - City, State, Country +``` + +### 4.2 Tạo key.properties: +```properties +storePassword=YOUR_STORE_PASSWORD +keyPassword=YOUR_KEY_PASSWORD +keyAlias=menugreen +storeFile=menugreen_release.jks +``` + +### 4.3 Cập nhật build.gradle.kts: +```kotlin +// Thêm vào đầu file +def keystoreProperties = new Properties() +def keystorePropertiesFile = rootProject.file('key.properties') +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) +} + +android { + // ... existing config ... + + signingConfigs { + create("release") { + keyAlias = keystoreProperties['keyAlias'] + keyPassword = keystoreProperties['keyPassword'] + storeFile = file(keystoreProperties['storeFile']) + storePassword = keystoreProperties['storePassword'] + } + } + + buildTypes { + release { + signingConfig = signingConfigs.getByName("release") + isMinifyEnabled = true + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } +} +``` + +### 4.4 Build AAB: +```bash +cd frontend + +# Clean project +flutter clean +flutter pub get + +# Build release bundle +flutter build appbundle --release + +# Output: build/app/outputs/bundle/release/app-release.aab +``` + +### 4.5 Verify AAB: +```bash +# Kiểm tra thông tin bundle +java -jar ~/.android/build-tools/VERSION/apksigner verify -v build/app/outputs/bundle/release/app-release.aab +``` + +--- + +## ✅ Phase 5: Google Play Console Setup + +| # | Task | Status | Notes | +|---|------|--------|-------| +| 5.1 | Tạo App mới | ⬜ | All apps → Create app | +| 5.2 | Điền App name | ⬜ | "MenuGreen" | +| 5.3 | Chọn App type | ⬜ | "App" | +| 5.4 | Chọn Free/Paid | ⬜ | "Free" | +| 5.5 | Declare App access | ⬜ | All/Conditional/Not | +| 5.6 | Ads declaration | ⬜ | Yes/No | +| 5.7 | Content rating questionnaire | ⬜ | Bắt buộc | +| 5.8 | Target audience | ⬜ | Chọn age group | +| 5.9 | Privacy policy | ⬜ | URL bắt buộc | + +### 5.1-5.4: Tạo App +``` +URL: https://play.google.com/console + +1. Go to "All apps" +2. Click "Create app" +3. Fill: + - App name: MenuGreen + - Default language: Tiếng Việt (vi) + - App type: App + - Free or Paid: Free +4. Create +``` + +### 5.5: App Access Declaration +``` +- Nếu app yêu cầu login → Conditional access +- Default: All functionality available without special access +``` + +### 5.6: Ads Declaration +``` +- App có hiển thị ads không? +- Trả lời: No (nếu không có ads) +``` + +### 5.7: Content Rating (BẮT BUỘC) +``` +1. Go to "Content rating" +2. Click "Continue to questionnaire" +3. Fill: + - Category: Health & Fitness + - Answer all questions +4. Submit +``` + +### 5.8: Target Audience & Experience +``` +- Age group: 13+ +- Chọn các nhóm phù hợp +``` + +### 5.9: Privacy Policy +``` +Cần tạo Privacy Policy page. Options: + +Option 1: Tạo trang static trên website +URL: https://menugreen.food/privacy + +Option 2: Sử dụng generator miễn phí +- https://www.termsfeed.com/privacy-policy-generator/ +- https://privacypolicies.com/privacy-policy-generator/ + +Option 3: Firebase Privacy Policy (nếu dùng Firebase) + +Template: +--- +CHÍNH SÁCH BẢO MẬT +MenuGreen + +1. Thông tin thu thập +2. Cách sử dụng thông tin +3. Lưu trữ dữ liệu +4. Quyền của người dùng +5. Liên hệ +--- +``` + +--- + +## ✅ Phase 6: Store Listing + +| # | Task | Status | Char Limit | Content | +|---|------|--------|-----------|---------| +| 6.1 | App name | ⬜ | 50 chars | MenuGreen | +| 6.2 | Short description | ⬜ | 80 chars | Ứng dụng dinh dưỡng cá nhân hóa | +| 6.3 | Full description | ⬜ | 4000 chars | Chi tiết app | +| 6.4 | Upload icon | ⬜ | 512x512 | | +| 6.5 | Upload feature graphic | ⬜ | 1024x500 | | +| 6.6 | Upload screenshots | ⬜ | 2-8 each | Phone + Tablet | +| 6.7 | App category | ⬜ | | Health & Fitness | +| 6.8 | Tags | ⬜ | 500 chars | nutrition, diet, health | + +### 6.2 Short Description (Tiếng Việt): +``` +Ứng dụng dinh dưỡng cá nhân hóa, giúp bạn ăn uống lành mạnh mỗi ngày. +``` + +### 6.3 Full Description Template: +```markdown +🍎 MenuGreen - Chuyên gia dinh dưỡng trong túi áo + +Bạn không biết hôm nay ăn gì? Bạn muốn cải thiện sức khỏe qua ăn uống? +MenuGreen sẽ giúp bạn! + +✨ TÍNH NĂNG NỔI BẬT + +📋 Lên thực đơn thông minh +- Gợi ý thực đơn hàng ngày phù hợp với mục tiêu +- Cân bằng dinh dưỡng tự động +- Đa dạng món ăn Việt Nam + +🥗 Theo dõi dinh dưỡng +- Đếm calories, protein, carb, fat +- Nhận diện thực phẩm bằng camera +- Theo dõi cân nặng và tiến độ + +🎯 Cá nhân hóa +- Phù hợp với chế độ ăn kiêng (giảm cân, tăng cơ, giữ dáng) +- Cân nhắc dị ứng thực phẩm +- Điều chỉnh theo ngân sách + +💪 Cho Gym/PT +- Hỗ trợ người tập gym +- Tính macro theo mục tiêu +- Gợi ý bữa ăn pre-workout, post-workout + +🔔 Nhắc nhở thông minh +- Không quên bữa ăn +- Uống nước đúng giờ +- Thời gian biểu dinh dưỡng + +📱 Dễ sử dụng +- Giao diện tiếng Việt +- Thao tác đơn giản +- Offline support + +👥 Phù hợp cho: +- Người muốn giảm cân +- Gymer và người tập thể hình +- Người bệnh cần kiểm soát ăn uống +- Người muốn ăn uống lành mạnh + +Tải MenuGreen ngay hôm nay và bắt đầu hành trình sống khỏe! + +--- +Liên hệ: support@menugreen.food +Website: https://menugreen.food +``` + +--- + +## ✅ Phase 7: Upload & Release + +| # | Task | Status | Notes | +|---|------|--------|-------| +| 7.1 | Tạo Production release | ⬜ | | +| 7.2 | Upload AAB file | ⬜ | | +| 7.3 | Hoàn thành Release notes | ⬜ | VN + EN | +| 7.4 | Chọn App Signing | ⬜ | Recommend: Google | +| 7.5 | Submit for review | ⬜ | | +| 7.6 | Đợi review | ⬜ | 1-7 ngày | + +### 7.1-7.2: Tạo Release +``` +1. Go to "Production" +2. Click "Create release" +3. Upload .aab file +4. Hoặc kéo thả file +``` + +### 7.3: Release Notes +``` +Vietnamese: +- Phiên bản đầu tiên của ứng dụng MenuGreen +- Các tính năng: Lên thực đơn, theo dõi dinh dưỡng, gợi ý cá nhân hóa + +English: +- First release of MenuGreen app +- Features: Meal planning, nutrition tracking, personalized recommendations +``` + +### 7.4: App Signing +``` +Options: +1. Google App Signing (RECOMMENDED) + - Google sẽ quản lý signing key + - An toàn hơn, không sợ mất key + +2. Export and upload a key + - Bạn tự quản lý key + - Rủi ro mất key cao hơn + +→ Chọn: "Let Google create and manage my app signing key" +``` + +### 7.5-7.6: Submit +``` +1. Click "Save" +2. Click "Review release" +3. Confirm all checks passed +4. Click "Start release to Production" +5. Confirm +``` + +--- + +## ✅ Phase 8: Post-Launch + +| # | Task | Status | Notes | +|---|------|--------|-------| +| 8.1 | Kiểm tra app trên Store | ⬜ | Sau khi approved | +| 8.2 | Test download từ Play Store | ⬜ | | +| 8.3 | Setup Crashlytics/Bug reporting | ⬜ | Firebase | +| 8.4 | Setup Analytics | ⬜ | Firebase Analytics | +| 8.5 | Monitor reviews | ⬜ | | +| 8.6 | Chuẩn bị update tiếp theo | ⬜ | | + +--- + +## 🐛 Common Issues & Fixes + +| Issue | Solution | +|-------|----------| +| App crash on launch | Test kỹ, fix bugs trước upload | +| Violates policy | Đọc kỹ [Policy](https://play.google.com/about/developer-content-policy/) | +| Missing privacy policy | Thêm URL vào Store listing | +| Wrong package name | Đổi lại trong build.gradle.kts | +| Version conflict | Tăng versionCode trước mỗi release | +| Review takes too long | Lần đầu 3-7 ngày, lần sau 1-2 ngày | + +--- + +## 📞 Resources + +| Resource | URL | +|----------|-----| +| Play Console | https://play.google.com/console | +| Developer Policy | https://play.google.com/about/developer-content-policy/ | +| App Signing | https://developer.android.com/studio/publish/app-signing | +| Play Core Library | https://developer.android.com/guide/playcore | + +--- + +## 📝 Notes & Decisions + + + +| Field | Value | +|-------|-------| +| Play Console Email | | +| App Signing by | Google / Self | +| Keystore Location | | +| Privacy Policy URL | | +| Initial Version | 1.0.0 | +| Review Date | | +| Approved Date | | +| Published Date | | + +--- + +**Last Updated:** 2026-07-09 +**Status:** Draft - Ready to execute diff --git a/docs/issues.md b/docs/issues.md index 0b55fcf2..5ea913df 100644 --- a/docs/issues.md +++ b/docs/issues.md @@ -244,8 +244,6 @@ Commented YAML blocks gây parse error. --- ---- - ## [RESOLVED] CORS Configuration - Backend + Nginx + Cloudflare **Date:** 2026-07-05 @@ -399,8 +397,6 @@ REDIS_URL="redis://:eH671/FNx4LyTMJcEXQJ@menugreen_redis:6379" --- ---- - ## [DOCUMENTED] Production Infrastructure **Date:** 2026-07-02 @@ -474,8 +470,6 @@ cd ~/apps/MenuGreenSystem --- ---- - ## [RESOLVED] Canonical Docs Review - Endpoint Count & Formula Mismatches **Date:** 2026-07-08 @@ -642,6 +636,63 @@ Script `scripts/verify_endpoints.py` đếm `[HttpGet]`, `[HttpPost]`, `[HttpPut --- + +--- + +## [PENDING] Deployment Failed - Database "MenuGreenDb" Does Not Exist + +**Date:** 2026-07-09 +**Status:** Pending +**Severity:** High + +### Description + +GitHub Actions deployment thất bại tại bước backup database. Lỗi: + +``` +pg_dump: error: connection to server at "menugreen-db.cr4uo6sksium.ap-southeast-1.rds.amazonaws.com" (13.250.214.140), port 5432 failed: FATAL: database "MenuGreenDb" does not exist +``` + +### Root Cause + +Tên database trong connection string là `MenuGreenDb` (PascalCase) nhưng database thực tế trên RDS là `menugreendb` (lowercase). PostgreSQL database names thường case-sensitive. + +### Environment + +- **Server:** AWS Lightsail Ubuntu 22.04 +- **RDS:** PostgreSQL 18.3 @ ap-southeast-1 +- **Endpoint:** `menugreen-db.cr4uo6sksium.ap-southeast-1.rds.amazonaws.com` +- **Expected DB Name:** `menugreendb` +- **Wrong DB Name:** `MenuGreenDb` + +### Logs + +``` +2026-07-09T09:01:11.2797761Z out: === Starting database backup === +2026-07-09T09:01:11.4251196Z out: pg_dump: error: connection to server at "menugreen-db.cr4uo6sksium.ap-southeast-1.rds.amazonaws.com" (13.250.214.140), port 5432 failed: FATAL: database "MenuGreenDb" does not exist +2026-07-09T09:01:11.4291841Z 2026/07/09 09:01:11 Process exited with status 1 +2026-07-09T09:01:11.4315538Z ##[error]Process completed with exit code 1. +``` + +### Fix Required + +1. **Kiểm tra Doppler secrets** - Tìm `CONNECTIONSTRINGS__DEFAULTCONNECTION` và sửa database name từ `MenuGreenDb` → `menugreendb` +2. **Hoặc sửa CI/CD script** - Thêm step rename/sanitise database name trong backup script: + ```bash + # Sanitise database name (lowercase) + DB_NAME_LOWER=$(echo "$DB_NAME" | tr '[:upper:]' '[:lower:]') + PGPASSWORD="$DB_PASSWORD" pg_dump -h "$DB_HOST" -p "${DB_PORT:-5432}" -U "$DB_USER" -d "$DB_NAME_LOWER" -F p -f "$BACKUP_FILE" + ``` + +### Verification After Fix + +```bash +PGPASSWORD='MenuGreen2026!' psql -h menugreen-db.cr4uo6sksium.ap-southeast-1.rds.amazonaws.com -U postgres -l +# Kiểm tra database name hiển thị đúng +``` + +--- + ## [RESOLVED] Vietnam Local Features — UI triển khai hoàn chỉnh **Date:** 2026-07-09 @@ -660,7 +711,7 @@ Tính năng backend hoàn thiện sớm nhưng chưa được ưu tiên phát tr - Frontend: `d:\CSharp_UpSpeed\MenuGreenSystem\frontend` (Flutter 3.11, Dart 3.x) - Backend: `d:\CSharp_UpSpeed\MenuGreenSystem\backend\MenuGreen.API` -- Tài liệu tham chiếu: `docs/features/10-vietnam-local-features.md` +- Tài liệu tham khảo: `docs/features/10-vietnam-local-features.md` ### Fix Applied @@ -691,8 +742,6 @@ Tạo feature hoàn chỉnh `frontend/lib/features/vietnam_local/` với: - `flutter build apk --debug --no-pub` → built thành công APK debug. - Tài liệu `10-vietnam-local-features.md` cập nhật Status, UI Components table, Navigation Flow. ---- - ## Template for New Issues ```markdown @@ -715,6 +764,54 @@ Tạo feature hoàn chỉnh `frontend/lib/features/vietnam_local/` với: --- +## [PENDING] Google Play Console - Account Deletion URL + +**Date:** 2026-07-09 +**Status:** Pending (waiting for user to deploy to GitHub Pages) +**Severity:** Medium + +### Description +Google Play Console yêu cầu cung cấp **URL xoá tài khoản** để tuân thủ +chính sách User Data Policy. Cần phải có trang web công khai hướng dẫn +user cách yêu cầu xoá tài khoản và dữ liệu cá nhân. + +### Root Cause +App cung cấp đăng ký tài khoản (email + Google OAuth) nên theo chính +sách Google, phải có URL xoá tài khoản hiển thị trên trang CH Play. + +### Environment +- Google Play Console → App content → Data safety +- Trang: https://play.google.com/console + +### Fix Applied +**Đã tạo 2 file HTML sẵn sàng deploy:** + +1. `assets/delete-account/delete-account.html` - Tiếng Việt (mặc định) +2. `assets/delete-account/delete-account-en.html` - English +3. `assets/delete-account/README.md` - Hướng dẫn deploy GitHub Pages + +**Trang bao gồm đầy đủ nội dung theo yêu cầu Google:** +- ✅ Nhắc đến tên app "MenuGreen" +- ✅ 5 bước yêu cầu xoá tài khoản (in-app + email) +- ✅ Liệt kê dữ liệu bị xoá (6 loại) +- ✅ Liệt kê dữ liệu giữ lại (2 loại, theo yêu cầu pháp lý) +- ✅ Thời gian xử lý (7 ngày làm việc) +- ✅ Thông tin liên hệ support + +### Attempts +- [x] Tạo file HTML song ngữ với thiết kế chuyên nghiệp +- [x] Responsive (mobile + desktop) +- [ ] User deploy lên GitHub Pages +- [ ] User dán URL vào Play Console +- [ ] User bấm Save trong form An toàn dữ liệu + +### Customization cần thay trước khi deploy +- `support@menugreen.app` → email thật của nhà phát triển +- `https://menugreen.app` → website thật (nếu có) +- `MenuGreen Team` → tên nhà phát triển chính xác theo Play Console + +--- + ## Prevention Guidelines ### YAML Files diff --git a/frontend/android/app/build.gradle.kts b/frontend/android/app/build.gradle.kts index 4608d8bc..b4eebc66 100644 --- a/frontend/android/app/build.gradle.kts +++ b/frontend/android/app/build.gradle.kts @@ -6,8 +6,16 @@ plugins { id("com.google.gms.google-services") } +// Load signing configuration from key.properties +import java.util.Properties +val keystoreProperties = Properties() +val keystorePropertiesFile = rootProject.file("key.properties") +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(keystorePropertiesFile.inputStream()) +} + android { - namespace = "com.example.frontend" + namespace = "com.menugreen.app" compileSdk = flutter.compileSdkVersion ndkVersion = flutter.ndkVersion @@ -16,8 +24,17 @@ android { targetCompatibility = JavaVersion.VERSION_17 } + signingConfigs { + create("release") { + keyAlias = keystoreProperties.getProperty("keyAlias") + keyPassword = keystoreProperties.getProperty("keyPassword") + storeFile = rootProject.file(keystoreProperties.getProperty("storeFile")) + storePassword = keystoreProperties.getProperty("storePassword") + } + } + defaultConfig { - applicationId = "com.example.frontend" + applicationId = "com.menugreen.app" minSdk = flutter.minSdkVersion targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode @@ -26,6 +43,11 @@ android { buildTypes { release { + signingConfig = signingConfigs.getByName("release") + isMinifyEnabled = false + isShrinkResources = false + } + debug { signingConfig = signingConfigs.getByName("debug") } } diff --git a/frontend/android/app/google-services.json b/frontend/android/app/google-services.json index fe7e51df..9633cff3 100644 --- a/frontend/android/app/google-services.json +++ b/frontend/android/app/google-services.json @@ -7,20 +7,12 @@ "client": [ { "client_info": { - "mobilesdk_app_id": "1:709315528907:android:c75baa68ff19bc9c54b34e", + "mobilesdk_app_id": "1:709315528907:android:1a896858d0654a9454b34e", "android_client_info": { - "package_name": "com.example.frontend" + "package_name": "com.menugreen.app" } }, "oauth_client": [ - { - "client_id": "709315528907-56n5sdu7e62nd3tm6j8lc2retcemb3lc.apps.googleusercontent.com", - "client_type": 1, - "android_info": { - "package_name": "com.example.frontend", - "certificate_hash": "875672f3641509cecc61923a10aa83670bb89858" - } - }, { "client_id": "709315528907-sd0et9a55hqo9ksitbn3lg3jpvhmiqol.apps.googleusercontent.com", "client_type": 3 diff --git a/frontend/android/app/proguard-rules.pro b/frontend/android/app/proguard-rules.pro new file mode 100644 index 00000000..796d8964 --- /dev/null +++ b/frontend/android/app/proguard-rules.pro @@ -0,0 +1,28 @@ +# Flutter default ProGuard rules +# Keep Flutter classes +-keep class io.flutter.app.** { *; } +-keep class io.flutter.plugin.** { *; } +-keep class io.flutter.util.** { *; } +-keep class io.flutter.view.** { *; } +-keep class io.flutter.** { *; } +-keep class io.flutter.plugins.** { *; } + +# Keep Firebase classes +-keep class com.google.firebase.** { *; } +-keep class com.google.android.gms.** { *; } + +# Keep model classes +-keep class com.menugreen.app.models.** { *; } +-keep class com.menugreen.app.data.** { *; } + +# Keep Gson serialization +-keepattributes Signature +-keepattributes *Annotation* +-keep class sun.misc.Unsafe { *; } +-keep class com.google.gson.stream.** { *; } + +# Keep enum classes +-keepclassmembers enum * { + public static **[] values(); + public static ** valueOf(java.lang.String); +} diff --git a/frontend/android/app/src/main/kotlin/com/example/frontend/MainActivity.kt b/frontend/android/app/src/main/kotlin/com/menugreen/app/MainActivity.kt similarity index 76% rename from frontend/android/app/src/main/kotlin/com/example/frontend/MainActivity.kt rename to frontend/android/app/src/main/kotlin/com/menugreen/app/MainActivity.kt index 1bd2dee2..e5aafffa 100644 --- a/frontend/android/app/src/main/kotlin/com/example/frontend/MainActivity.kt +++ b/frontend/android/app/src/main/kotlin/com/menugreen/app/MainActivity.kt @@ -1,4 +1,4 @@ -package com.example.frontend +package com.menugreen.app import io.flutter.embedding.android.FlutterActivity diff --git a/frontend/android/app/src/main/res/drawable/ic_launcher_foreground.xml b/frontend/android/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 00000000..c1daee65 --- /dev/null +++ b/frontend/android/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/frontend/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..5ed0a2df --- /dev/null +++ b/frontend/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/frontend/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index db77bb4b..02620015 100644 Binary files a/frontend/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/frontend/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/frontend/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/frontend/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 17987b79..3ec2f86b 100644 Binary files a/frontend/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/frontend/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/frontend/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/frontend/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 09d43914..07bb153d 100644 Binary files a/frontend/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/frontend/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/frontend/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/frontend/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index d5f1c8d3..bfddcfcf 100644 Binary files a/frontend/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/frontend/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/frontend/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/frontend/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 4d6372ee..b9cddfa7 100644 Binary files a/frontend/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/frontend/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/frontend/android/app/src/main/res/values/colors.xml b/frontend/android/app/src/main/res/values/colors.xml new file mode 100644 index 00000000..eff930f6 --- /dev/null +++ b/frontend/android/app/src/main/res/values/colors.xml @@ -0,0 +1,4 @@ + + + #009650 + diff --git a/frontend/macos/Flutter/GeneratedPluginRegistrant.swift b/frontend/macos/Flutter/GeneratedPluginRegistrant.swift deleted file mode 100644 index 61a99a0f..00000000 --- a/frontend/macos/Flutter/GeneratedPluginRegistrant.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// Generated file. Do not edit. -// - -import FlutterMacOS -import Foundation - -import file_selector_macos -import firebase_auth -import firebase_core -import firebase_messaging -import firebase_storage -import geolocator_apple -import google_sign_in_ios -import package_info_plus -import shared_preferences_foundation - -func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) - FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) - FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) - FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) - FLTFirebaseStoragePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseStoragePlugin")) - GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) - FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin")) - FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) - SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) -}