Church App — Local Development Environment

Church App — Local Development Environment

PHP and Yarn run on the host. Everything else in Docker Compose. One docker compose up and you’re ready to code.


Philosophy

  • PHP on host: Faster execution, better IDE integration (PHPStan, Xdebug, autocompletion). No container file sync overhead. Symfony CLI provides a local web server with TLS.
  • Yarn on host: Same reasoning. Hot reload, fast builds, native file watching. Expo CLI needs direct host access for device connections.
  • Everything else in Docker: PostgreSQL, Redis, Zitadel, Mailpit (local email testing), MinIO (S3-compatible, replaces R2 locally). Consistent versions, isolated data, disposable.

Prerequisites

REQUIRED ON HOST:
├── PHP 8.3+ with extensions:
│   ├── pdo_pgsql, intl, mbstring, xml, curl, zip, gd, redis
│   ├── Composer 2.x
│   └── Symfony CLI (for local web server + TLS)

├── Node.js 20 LTS + Yarn (or npm)
│   ├── Expo CLI (npx expo)
│   └── EAS CLI (for builds, optional locally)

├── Docker + Docker Compose v2

└── OPTIONAL:
    ├── Xdebug 3.x (PHP debugging)
    ├── make (for Makefile shortcuts)
    └── mkcert (for local TLS certificates if not using Symfony CLI)

Docker Compose Services

# docker-compose.yml

services:

  # ──────────────────────────────────────
  # DATABASE
  # ──────────────────────────────────────
  postgres:
    image: postgres:16-alpine
    container_name: churchapp-postgres
    ports:
      - "5432:5432"
    environment:
      POSTGRES_DB: churchapp
      POSTGRES_USER: churchapp
      POSTGRES_PASSWORD: churchapp_dev
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./infrastructure/docker/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U churchapp"]
      interval: 5s
      timeout: 3s
      retries: 5

  # ──────────────────────────────────────
  # CACHE / MESSAGE QUEUE / EVENT BUS
  # ──────────────────────────────────────
  redis:
    image: redis:7-alpine
    container_name: churchapp-redis
    ports:
      - "6379:6379"
    command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5

  # ──────────────────────────────────────
  # IDENTITY PROVIDER
  # ──────────────────────────────────────
  zitadel:
    image: ghcr.io/zitadel/zitadel:latest
    container_name: churchapp-zitadel
    command: start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled
    ports:
      - "8080:8080"
    environment:
      ZITADEL_DATABASE_POSTGRES_HOST: postgres
      ZITADEL_DATABASE_POSTGRES_PORT: 5432
      ZITADEL_DATABASE_POSTGRES_DATABASE: zitadel
      ZITADEL_DATABASE_POSTGRES_USER_USERNAME: zitadel
      ZITADEL_DATABASE_POSTGRES_USER_PASSWORD: zitadel_dev
      ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE: disable
      ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME: churchapp
      ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD: churchapp_dev
      ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE: disable
      ZITADEL_EXTERNALSECURE: "false"
      ZITADEL_EXTERNALPORT: 8080
      ZITADEL_EXTERNALDOMAIN: localhost
      ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME: admin@localhost
      ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD: Admin123!
    depends_on:
      postgres:
        condition: service_healthy

  # ──────────────────────────────────────
  # LOCAL EMAIL TESTING (replaces Mailgun in dev)
  # ──────────────────────────────────────
  mailpit:
    image: axllent/mailpit:latest
    container_name: churchapp-mailpit
    ports:
      - "1025:1025"   # SMTP
      - "8025:8025"   # Web UI (view sent emails)
    environment:
      MP_SMTP_AUTH_ACCEPT_ANY: "true"
      MP_SMTP_AUTH_ALLOW_INSECURE: "true"

  # ──────────────────────────────────────
  # S3-COMPATIBLE OBJECT STORAGE (replaces Cloudflare R2 in dev)
  # ──────────────────────────────────────
  minio:
    image: minio/minio:latest
    container_name: churchapp-minio
    ports:
      - "9000:9000"   # API
      - "9001:9001"   # Console UI
    command: server /data --console-address ":9001"
    environment:
      MINIO_ROOT_USER: churchapp
      MINIO_ROOT_PASSWORD: churchapp_dev
    volumes:
      - minio_data:/data

  # ──────────────────────────────────────
  # DATABASE ADMIN (optional, useful for quick queries)
  # ──────────────────────────────────────
  adminer:
    image: adminer:latest
    container_name: churchapp-adminer
    ports:
      - "8081:8080"
    depends_on:
      - postgres

