services: db: image: postgres:18 environment: POSTGRES_DB: ${POSTGRES_DB:-hifd} POSTGRES_USER: ${POSTGRES_USER:-hifd} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-hifd} ports: - "5432:5432" volumes: - pgdata:/var/lib/postgresql - ./docker/postgres/init:/docker-entrypoint-initdb.d:ro healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-hifd} -d ${POSTGRES_DB:-hifd}"] interval: 5s timeout: 3s retries: 10 keycloak: image: quay.io/keycloak/keycloak:26.0 command: ["start-dev", "--import-realm", "--http-port=8180"] depends_on: db: condition: service_healthy environment: KC_BOOTSTRAP_ADMIN_USERNAME: ${KEYCLOAK_ADMIN:-admin} KC_BOOTSTRAP_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:-admin} # Persist Keycloak in its own database inside the same Postgres instance. KC_DB: postgres KC_DB_URL: jdbc:postgresql://db:5432/keycloak KC_DB_USERNAME: ${POSTGRES_USER:-hifd} KC_DB_PASSWORD: ${POSTGRES_PASSWORD:-hifd} # Tokens are issued with this exact hostname so the `iss` claim matches # what the backend validates against and what the browser uses to log in. KC_HOSTNAME: ${KEYCLOAK_PUBLIC_URL:-http://localhost:8180} KC_HEALTH_ENABLED: "true" volumes: - ./docker/keycloak/realm-export.json:/opt/keycloak/data/import/realm-export.json:ro ports: - "8180:8180" - "9000:9000" healthcheck: test: ["CMD-SHELL", "exec 3<>/dev/tcp/localhost/9000 && echo -e 'GET /health/ready HTTP/1.1\\r\\nHost: localhost\\r\\nConnection: close\\r\\n\\r\\n' >&3 && cat <&3 | grep -q '\"status\": \"UP\"'"] interval: 10s timeout: 5s retries: 20 app: build: context: ./backend image: hifd-backend:latest depends_on: db: condition: service_healthy keycloak: condition: service_healthy environment: DB_URL: jdbc:postgresql://db:5432/${POSTGRES_DB:-hifd} DB_USERNAME: ${POSTGRES_USER:-hifd} DB_PASSWORD: ${POSTGRES_PASSWORD:-hifd} # Canonical issuer must match the public Keycloak URL used by browsers, # because Keycloak writes this value into the token `iss` claim. KEYCLOAK_ISSUER_URI: ${KEYCLOAK_ISSUER_URI:-http://localhost:8180/realms/${KEYCLOAK_REALM:-elearning}} KEYCLOAK_ADMIN_SERVER_URL: ${KEYCLOAK_ADMIN_SERVER_URL:-http://keycloak:8180} KEYCLOAK_ADMIN_REALM: ${KEYCLOAK_REALM:-elearning} KEYCLOAK_ADMIN_CLIENT_ID: ${KEYCLOAK_ADMIN_CLIENT_ID:-hifd-backend-admin} KEYCLOAK_ADMIN_CLIENT_SECRET: ${KEYCLOAK_ADMIN_CLIENT_SECRET:-dev-backend-admin-secret} SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI: http://keycloak:8180/realms/${KEYCLOAK_REALM:-elearning}/protocol/openid-connect/certs APP_CORS_ALLOWED_ORIGINS: ${APP_CORS_ALLOWED_ORIGINS:-http://localhost:5173} PREDICTION_PROVIDER: ${PREDICTION_PROVIDER:-} PREDICTION_MODEL: ${PREDICTION_MODEL:-} PREDICTION_API_KEY: ${PREDICTION_API_KEY:-} PREDICTION_BASE_URL: ${PREDICTION_BASE_URL:-} PREDICTION_TIMEOUT: ${PREDICTION_TIMEOUT:-30s} PREDICTION_MAX_RETRIES: ${PREDICTION_MAX_RETRIES:-1} PREDICTION_TEMPERATURE: ${PREDICTION_TEMPERATURE:-0.2} ports: - "8080:8080" extra_hosts: - "localhost:host-gateway" # Frontend in dev mode (Vite dev server, hot reload). Default on `up`. frontend: build: context: ./frontend dockerfile: Dockerfile.dev image: hifd-frontend-dev:latest profiles: ["dev"] depends_on: - app ports: - "5173:5173" environment: VITE_API_BASE_URL: ${FRONTEND_API_BASE_URL:-http://localhost:8080} VITE_KEYCLOAK_URL: ${KEYCLOAK_PUBLIC_URL:-http://localhost:8180} VITE_KEYCLOAK_REALM: ${KEYCLOAK_REALM:-elearning} VITE_KEYCLOAK_CLIENT_ID: ${FRONTEND_KEYCLOAK_CLIENT_ID:-elearning-frontend} volumes: # Bind-mount source for hot reload; keep the container's node_modules. - ./frontend:/app - /app/node_modules # Frontend in prod mode (static build served by nginx). Opt in: --profile prod. frontend-prod: build: context: ./frontend dockerfile: Dockerfile args: VITE_API_BASE_URL: ${FRONTEND_API_BASE_URL:-http://localhost:8080} VITE_KEYCLOAK_URL: ${KEYCLOAK_PUBLIC_URL:-http://localhost:8180} VITE_KEYCLOAK_REALM: ${KEYCLOAK_REALM:-elearning} VITE_KEYCLOAK_CLIENT_ID: ${FRONTEND_KEYCLOAK_CLIENT_ID:-elearning-frontend} image: hifd-frontend:latest profiles: ["prod"] depends_on: - app ports: - "${FRONTEND_HTTP_PORT:-5173}:5173" seed-quran: image: hifd-backend:latest profiles: ["seed"] depends_on: db: condition: service_healthy environment: DB_URL: jdbc:postgresql://db:5432/${POSTGRES_DB:-hifd} DB_USERNAME: ${POSTGRES_USER:-hifd} DB_PASSWORD: ${POSTGRES_PASSWORD:-hifd} SPRING_PROFILES_ACTIVE: seed APP_COMMAND: seed-quran restart: "no" seed-reciters: image: hifd-backend:latest profiles: ["seed"] depends_on: seed-quran: condition: service_completed_successfully environment: DB_URL: jdbc:postgresql://db:5432/${POSTGRES_DB:-hifd} DB_USERNAME: ${POSTGRES_USER:-hifd} DB_PASSWORD: ${POSTGRES_PASSWORD:-hifd} SPRING_PROFILES_ACTIVE: seed APP_COMMAND: seed-reciters restart: "no" volumes: pgdata: