Skip to main content

CI/CD Pipeline

Overview

push to develop → GitHub Actions CI → deploy to Atlas (staging)
push to master → GitHub Actions CI → deploy to Hermes (production)

All pipeline configuration is in .github/workflows/ci.yml.


CI Job (runs on ubuntu-latest)

Triggered on push or PR to master or develop.

Steps

  1. Checkoutactions/checkout@v4
  2. Setup pnpmpnpm/action-setup@v4, version 10.32.1
  3. Setup Nodeactions/setup-node@v4, Node 20, pnpm cache
  4. Install dependenciespnpm install --frozen-lockfile
  5. Generate Prisma clientpnpm --filter api exec prisma generate (required before typecheck)
  6. Typecheckpnpm turbo typecheck
  7. Lintpnpm turbo lint
  8. Buildpnpm turbo build

The CI job sets NEXT_PUBLIC_API_URL=http://localhost:3001 — required for the Next.js build to succeed (the variable is used at build time via next.config.ts).


Deploy Jobs

Both deploy jobs run on the self-hosted runner (which runs on the respective server).

Deploy jobs call the Coolify API locally (no network hop):

curl -v -X GET \
-H "Authorization: Bearer ${{ secrets.COOLIFY_API_TOKEN }}" \
"http://localhost:8000/api/v1/deploy?uuid=${{ secrets.COOLIFY_UUID_WEB }}&force=false"

http://localhost:8000 — Coolify listens on port 8000 on the server. The runner talks to Coolify over localhost, so the Coolify API token is not exposed over the network.

Production (deploy-production)

  • Condition: github.ref == 'refs/heads/master'
  • Needs: ci job
  • Deploys: web (COOLIFY_UUID_WEB) and API (COOLIFY_UUID_API)
  • Runner: self-hosted on Hermes

Staging (deploy-staging)

  • Condition: github.ref == 'refs/heads/develop'
  • Needs: ci job
  • Deploys: web staging (COOLIFY_UUID_WEB_STAGING) and API staging (COOLIFY_UUID_API_STAGING)
  • Runner: self-hosted on Atlas

GitHub Secrets

SecretUsed by
COOLIFY_API_TOKENAll deploy jobs
COOLIFY_UUID_WEBdeploy-production
COOLIFY_UUID_APIdeploy-production
COOLIFY_UUID_WEB_STAGINGdeploy-staging
COOLIFY_UUID_API_STAGINGdeploy-staging
COOLIFY_DOCS_WEBHOOK_URLdeploy-docs

Docs Deploy Job (deploy-docs)

The docs site deploys on both master and develop (no staging/prod split — there's one docs site on Iris):

deploy-docs:
name: Deploy Docs to Iris
runs-on: self-hosted
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop'
needs: [] # No dependency on CI job — docs don't need Prisma or full build
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm --filter=@pcmr/docs build
- name: Deploy to Coolify
run: curl -X POST "${{ secrets.COOLIFY_DOCS_WEBHOOK_URL }}"

needs: [] — docs don't depend on the CI job. Docs can deploy even if typecheck/lint fails (docs are content, not code). Adjust this if you want to block docs deploys on CI failures.


Coolify

Coolify UI: https://coolify.ctsolutions.gr (Cloudflare Access — email OTP).

App Configuration

Both apps use explicit Dockerfiles (not Nixpacks — pnpm monorepos aren't handled reliably by Nixpacks):

SettingValue
Build packDockerfile
Base directory/ (Turborepo needs the full workspace)
Dockerfile pathapps/web/Dockerfile or apps/api/Dockerfile

Environment Variables

All environment variables are set in the Coolify UI — never committed to the repository.

Container Logs

Access via Coolify UI → Application → Logs tab, or via:

sudo docker logs <container_name> --follow

Migration Automation

apps/api/start.sh runs before the NestJS server starts on every container boot:

#!/bin/sh
set -e
echo "Running database migrations..."
node /app/node_modules/.pnpm/.../prisma/build/index.js \
migrate deploy \
--config /app/prisma/prisma.config.deploy.js
echo "Migrations complete. Starting server..."
exec node dist/src/main.js

This means migrations run automatically on every deploy — no manual intervention needed for schema changes. migrate deploy applies only unapplied migrations (idempotent).

prisma.config.deploy.js uses absolute paths and module.exports format (not ESM) — required by Prisma v7's config file format:

module.exports = {
schema: "/app/prisma/schema.prisma",
migrations: "/app/prisma/migrations",
};

Baseline Migration Strategy

Migration 9 (20260419164134) is a full schema baseline — it contains the complete schema. Migrations 1–8 are superseded.

For new environments (fresh database):

  1. Drop and recreate the schema
  2. Mark migrations 1–8 as applied without running them:
    for i in 1 2 3 4 5 6 7 8; do
    prisma migrate resolve --applied "<migration_name_$i>"
    done
  3. Run prisma migrate deploy — only migration 9 executes against the empty schema