volumes:
  postgres_data:
  redis_data:
  minio_data:

PostgreSQL Init Script

-- infrastructure/docker/postgres/init.sql
-- Creates the separate database for Zitadel and enables extensions

CREATE USER zitadel WITH PASSWORD 'zitadel_dev';
CREATE DATABASE zitadel OWNER zitadel;

-- Extensions for the main app database
\c churchapp;
CREATE EXTENSION IF NOT EXISTS ltree;
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

Service Map

HOST MACHINE (your laptop)

├── PHP 8.3 (Symfony CLI)
│   ├── symfony serve → https://localhost:8000 (API + admin backend)
│   ├── Connects to: postgres:5432, redis:6379, mailpit:1025, minio:9000
│   ├── Symfony Messenger workers: symfony run -- php bin/console messenger:consume
│   └── Xdebug listens on: host port 9003

├── Yarn / Expo
│   ├── npx expo start → Expo dev server (mobile + web)
│   ├── Web: http://localhost:8081 (Expo Web / PWA dev)
│   ├── iOS: Expo Go or dev build on simulator
│   ├── Android: Expo Go or dev build on emulator
│   └── Connects to: Symfony API at https://localhost:8000

└── Docker Compose
    ├── postgres:5432      — main DB + Zitadel DB
    ├── redis:6379         — cache, message queue, event bus
    ├── zitadel:8080       — identity provider UI + API
    ├── mailpit:8025       — email testing UI (view all sent emails)
    ├── minio:9000/9001    — object storage (S3 API + web console)
    └── adminer:8081       — database admin UI (optional)

Local URLs

ServiceURLPurpose
Symfony APIhttps://localhost:8000Backend API + admin
Expo Webhttp://localhost:8081React Native web dev
Zitadelhttp://localhost:8080Identity provider console
Mailpithttp://localhost:8025View sent emails
MinIO Consolehttp://localhost:9001Object storage browser
Adminerhttp://localhost:8081Database admin
Redislocalhost:6379(no UI, use redis-cli)

Symfony Configuration (dev environment)

# .env.local (not committed, dev overrides)

DATABASE_URL="postgresql://churchapp:churchapp_dev@localhost:5432/churchapp?serverVersion=16&charset=utf8"
REDIS_URL="redis://localhost:6379"
MAILER_DSN="smtp://localhost:1025"

# Zitadel
ZITADEL_URL="http://localhost:8080"
ZITADEL_ISSUER="http://localhost:8080"

# MinIO (S3-compatible)
S3_ENDPOINT="http://localhost:9000"
S3_ACCESS_KEY="churchapp"
S3_SECRET_KEY="churchapp_dev"
S3_BUCKET="churchapp-media"
S3_REGION="us-east-1"
S3_USE_PATH_STYLE="true"

# App
APP_ENV=dev
APP_SECRET=dev_secret_change_in_production

Makefile (Developer Shortcuts)

# Makefile — top-level developer commands

.PHONY: up down restart logs db-reset db-migrate db-seed test lint ci worker

# ── Docker ──────────────────────────────
up:
	docker compose up -d
	@echo "Services started. Waiting for health checks..."
	@sleep 3
	@docker compose ps

down:
	docker compose down

restart:
	docker compose down && docker compose up -d

logs:
	docker compose logs -f

# ── Database ────────────────────────────
db-migrate:
	php bin/console doctrine:migrations:migrate --no-interaction

db-reset:
	php bin/console doctrine:database:drop --force --if-exists
	php bin/console doctrine:database:create
	php bin/console doctrine:migrations:migrate --no-interaction
	@echo "Database reset complete."

db-seed:
	php bin/console app:seed:demo-data
	@echo "Demo data seeded."

# ── Symfony ─────────────────────────────
serve:
	symfony serve -d

worker:
	symfony run -- php bin/console messenger:consume async audit history --time-limit=3600 -vv

# ── Frontend ────────────────────────────
expo:
	cd frontend && npx expo start

expo-web:
	cd frontend && npx expo start --web

# ── Quality ─────────────────────────────
lint:
	vendor/bin/phpstan analyse --level=8
	vendor/bin/deptrac analyse
	cd frontend && npx eslint . && npx tsc --noEmit

test:
	vendor/bin/phpunit

test-integration:
	vendor/bin/phpunit --testsuite=integration

# ── Full CI check (run before pushing) ──
ci: lint test
	@echo "All checks passed."

# ── MinIO bucket setup ──────────────────
minio-setup:
	docker compose exec minio mc alias set local http://localhost:9000 churchapp churchapp_dev
	docker compose exec minio mc mb local/churchapp-media --ignore-existing
	@echo "MinIO bucket created."

# ── Complete local setup ────────────────
setup: up minio-setup db-reset db-migrate db-seed
	@echo ""
	@echo "Local environment ready!"
	@echo "  API:      symfony serve"
	@echo "  Frontend: cd frontend && npx expo start"
	@echo "  Zitadel:  http://localhost:8080"
	@echo "  Mailpit:  http://localhost:8025"
	@echo "  MinIO:    http://localhost:9001"

First-Time Setup

# 1. Clone the repo
git clone git@gitlab.com:segli/church-app.git
cd church-app

# 2. Install PHP dependencies
composer install

# 3. Install frontend dependencies
cd frontend && yarn install && cd ..

# 4. Copy environment config
cp .env .env.local
# Edit .env.local if needed (defaults should work)

# 5. Start all Docker services + setup DB + seed data
make setup

# 6. Start Symfony dev server
make serve

# 7. Start Expo (in a second terminal)
make expo

# 8. Open in browser
# API + Admin:  https://localhost:8000
# Expo Web:     http://localhost:8081
# Zitadel:      http://localhost:8080 (admin@localhost / Admin123!)
# Mailpit:      http://localhost:8025 (check emails here)
# MinIO:        http://localhost:9001 (churchapp / churchapp_dev)

# You're ready to develop.

Working With the Environment

Daily workflow

# Morning: start everything
make up && make serve
# (in another terminal)
make expo

# Run Symfony Messenger workers (for async events, audit, history)
make worker

# Check sent emails (password resets, invites, etc.)
open http://localhost:8025

# Run quality checks before pushing
make ci

Reset everything

# Nuclear option: wipe all data, start fresh
docker compose down -v    # removes volumes (all data!)
make setup                # recreate everything

Run a single test

vendor/bin/phpunit tests/Unit/Module/Groups/Domain/Model/GroupTest.php

Database access

# Via psql
docker compose exec postgres psql -U churchapp -d churchapp

# Via Adminer UI
open http://localhost:8081
# Server: postgres, User: churchapp, Password: churchapp_dev, Database: churchapp

Check Redis

docker compose exec redis redis-cli
> KEYS *
> MONITOR    # watch all commands in real-time

Environment Differences

ConcernLocal (dev)StagingProduction
PHPHost (Symfony CLI)DockerDocker
DatabaseDocker (postgres:16)Hetzner CPX11 (Docker)Hetzner CPX11 (Docker)
RedisDockerDockerDocker
IdentityDocker (Zitadel)Docker (Zitadel)Docker (Zitadel)
EmailMailpit (catches all)Mailgun (sandbox)Mailgun (EU region)
StorageMinIO (local S3)Cloudflare R2Cloudflare R2
Web appExpo dev serverCloudflare PagesCloudflare Pages
TLSSymfony CLI (self-signed)Let’s EncryptLet’s Encrypt
Domainlocalhoststaging.yourapp.comyourapp.com

The key principle: Symfony’s environment config (APP_ENV=dev) switches all service endpoints. The same code runs everywhere — only the .env values change.


Project Directory Structure

church-app/
├── .ai/                          # Agent guidelines, ADRs, skills, templates
├── .github/
│   └── workflows/                # CI/CD pipelines
├── infrastructure/
│   ├── docker/
│   │   ├── postgres/
│   │   │   └── init.sql          # DB init script
│   │   ├── php/
│   │   │   └── Dockerfile        # Production PHP image
│   │   └── nginx/
│   │       └── nginx.conf        # Production web server
│   ├── opentofu/                 # IaC for staging + production
│   └── scripts/
│       ├── db-clone.sh
│       └── deploy.sh
├── src/                          # Symfony application (DDD structure)
│   ├── Core/
│   ├── Module/
│   └── Pipeline/
├── config/                       # Symfony config
├── migrations/                   # Doctrine migrations
├── tests/                        # PHPUnit tests
├── admin/                        # Admin backend (Twig + Vue)
├── frontend/                     # React Native (Expo) project
│   ├── app/                      # Expo Router pages
│   ├── components/               # Shared components
│   ├── lib/                      # API client, auth, storage
│   └── package.json
├── docker-compose.yml            # Local dev services
├── Makefile                      # Developer shortcuts
├── composer.json
├── deptrac.yaml
├── phpunit.xml
└── .env                          # Default config (committed)

Status: Local Dev Environment — Ready for setup Run make setup and start building.