The platform ships as a Docker Compose stack. Production and development use separate compose files so that development conveniences never reach a live install.
Services
| Service | Role |
|---|---|
| app | PHP-FPM running the Laravel API. |
| nginx | Serves the API and static public assets, and fronts app. |
| postgres | PostgreSQL database. |
| redis | Cache, sessions and queues. |
| queue-worker | Processes queued jobs, including all provisioning. |
| scheduler | Runs the cron-driven tasks: billing, reminders, backups. |
| frontend | Next.js portal (development compose only; in production this is usually built and hosted separately). |
Starting production
docker compose -f docker-compose.prod.yml up -d --build
docker compose -f docker-compose.prod.yml exec app php artisan migrate --forceNoteapp and nginx are separate images, and the nginx image bakes in backend/public at build time. If you change anything under backend/public, rebuild nginx as well or the old file will keep being served.
Applying code changes
Application code is baked into the image rather than mounted, so a git pull alone changes nothing that is running. Rebuild the app image, recreate the containers, then run migrations.
git pull --ff-only
docker compose -f docker-compose.prod.yml build app
docker compose -f docker-compose.prod.yml up -d --no-deps app queue-worker scheduler
docker compose -f docker-compose.prod.yml exec app php artisan migrate --forceTipRecreating app gives it a new container IP. If nginx resolved the old one at start-up you will see 502s until you restart nginx too.