# Coolify deployment — Build Pack: Docker Compose # This is the production APP stack (app + workers ONLY). MariaDB and Redis are # run as SEPARATE Coolify Database Resources, not part of this compose. # - Create a standalone MariaDB resource and a standalone Redis resource in Coolify. # - Enable "Connect to Predefined Network" on THIS app stack. # - Set DATABASE_URL / REDIS_URL / MESSENGER_TRANSPORT_DSN in Coolify's env tab, # pointing at the resources via their internal hostnames (mariadb- / redis-). # The ddev local env uses its own compose.yaml; do not confuse the two. # Select this file (docker-compose.yml) as the Compose file in the Coolify resource. # Do NOT define custom `networks:` — Coolify manages the network; custom ones # break Traefik routing (per Coolify docs). # Shared application environment, reused by app + both workers. x-app-env: &app-env APP_ENV: prod APP_DEBUG: "0" APP_SECRET: ${APP_SECRET} # Connection strings point at the SEPARATE Coolify DB/Redis resources. # Real values are injected from Coolify's Environment Variables tab — the # hostname is mariadb- / redis- from each resource's page, so they # cannot be hardcoded here (the UUID differs per installation). DATABASE_URL: ${DATABASE_URL} MESSENGER_TRANSPORT_DSN: ${MESSENGER_TRANSPORT_DSN} REDIS_URL: ${REDIS_URL} JWT_SECRET_KEY: "%kernel.project_dir%/config/jwt/private.pem" JWT_PUBLIC_KEY: "%kernel.project_dir%/config/jwt/public.pem" JWT_PASSPHRASE: ${JWT_PASSPHRASE} CORS_ALLOW_ORIGIN: ${CORS_ALLOW_ORIGIN} DEFAULT_URI: ${APP_BASE_URL} APP_BASE_URL: ${APP_BASE_URL} ALLOWED_FRONTEND_HOSTS: ${ALLOWED_FRONTEND_HOSTS} TRUSTED_PROXIES: ${TRUSTED_PROXIES} API_IR_BASE_URL: ${API_IR_BASE_URL} API_IR_TOKEN: ${API_IR_TOKEN} REFRESH_TOKEN_TTL: "2592000" OTP_TTL: "1200" MAX_FILE_SIZE_BYTES: "5242880" UPLOAD_DIR: var/uploads # Shared volume mounts for services that read/write JWT keys and uploads. x-app-volumes: &app-volumes - jwt_keys:/app/config/jwt - uploads_public:/app/public/uploads - uploads_var:/app/var/uploads services: # Web app (PHP-FPM + Nginx). # In Coolify, assign ALL serving domains to THIS service (port 80) — the backend # API host plus every frontend domain you want Traefik to route + issue TLS for. # Coolify supports a comma-separated domain list on the service. # CORS/payload allow-listing for those frontends is handled separately via # CORS_ALLOW_ORIGIN / ALLOWED_FRONTEND_HOSTS (see docker/frontend-domains.json). app: # Shared image tag — app builds it once; the two workers reuse the SAME image # (image: below, no build:). Without this Coolify builds the Dockerfile 3×, # tripling apk network fetches and intermittently failing on the host. image: clinicpro-app:latest build: context: . dockerfile: Dockerfile restart: unless-stopped environment: <<: *app-env RUN_INIT: "1" # runs JWT keygen + migrations on start (only this service) volumes: *app-volumes networks: [coolify] # Hit the real /health route (nginx on 8080, non-root) via docker/healthcheck.sh. healthcheck: test: ["CMD", "/usr/local/bin/healthcheck.sh"] interval: 15s timeout: 5s retries: 5 start_period: 60s # SMS / async message consumer. Reuses the image built by `app` (no build:). worker-async: image: clinicpro-app:latest restart: unless-stopped command: php bin/console messenger:consume async --time-limit=3600 --memory-limit=128M -v environment: <<: *app-env RUN_INIT: "0" volumes: *app-volumes networks: [coolify] depends_on: [app] # Messenger consumers handle SIGTERM gracefully — give the in-flight message # time to finish before the container is killed. stop_grace_period: 30s # No port to probe — just confirm the consumer process is alive (busybox ps). healthcheck: test: ["CMD-SHELL", "ps -o args 2>/dev/null | grep -q '[m]essenger:consume async' || exit 1"] interval: 30s timeout: 5s retries: 3 start_period: 20s # Scheduler consumer — expires unpaid appointments every minute. Reuses app's image. worker-scheduler: image: clinicpro-app:latest restart: unless-stopped command: php bin/console messenger:consume scheduler_default --time-limit=3600 -v environment: <<: *app-env RUN_INIT: "0" volumes: *app-volumes networks: [coolify] depends_on: [app] stop_grace_period: 30s healthcheck: test: ["CMD-SHELL", "ps -o args 2>/dev/null | grep -q '[m]essenger:consume scheduler_default' || exit 1"] interval: 30s timeout: 5s retries: 3 start_period: 20s volumes: jwt_keys: uploads_public: uploads_var: networks: coolify: name: coolify driver: bridge external: true