services: db: image: postgres:18-alpine environment: POSTGRES_USER: ledgr POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-ledgr} POSTGRES_DB: ledgr command: > postgres -c shared_buffers=256MB -c work_mem=16MB -c maintenance_work_mem=64MB -c random_page_cost=1.1 volumes: - pgdata:/var/lib/postgresql ports: - "${DB_PORT:-5433}:5432" restart: unless-stopped healthcheck: test: ["CMD-SHELL", "pg_isready -U ledgr"] interval: 10s timeout: 5s retries: 5 logging: driver: json-file options: max-size: "10m" max-file: "3" app: image: ghcr.io/kentaniguchi-r/ledgr:latest depends_on: db: condition: service_healthy ports: - "${PORT:-4200}:3000" volumes: - appdata:/data env_file: - path: .env required: false environment: PORT: 3000 LEDGR_DATA_DIR: /data # Superuser/owner — migrations and app-role provisioning only. RLS # policies (docs/rls-pilot.md) never restrict this role. DATABASE_URL: postgresql://ledgr:${POSTGRES_PASSWORD:-ledgr}@db:5432/ledgr # Restricted, non-superuser role the app actually queries through, so # RLS policies apply. ensure-app-role.mjs creates it on boot from these # credentials — reuses POSTGRES_PASSWORD rather than a separate secret # since both roles are equally trusted inside this container/network. APP_DATABASE_URL: postgresql://ledgr_app:${POSTGRES_PASSWORD:-ledgr}@db:5432/ledgr BETTER_AUTH_URL: http://localhost:${PORT:-4200} NEXT_PUBLIC_APP_URL: http://localhost:${PORT:-4200} restart: unless-stopped healthcheck: test: ["CMD", "node", "-e", "fetch('http://localhost:3000/api/health').then(r=>{if(!r.ok)throw 1}).catch(()=>process.exit(1))"] interval: 30s timeout: 5s retries: 3 start_period: 30s logging: driver: json-file options: max-size: "10m" max-file: "3" volumes: pgdata: appdata: