Add build-and-push and deploy jobs to CI workflow #142

Open
opened 2026-03-28 10:31:43 +01:00 by marcel · 0 comments
Owner

Why

The CI pipeline currently only runs tests. There is no step that builds production Docker images or deploys them to the VPS. Without this, every deployment is a manual process — someone has to SSH into the VPS, pull the repo, build images, and restart services by hand.

This issue adds two new CI jobs:

  1. build-and-push — builds production Docker images and pushes them to the Gitea container registry
  2. deploy — SSHes into the VPS over Tailscale and restarts services with the new images

Both jobs only run on semver tags (deployment trigger defined in #143). This issue covers the job definitions themselves.

Prerequisites: #141 (Tailscale setup), production Dockerfiles (#134, #135).

What to do

1. Enable the Gitea container registry

In infra/gitea/app.ini (or however the Gitea instance is configured), ensure packages are enabled:

[packages]
ENABLED = true

Images will be served at 192.168.178.71:3005/marcel/familienarchiv/backend and .../frontend.

2. Create an SSH deploy key pair

On the home server (not the VPS):

ssh-keygen -t ed25519 -C "gitea-ci-deploy" -f gitea_deploy_key -N ""
  • Add gitea_deploy_key.pub contents to /home/deploy/.ssh/authorized_keys on the VPS
  • Add gitea_deploy_key (private key) as a Gitea secret named VPS_SSH_PRIVATE_KEY
  • Delete the local key files after storing them

3. Add a Gitea secret for the registry password

The VPS needs to pull images from the Gitea registry. Create a Gitea access token (user settings → Applications → Access tokens) with read:package scope, and store it as:

  • REGISTRY_TOKEN — used by the VPS to authenticate docker pull

4. Add jobs to .gitea/workflows/ci.yml

Append after the existing e2e-tests job:

build-and-push:
  name: Build & Push Images
  needs: [unit-tests, backend-unit-tests, e2e-tests]
  if: startsWith(gitea.ref, 'refs/tags/v')
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4

    - name: Log in to Gitea registry
      uses: docker/login-action@v3
      with:
        registry: 192.168.178.71:3005
        username: marcel
        password: ${{ secrets.REGISTRY_TOKEN }}

    - name: Build and push backend
      uses: docker/build-push-action@v5
      with:
        context: ./backend
        push: true
        tags: |
          192.168.178.71:3005/marcel/familienarchiv/backend:${{ gitea.sha }}
          192.168.178.71:3005/marcel/familienarchiv/backend:latest

    - name: Build and push frontend
      uses: docker/build-push-action@v5
      with:
        context: ./frontend
        push: true
        tags: |
          192.168.178.71:3005/marcel/familienarchiv/frontend:${{ gitea.sha }}
          192.168.178.71:3005/marcel/familienarchiv/frontend:latest

deploy:
  name: Deploy to VPS
  needs: build-and-push
  if: startsWith(gitea.ref, 'refs/tags/v')
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4

    - name: Set up SSH
      run: |
        mkdir -p ~/.ssh
        echo "${{ secrets.VPS_SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key
        chmod 600 ~/.ssh/deploy_key
        ssh-keyscan -H ${{ secrets.VPS_TAILSCALE_IP }} >> ~/.ssh/known_hosts

    - name: Deploy over Tailscale SSH
      run: |
        ssh -i ~/.ssh/deploy_key deploy@${{ secrets.VPS_TAILSCALE_IP }} bash <<'EOF'
          set -euo pipefail
          cd /opt/familienarchiv

          # Authenticate to Gitea registry (idempotent)
          echo "${{ secrets.REGISTRY_TOKEN }}" | \
            docker login 192.168.178.71:3005 -u marcel --password-stdin

          # Pull new images
          docker compose -f docker-compose.yml -f docker-compose.prod.yml pull backend frontend

          # Restart only backend and frontend — db and caddy keep running
          docker compose -f docker-compose.yml -f docker-compose.prod.yml \
            up -d --no-deps backend frontend

          # Clean up old images
          docker image prune -f

          echo "Deploy complete: ${{ gitea.ref_name }}"
        EOF

    - name: Verify deployment health
      run: |
        sleep 15
        ssh -i ~/.ssh/deploy_key deploy@${{ secrets.VPS_TAILSCALE_IP }} \
          "curl -sf http://localhost:8080/actuator/health | grep -q UP"

5. First-time VPS setup (one-off, before first deploy)

Clone the repo onto the VPS and create the .env file:

git clone ssh://git@<gitea-tailscale-ip>:3005/marcel/familienarchiv.git /opt/familienarchiv
cd /opt/familienarchiv
cp .env.example .env
nano .env   # fill in production values

The compose files and Caddyfile need to be present — the deploy job only restarts containers, it does not update compose files. Compose file changes require a manual pull or a separate sync step (out of scope for this issue).

Gitea secrets summary

Secret Purpose
VPS_TAILSCALE_IP Tailscale IP of the VPS (from #141)
VPS_SSH_PRIVATE_KEY Ed25519 private key for deploy user SSH
REGISTRY_TOKEN Gitea access token for docker pull on VPS
E2E_ADMIN_PASSWORD E2E test credentials (from #128)

Acceptance criteria

  • Pushing a v* tag triggers build-and-pushdeploy in CI.
  • Both Docker images appear in the Gitea package registry after a successful run.
  • The deploy job completes without error and the health check passes.
  • Pushing to a branch (not a tag) runs only the three test jobs — no build or deploy.
  • A failed test job prevents build-and-push from running (needs: dependency).
## Why The CI pipeline currently only runs tests. There is no step that builds production Docker images or deploys them to the VPS. Without this, every deployment is a manual process — someone has to SSH into the VPS, pull the repo, build images, and restart services by hand. This issue adds two new CI jobs: 1. **`build-and-push`** — builds production Docker images and pushes them to the Gitea container registry 2. **`deploy`** — SSHes into the VPS over Tailscale and restarts services with the new images Both jobs only run on semver tags (deployment trigger defined in #143). This issue covers the job definitions themselves. **Prerequisites:** #141 (Tailscale setup), production Dockerfiles (#134, #135). ## What to do ### 1. Enable the Gitea container registry In `infra/gitea/app.ini` (or however the Gitea instance is configured), ensure packages are enabled: ```ini [packages] ENABLED = true ``` Images will be served at `192.168.178.71:3005/marcel/familienarchiv/backend` and `.../frontend`. ### 2. Create an SSH deploy key pair On the home server (not the VPS): ```bash ssh-keygen -t ed25519 -C "gitea-ci-deploy" -f gitea_deploy_key -N "" ``` - Add `gitea_deploy_key.pub` contents to `/home/deploy/.ssh/authorized_keys` on the VPS - Add `gitea_deploy_key` (private key) as a Gitea secret named `VPS_SSH_PRIVATE_KEY` - Delete the local key files after storing them ### 3. Add a Gitea secret for the registry password The VPS needs to pull images from the Gitea registry. Create a Gitea access token (user settings → Applications → Access tokens) with `read:package` scope, and store it as: - **`REGISTRY_TOKEN`** — used by the VPS to authenticate `docker pull` ### 4. Add jobs to `.gitea/workflows/ci.yml` Append after the existing `e2e-tests` job: ```yaml build-and-push: name: Build & Push Images needs: [unit-tests, backend-unit-tests, e2e-tests] if: startsWith(gitea.ref, 'refs/tags/v') runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Log in to Gitea registry uses: docker/login-action@v3 with: registry: 192.168.178.71:3005 username: marcel password: ${{ secrets.REGISTRY_TOKEN }} - name: Build and push backend uses: docker/build-push-action@v5 with: context: ./backend push: true tags: | 192.168.178.71:3005/marcel/familienarchiv/backend:${{ gitea.sha }} 192.168.178.71:3005/marcel/familienarchiv/backend:latest - name: Build and push frontend uses: docker/build-push-action@v5 with: context: ./frontend push: true tags: | 192.168.178.71:3005/marcel/familienarchiv/frontend:${{ gitea.sha }} 192.168.178.71:3005/marcel/familienarchiv/frontend:latest deploy: name: Deploy to VPS needs: build-and-push if: startsWith(gitea.ref, 'refs/tags/v') runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up SSH run: | mkdir -p ~/.ssh echo "${{ secrets.VPS_SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key chmod 600 ~/.ssh/deploy_key ssh-keyscan -H ${{ secrets.VPS_TAILSCALE_IP }} >> ~/.ssh/known_hosts - name: Deploy over Tailscale SSH run: | ssh -i ~/.ssh/deploy_key deploy@${{ secrets.VPS_TAILSCALE_IP }} bash <<'EOF' set -euo pipefail cd /opt/familienarchiv # Authenticate to Gitea registry (idempotent) echo "${{ secrets.REGISTRY_TOKEN }}" | \ docker login 192.168.178.71:3005 -u marcel --password-stdin # Pull new images docker compose -f docker-compose.yml -f docker-compose.prod.yml pull backend frontend # Restart only backend and frontend — db and caddy keep running docker compose -f docker-compose.yml -f docker-compose.prod.yml \ up -d --no-deps backend frontend # Clean up old images docker image prune -f echo "Deploy complete: ${{ gitea.ref_name }}" EOF - name: Verify deployment health run: | sleep 15 ssh -i ~/.ssh/deploy_key deploy@${{ secrets.VPS_TAILSCALE_IP }} \ "curl -sf http://localhost:8080/actuator/health | grep -q UP" ``` ### 5. First-time VPS setup (one-off, before first deploy) Clone the repo onto the VPS and create the `.env` file: ```bash git clone ssh://git@<gitea-tailscale-ip>:3005/marcel/familienarchiv.git /opt/familienarchiv cd /opt/familienarchiv cp .env.example .env nano .env # fill in production values ``` The compose files and `Caddyfile` need to be present — the deploy job only restarts containers, it does not update compose files. Compose file changes require a manual pull or a separate sync step (out of scope for this issue). ## Gitea secrets summary | Secret | Purpose | |---|---| | `VPS_TAILSCALE_IP` | Tailscale IP of the VPS (from #141) | | `VPS_SSH_PRIVATE_KEY` | Ed25519 private key for deploy user SSH | | `REGISTRY_TOKEN` | Gitea access token for `docker pull` on VPS | | `E2E_ADMIN_PASSWORD` | E2E test credentials (from #128) | ## Acceptance criteria - Pushing a `v*` tag triggers `build-and-push` → `deploy` in CI. - Both Docker images appear in the Gitea package registry after a successful run. - The deploy job completes without error and the health check passes. - Pushing to a branch (not a tag) runs only the three test jobs — no build or deploy. - A failed test job prevents `build-and-push` from running (`needs:` dependency).
marcel added the devopsphase-3: prod-compose labels 2026-03-28 10:46:50 +01:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#142