church-website/infra
Benno Tielen 48548e27f0 fix: docs
2026-04-24 09:13:39 +02:00
..
ansible fix: more details deployment 2026-04-16 13:22:17 +02:00
scripts feature: infrastructure for deployment 2026-04-10 11:39:02 +02:00
README.md fix: docs 2026-04-24 09:13:39 +02:00

Infrastructure & Deployment

Architecture

VPS (Ubuntu 24, 8 GB RAM)
├── Caddy          — reverse proxy + auto SSL (native)
├── PostgreSQL     — postgis/postgis:16-3.4 (Docker)
├── Forgejo        — git server + CI/CD (Docker)
├── Forgejo Runner — executes CI/CD jobs (Docker)
├── app-staging    — Next.js + Payload CMS (Docker)
└── app-test       — Next.js + Payload CMS (Docker)
URL Port Purpose
mutter-teresa.skick.app 3001 Client demo (staging)
mutter-teresa-test.skick.app 3002 Developer testing
git.skick.app 3003 Forgejo git server

All app and database containers share the Docker network church-website-net.


Prerequisites

  • Ansible installed locally (pip install ansible or brew install ansible)
  • SSH access to the VPS (root or sudo user)
  • DNS records pointing to the VPS IP:
    • mutter-teresa.skick.app → VPS IP
    • mutter-teresa-test.skick.app → VPS IP
    • git.skick.app → VPS IP

Quick Start: First-Time Server Setup

1. Configure secrets

Create an encrypted vault from the example template:

cd infra/ansible
cp inventory/group_vars/all/vault.yml.example inventory/group_vars/all/vault.yml
ansible-vault encrypt inventory/group_vars/all/vault.yml
ansible-vault edit inventory/group_vars/all/vault.yml

