refactor(docker): streamline Dockerfiles for API, Docs, and Web apps #29
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Docker-based deployment to VPS via GitHub Container Registry | |
| # | |
| # Required GitHub Secrets: | |
| # - SSH_HOST: VPS IP address or hostname | |
| # - SSH_USER: SSH username | |
| # - SSH_KEY: SSH private key for VPS access | |
| # - ENV_DOCKER: Full contents of the production .env file | |
| # - GCP_SA_KEY: JSON contents of a Google Cloud service account key with the | |
| # "reCAPTCHA Enterprise Agent" role. Mounted into the api container as | |
| # /run/secrets/gcp-credentials.json (GOOGLE_APPLICATION_CREDENTIALS). | |
| # | |
| # Uses the built-in GITHUB_TOKEN for GHCR auth — no PAT needed. | |
| # The GHCR package must be set to "public" in the repo's Packages settings so | |
| # the VPS can `docker pull` without credentials. | |
| # | |
| # Required GitHub Variables (Settings → Secrets and variables → Actions → Variables tab): | |
| # - NEXT_PUBLIC_API_URL: Public backend API URL (e.g. https://ogstack.dev) | |
| # - NEXT_PUBLIC_DEMO_PROJECT_ID: Public project ID used by the playground demo | |
| # - NEXT_PUBLIC_DOCS_URL: Public docs URL (e.g. https://docs.ogstack.dev) | |
| # - NEXT_PUBLIC_RECAPTCHA_SITE_KEY: reCAPTCHA Enterprise site key (public, baked into web bundle) | |
| name: Build and Deploy | |
| on: | |
| push: | |
| branches: [ prod ] | |
| workflow_dispatch: | |
| env: | |
| REGISTRY: ghcr.io | |
| APP_DIR: ~/deploy/ogstack | |
| jobs: | |
| build: | |
| name: Build ${{ matrix.image }} | |
| runs-on: ubuntu-latest | |
| concurrency: | |
| group: build-${{ matrix.image }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| packages: write | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - image: api | |
| file: ./apps/api/Dockerfile | |
| - image: web | |
| file: ./apps/web/Dockerfile | |
| - image: docs | |
| file: ./apps/docs/Dockerfile | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Set lowercase repository name | |
| id: repo | |
| run: echo "name=${GITHUB_REPOSITORY,,}" >> $GITHUB_OUTPUT | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Login to GitHub Container Registry | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build and push ${{ matrix.image }} | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| file: ${{ matrix.file }} | |
| push: true | |
| tags: ${{ env.REGISTRY }}/${{ steps.repo.outputs.name }}/${{matrix.image}}:latest | |
| build-args: | | |
| NEXT_PUBLIC_API_URL=${{ vars.NEXT_PUBLIC_API_URL }} | |
| NEXT_PUBLIC_DEMO_PROJECT_ID=${{ vars.NEXT_PUBLIC_DEMO_PROJECT_ID }} | |
| NEXT_PUBLIC_DOCS_URL=${{ vars.NEXT_PUBLIC_DOCS_URL }} | |
| NEXT_PUBLIC_RECAPTCHA_SITE_KEY=${{ vars.NEXT_PUBLIC_RECAPTCHA_SITE_KEY }} | |
| cache-from: type=registry,ref=${{ env.REGISTRY }}/${{steps.repo.outputs.name}}/${{ matrix.image }}:buildcache | |
| cache-to: type=registry,ref=${{ env.REGISTRY }}/${{steps.repo.outputs.name}}/${{ matrix.image }}:buildcache,mode=max | |
| deploy: | |
| name: Deploy to VPS | |
| runs-on: ubuntu-latest | |
| needs: build | |
| concurrency: | |
| group: deploy-production | |
| cancel-in-progress: false | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Write .env from ENV_DOCKER secret | |
| run: | | |
| mkdir -p deploy | |
| printf '%s' "${{ secrets.ENV_DOCKER }}" > deploy/.env | |
| - name: Write GCP service account credentials | |
| run: | | |
| printf '%s' '${{ secrets.GCP_SA_KEY }}' > deploy/gcp-credentials.json | |
| chmod 600 deploy/gcp-credentials.json | |
| - name: Copy docker-compose and secrets to VPS | |
| uses: appleboy/scp-action@v1 | |
| with: | |
| host: ${{ secrets.SSH_HOST }} | |
| username: ${{ secrets.SSH_USER }} | |
| key: ${{ secrets.SSH_KEY }} | |
| source: "deploy/docker-compose.yml,deploy/.env,deploy/gcp-credentials.json" | |
| target: ${{ env.APP_DIR }} | |
| strip_components: 1 | |
| - name: Deploy with Docker Compose | |
| uses: appleboy/ssh-action@v1 | |
| env: | |
| GITHUB_REPOSITORY: ${{ github.repository }} | |
| with: | |
| host: ${{ secrets.SSH_HOST }} | |
| username: ${{ secrets.SSH_USER }} | |
| key: ${{ secrets.SSH_KEY }} | |
| envs: GITHUB_REPOSITORY | |
| script: | | |
| cd ${{ env.APP_DIR }} | |
| # GHCR requires lowercase image names. | |
| export GITHUB_REPOSITORY=$(echo "$GITHUB_REPOSITORY" | tr '[:upper:]' '[:lower:]') | |
| # Pull new images while old containers are still running | |
| docker compose pull | |
| # Recreate containers with new images (stops old, starts new) | |
| docker compose up -d --force-recreate --remove-orphans | |
| # Clean up old images | |
| docker image prune -f | |
| echo "=== Running Containers ===" | |
| docker compose ps --format "table {{.Name}}\t{{.Status}}" | |
| - name: Health check | |
| uses: appleboy/ssh-action@v1 | |
| with: | |
| host: ${{ secrets.SSH_HOST }} | |
| username: ${{ secrets.SSH_USER }} | |
| key: ${{ secrets.SSH_KEY }} | |
| script: | | |
| echo "Waiting for services to start..." | |
| sleep 15 | |
| echo "=== API Health Check ===" | |
| curl -sf http://localhost:5000/health || echo "API not responding yet" | |
| echo "=== Container Status ===" | |
| docker compose -f ${{ env.APP_DIR }}/docker-compose.yml ps --format "table {{.Name}}\t{{.Status}}" | |
| echo "=== Recent Logs ===" | |
| docker compose -f ${{ env.APP_DIR }}/docker-compose.yml logs --tail=10 2>/dev/null || true |