GitHub

CI/CD Pipeline

This runbook explains how Orcta deploys services from GitHub to the VPS using GitHub Actions, Docker, and a shared PostgreSQL cluster.

High-Level Architecture

  • Codebase: GitHub (dev and main branches).
  • CI/CD: GitHub Actions (SSH deploy to VPS).
  • Runtime: Ubuntu VPS running Docker + Docker Compose.
  • Services:
    • App containers per repo.
    • Shared PostgreSQL cluster (Dockerized) with per-app DBs/roles.
    • Caddy reverse proxy for routing and TLS (optional).

Deployment Flow

  1. Push to main or dev.
  2. GitHub Actions connects to the VPS via SSH.
  3. The repo is cloned or pulled into /srv/apps/<APP>.
  4. Docker Compose rebuilds and restarts the service.
  5. Caddy reloads to pick up new routes.

Architecture Diagram

             ┌───────────────────────┐
             │       GitHub Repo     │
             │  (main & dev branches)│
             └───────────┬───────────┘
                         │
                    Push triggers
                         │
                         ▼
             ┌───────────────────────┐
             │    GitHub Actions     │
             │  (CI/CD Workflows)    │
             └───────────┬───────────┘
                         │
                   SSH Deploys to
                         │
                         ▼
    ┌─────────────────────────────────────────┐
    │                 VPS                     │
    │   (Ubuntu + Docker + Docker Compose)    │
    │                                         │
    │  ┌───────────────────────────────────┐  │
    │  │      Shared Docker Network        │  │
    │  │         `orcta-net`               │  │
    │  │                                   │  │
    │  │  ┌─────────────┐   ┌────────────┐ │  │
    │  │  │   app1      │   │   app2     │ │  │
    │  │  │ dockerized  │   │ dockerized │ │  │
    │  │  │ service     │   │ service    │ │  │
    │  │  └─────┬───────┘   └──────┬─────┘ │  │
    │  │        │                  │       │  │
    │  │        ▼                  ▼       │  │
    │  │   postgres://app1_db   postgres://app2_db
    │  │                                   │  │
    │  │        ┌───────────────────┐      │  │
    │  │        │   PostgreSQL DB   │      │  │
    │  │        │   dockerized      │      │  │
    │  │        │   per-app roles   │      │  │
    │  │        └───────────────────┘      │  │
    │  └───────────────────────────────────┘  │
    │                                         │
    │ Backups → /srv/orcta/postgres/backups/  │
    └─────────────────────────────────────────┘

VPS Directory Layout

/srv/apps
  app1/
    docker-compose.yml
    Caddyfile-snippet
  app2/
    docker-compose.yml
    Caddyfile-snippet

/srv/orcta-postgres
  docker-compose.yml
  init/
    01-init.sql
  backups/
  backup.sh

GitHub Actions (Example)

.github/workflows/deploy.yml

name: CI/CD Pipeline (Direct VPS Build)

on:
  push:
    branches: [dev, main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      - name: Deploy on VPS
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.VPS_HOST }}
          username: ${{ secrets.VPS_USER }}
          key: ${{ secrets.VPS_SSH_KEY }}
          script: |
            set -e
            APP=orcta-app
            mkdir -p /srv/apps/$APP
            cd /srv/apps/$APP

            if [ ! -d ".git" ]; then
              git clone git@github.com:Orctatech-Engineering-Team/$APP.git .
            else
              git pull origin main
            fi

            docker compose up --build -d

PostgreSQL Setup

/srv/orcta-postgres/docker-compose.yml

version: "3.9"
services:
  postgres:
    image: postgres:16
    container_name: orcta_postgres
    restart: always
    environment:
      POSTGRES_PASSWORD: <set-in-env>
    volumes:
      - ./data:/var/lib/postgresql/data
      - ./init:/docker-entrypoint-initdb.d
    networks:
      - orcta_net

  pgbouncer:
    image: edoburu/pgbouncer
    container_name: orcta_pgbouncer
    restart: always
    ports:
      - "6432:6432"
    environment:
      DB_USER: pgbouncer
      DB_PASSWORD: <set-in-env>
      DB_HOST: postgres
      DB_PORT: 5432
      POOL_MODE: transaction
      MAX_CLIENT_CONN: 200
      DEFAULT_POOL_SIZE: 20
    depends_on:
      - postgres
    networks:
      - orcta_net

networks:
  orcta_net:
    external: true

Example init script (/srv/orcta-postgres/init/01-init.sql):

CREATE USER app1_user WITH PASSWORD '<password>';
CREATE DATABASE app1_db OWNER app1_user;
REVOKE ALL ON DATABASE app1_db FROM PUBLIC;

Caddy Reverse Proxy

Main /etc/caddy/Caddyfile:

{
    email admin@orctatech.com
}

import /srv/apps/*/Caddyfile-snippet

Example snippet:

orcta.example.com {
    reverse_proxy localhost:9999
}

Backups

/srv/orcta-postgres/backup.sh:

#!/bin/bash
TIMESTAMP=$(date +%F_%H-%M-%S)
docker exec -t orcta-postgres pg_dumpall -U postgres > /srv/orcta-postgres/backups/db_$TIMESTAMP.sql

Cron (2 AM daily):

0 2 * * * /srv/orcta-postgres/backup.sh

Monitoring & Troubleshooting

  • App logs: docker logs -f <container>
  • Postgres logs: docker logs -f orcta-postgres
  • Caddy logs: journalctl -u caddy --no-pager -n 100
Edit this page