Fill in all CHANGE_ME values:

  • vault_ansible_become_pass — VPS root password
  • vault_postgres_root_password — PostgreSQL root password
  • vault_db_password_staging / vault_db_password_test — database passwords
  • vault_payload_secret_staging / vault_payload_secret_test — Payload CMS secrets
  • vault_google_bucket — Google Cloud Storage bucket name
  • vault_resend_api_key — Resend email API key
  • vault_repo_url — Forgejo repository URL (e.g., ssh://git@git.skick.app:2222/org/church-website.git)

2. Configure inventory

Edit infra/ansible/inventory/test.yml:

  • Set ansible_host to your VPS IP address
  • Adjust ansible_user and SSH key path if needed

3. Run the playbook

cd infra/ansible
ansible-playbook playbooks/setup.yml -i inventory/test.yml --ask-vault-pass

This will:

  1. Install Docker, configure firewall
  2. Start PostgreSQL with both databases
  3. Install and configure Caddy with SSL
  4. Start Forgejo and the CI/CD runner
  5. Clone the repo, build, and deploy both environments

4. Set up Forgejo

After the playbook completes:

  1. Visit https://git.skick.app and complete the initial Forgejo setup
  2. Create an organization and repository
  3. Add the VPS SSH key to the repository for pull access
  4. Register the Forgejo Runner:
    ssh root@YOUR_VPS_IP
    docker exec -it forgejo-runner forgejo-runner register \
      --instance https://git.skick.app \
      --token YOUR_RUNNER_TOKEN \
      --name local-runner \
      --labels ubuntu-latest:docker://node:22
    
  5. Push to the staging branch — CI/CD will deploy automatically

Environment Variables

Variable Description Build-time?
DATABASE_URI PostgreSQL connection string No
PAYLOAD_SECRET Payload CMS encryption secret No
NEXT_PUBLIC_SERVER_URL Public URL of the app Yes
NEXT_PUBLIC_SITE_ID Site identifier (e.g., chemnitz) Yes
GOOGLE_BUCKET GCS bucket for media storage No
RESEND_API_KEY Resend API key for emails No

Variables marked "Build-time" are baked into the Docker image during docker build (via --build-arg). Changes to these require a rebuild.


Manual Operations

Check container logs

docker logs app-staging
docker logs app-test
docker logs postgres
docker logs forgejo

Redeploy manually (without CI/CD)

cd /opt/church-website/repo
git pull origin staging
/opt/church-website/scripts/deploy.sh staging 3001
/opt/church-website/scripts/deploy.sh test 3002

Run migrations manually

docker exec app-staging npx payload migrate
docker exec app-test npx payload migrate

Database backup

# Backup staging database
docker exec postgres pg_dump -U church_website_staging church_website_staging > backup_staging_$(date +%Y%m%d).sql

# Backup test database
docker exec postgres pg_dump -U church_website_test church_website_test > backup_test_$(date +%Y%m%d).sql

# Backup all databases
docker exec postgres pg_dumpall -U postgres > backup_all_$(date +%Y%m%d).sql

Database restore

# Restore staging database
cat backup_staging.sql | docker exec -i postgres psql -U church_website_staging church_website_staging

Restart a single service

docker restart app-staging
docker restart app-test
docker restart postgres

Deploy via Ansible (without CI/CD)

Use these playbooks to deploy from your local machine — no Forgejo runner needed.

cd ansible

# Deploy both environments (git pull once, then build+deploy each sequentially)
ansible-playbook playbooks/deploy.yml --ask-vault-pass
# Deploy staging only
ansible-playbook playbooks/deploy-staging.yml --ask-vault-pass

# Deploy test only
ansible-playbook playbooks/deploy-test.yml --ask-vault-pass

Steps executed per environment:

  1. Pull latest code from the configured branch (staging)
  2. Build app Docker image (bakes in NEXT_PUBLIC_SERVER_URL and NEXT_PUBLIC_SITE_ID)
  3. Build migration image and run npx payload migrate
  4. Stop and remove the old container
  5. Start the new container
  6. Fix upload volume permissions
  7. Prune old Docker images

Deploy a specific branch:

ansible-playbook playbooks/deploy.yml --ask-vault-pass -e repo_branch=feature/my-branch

Note: The server must already be provisioned with setup.yml before deploying. The deploy playbooks only pull code and rebuild containers — they do not install Docker, Caddy, or PostgreSQL.


Refresh Test from Staging

copy-staging-to-test.yml rebuilds the test environment as a clone of staging — useful when you want editors or developers to try out destructive changes against a realistic dataset without touching the staging client demo.

cd infra/ansible
ansible-playbook playbooks/copy-staging-to-test.yml --ask-vault-pass

What it does:

  1. Verifies the postgres container is up and the staging database exists
  2. Stops and removes the app-test container
  3. Drops church_website_test, recreates it, enables PostGIS, and pipes a pg_dump of staging into it
  4. Reassigns table/sequence/enum ownership in the test DB to church_website_test
  5. Replaces the uploads-test-media and uploads-test-documents Docker volumes with the contents of their staging counterparts
  6. Starts a new app-test container from the existing church-website:test image on port 3002
  7. Fixes upload volume permissions and waits for http://127.0.0.1:3002 to return 2xx/3xx

Before running:

  • The test image (church-website:test) must already exist on the VPS — this playbook does not rebuild it. Run deploy-test.yml first if the image is missing or stale.
  • All connections to church_website_test are forcibly terminated. Anyone editing in the test admin will be kicked.
  • The test DB and upload volumes are wiped — there is no rollback. Take a backup first if anything in test is worth keeping (see Database backup).

CI/CD

The Forgejo Actions workflow (.forgejo/workflows/deploy.yml) triggers on push to the staging branch. It:

  1. Pulls the latest code on the VPS
  2. Builds a new Docker image for staging
  3. Stops the old container, starts the new one
  4. Runs database migrations
  5. Repeats for the test environment (sequentially, to save RAM)

Adding a New Environment

  1. Add a new entry to app_environments in the inventory file
  2. Add a new entry to caddy_domains with the new domain
  3. Add a new database entry to databases
  4. Run the playbook: ansible-playbook playbooks/setup.yml -i inventory/test.yml
  5. Update the deploy workflow to include the new environment

Production Setup

  1. Copy and edit the production inventory:
    cp infra/ansible/inventory/production.yml infra/ansible/inventory/my-production.yml
    
  2. Fill in the production VPS IP, domain, and secrets
  3. Run the playbook (skip Forgejo):
    ansible-playbook playbooks/setup.yml -i inventory/my-production.yml --ask-vault-pass
    
  4. Set up a deploy workflow for production (triggered on tags/releases)

Troubleshooting

Build fails with OOM

The VPS has 4 GB RAM + 2 GB swap. Docker builds can peak at ~1.5 GB. If builds fail:

  • Ensure only one build runs at a time (deploy script is sequential)
  • Check swap: free -h
  • Increase swap: edit swap_size_mb in inventory and re-run playbook

SSL certificate not working

  • Ensure DNS records point to the VPS IP: dig mutter-teresa.skick.app
  • Check Caddy logs: journalctl -u caddy
  • Caddy auto-renews certificates — if stuck, restart: systemctl restart caddy

Database connection refused

  • Check PostgreSQL is running: docker ps | grep postgres
  • Check the container is on the right network: docker network inspect church-website-net
  • Test connection: docker exec postgres psql -U postgres -l

Container won't start

  • Check logs: docker logs app-staging
  • Check if port is in use: ss -tlnp | grep 3001
  • Check .env file: cat /opt/church-website/envs/staging/.env

Local Development

For local development with PostgreSQL:

# Start PostgreSQL (from project root)
docker compose up -d

# Configure .env
DATABASE_URI=postgres://postgres:password@localhost:5432/church_website_dev

# Start dev server
npm run dev