services: postgres: image: postgres:16 restart: unless-stopped environment: POSTGRES_DB: ${POSTGRES_DB} POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} volumes: - postgres_data:/var/lib/postgresql/data - ./docker/postgres-init:/docker-entrypoint-initdb.d ports: # Host-loopback only — this is for local operator access (psql), not for the # network/internet to reach. Nothing in the stack needs it published beyond # localhost; laravel-fpm/worker talk to `postgres` over the internal Docker network. - "127.0.0.1:${POSTGRES_PORT}:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] interval: 5s timeout: 5s retries: 10 # C# data-ingestion worker: fetches/blends weather + computes astronomy, persists to # Postgres. Internal-only (expose, not ports) — Laravel is the only caller. worker: build: context: ./backend/AstroCast.Worker restart: unless-stopped environment: ConnectionStrings__Postgres: "Host=postgres;Port=5432;Database=${POSTGRES_DB};Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD}" Internal__ApiToken: "${WORKER_API_TOKEN:?WORKER_API_TOKEN must be set}" expose: - "8080" depends_on: postgres: condition: service_healthy laravel-fpm: build: context: ./webapp target: fpm restart: unless-stopped environment: &laravel_env APP_NAME: AstroCast APP_ENV: "${APP_ENV:-production}" # No default — must come from the root .env (gitignored), never a literal here. APP_KEY: "${APP_KEY:?APP_KEY must be set (php artisan key:generate --show)}" APP_DEBUG: "${APP_DEBUG:-false}" APP_URL: "${APP_URL:-http://localhost:8080}" LOG_CHANNEL: stderr DB_CONNECTION: pgsql DB_HOST: postgres DB_PORT: 5432 DB_DATABASE: ${POSTGRES_DB} DB_USERNAME: ${POSTGRES_USER} DB_PASSWORD: ${POSTGRES_PASSWORD} SESSION_DRIVER: database SESSION_SECURE_COOKIE: "${SESSION_SECURE_COOKIE:-true}" TRUSTED_PROXIES: "${TRUSTED_PROXIES:-*}" CACHE_STORE: database QUEUE_CONNECTION: database WORKER_BASE_URL: "http://worker:8080" WORKER_API_TOKEN: "${WORKER_API_TOKEN:?WORKER_API_TOKEN must be set}" RUN_MIGRATIONS: "true" # Seeds a default admin on first boot — see AdminUserSeeder. Leave unset to skip. ADMIN_EMAIL: "${ADMIN_EMAIL:-}" ADMIN_PASSWORD: "${ADMIN_PASSWORD:-}" # Fill in a real SMTP provider's host/port/username/password (via a root .env # override) — see README "Configuring outgoing email". Until then, mail # (verification, clear-sky alerts) fails to send (logged as a warning) rather # than actually delivering. MAIL_MAILER: smtp MAIL_HOST: "${MAIL_HOST:-smtp.your-provider.example}" MAIL_PORT: "${MAIL_PORT:-587}" MAIL_USERNAME: "${MAIL_USERNAME:-}" MAIL_PASSWORD: "${MAIL_PASSWORD:-}" MAIL_FROM_ADDRESS: "${MAIL_FROM_ADDRESS:-noreply@astrocast.app}" MAIL_FROM_NAME: "AstroCast" # Only needed if MAIL_HOST's TLS cert doesn't match MAIL_HOST itself (common on # shared/reseller mail boxes) — see config/mail.php's tls_peer_name comment. MAIL_TLS_PEER_NAME: "${MAIL_TLS_PEER_NAME:-}" volumes: # Shared with laravel-scheduler so its monthly `geoip:update-db` refresh (see # routes/console.php) is actually visible to the container serving requests, # without needing an image rebuild. Seeded from the image's baked-in snapshot # (see Dockerfile) the first time the named volume is created. - geoip_data:/var/www/html/storage/app/geoip # Uploaded gallery photos (see GalleryPhotoController) — unlike the GeoIP snapshot # this is irreplaceable user data, so it must survive container rebuilds/recreates. # Only laravel-fpm ever reads/writes it: every photo (public or private) is served # through an access-controlled Laravel route, never directly by laravel-nginx. - gallery_data:/var/www/html/storage/app/private/gallery expose: - "9000" depends_on: postgres: condition: service_healthy worker: condition: service_started laravel-queue: build: context: ./webapp target: fpm restart: unless-stopped command: ["php", "artisan", "queue:work", "--sleep=1", "--tries=1"] environment: <<: *laravel_env RUN_MIGRATIONS: "false" depends_on: postgres: condition: service_healthy laravel-fpm: condition: service_started laravel-nginx: build: context: ./webapp target: nginx restart: unless-stopped ports: - "8080:80" depends_on: - laravel-fpm # Drives everything scheduled in routes/console.php (alerts:check, status:snapshot) # — nothing ran Laravel's scheduler before this. `schedule:work` polls once a minute # and dispatches whatever's due, so unlike laravel-queue there's no per-job command. laravel-scheduler: build: context: ./webapp target: fpm restart: unless-stopped command: ["php", "artisan", "schedule:work"] environment: <<: *laravel_env RUN_MIGRATIONS: "false" volumes: - geoip_data:/var/www/html/storage/app/geoip depends_on: postgres: condition: service_healthy laravel-fpm: condition: service_started # Only runs via `docker compose --profile test run --rm laravel-test` (or CI) — never # part of a normal `up`. Deliberately does NOT inherit *laravel_env: phpunit.xml's # tags only take effect for variables that aren't already set in the process # environment, so this container must start with DB_*/MAIL_*/etc. unset, letting # phpunit.xml point everything at the dedicated astrocast_test database instead of # the real one. laravel-test: profiles: ["test"] build: context: ./webapp target: test depends_on: postgres: condition: service_healthy volumes: postgres_data: geoip_data: gallery_data: