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 (
devandmainbranches). - 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
- Push to
mainordev. - GitHub Actions connects to the VPS via SSH.
- The repo is cloned or pulled into
/srv/apps/<APP>. - Docker Compose rebuilds and restarts the service.
- 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 -dPostgreSQL 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: trueExample 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.sqlCron (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