name: calendars services: # Shared PostgreSQL for all services (calendar, caldav server, keycloak) postgresql: image: postgres:15 ports: - "8936:5432" healthcheck: test: [ "CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB" ] interval: 1s timeout: 2s retries: 300 volumes: - ./docker/postgresql/init-databases.sh:/docker-entrypoint-initdb.d/init-databases.sh:ro env_file: - env.d/development/postgresql.defaults - env.d/development/postgresql.local redis: image: redis:5 ports: - "8934:6379" mailcatcher: image: sj26/mailcatcher:latest ports: - "8937:1080" backend-dev: build: context: src/backend target: backend-development args: DOCKER_USER: ${DOCKER_USER:-1000} user: ${DOCKER_USER:-1000} image: calendars:backend-development environment: - PYLINTHOME=/app/.pylint.d - DJANGO_CONFIGURATION=Development env_file: - env.d/development/backend.defaults - env.d/development/backend.local ports: - "8931:8000" volumes: - ./src/backend:/app - ./data/static:/data/static # Mount the whole i18n DIRECTORY, not just translations.json. A # single-file bind mount pins the container to one inode, so when a # tool (oxfmt, the i18n tooling, an editor) rewrites the file via the # standard write-temp-then-rename, the container keeps reading the old # detached inode — often a truncated mid-write copy — and every email # send 500s on json.load until the container is restarted. A directory # mount re-resolves the child path on each open, so renames are seen. - ./src/frontend/src/features/i18n:/data/i18n:ro - /app/.venv extra_hosts: - "host.docker.internal:host-gateway" networks: - lasuite - default depends_on: postgresql: condition: service_healthy restart: true mailcatcher: condition: service_started redis: condition: service_started worker-dev: condition: service_started caldav: condition: service_started # Hermetic test container — loads ONLY backend.defaults, never # backend.local, so tests don't inherit per-developer overrides like # OIDC_USERINFO_ORGANIZATION_CLAIM, FEATURE_MESSAGES_INTEGRATION, etc. # Used by bin/pytest. Mirrors backend-dev's image and dependencies so # tests that hit Postgres / CalDAV still work. backend-test: user: ${DOCKER_USER:-1000} image: calendars:backend-development environment: - PYLINTHOME=/app/.pylint.d - DJANGO_CONFIGURATION=Test # Point at the dedicated test SabreDAV instance (separate DB), so tests # never touch the developer's local CalDAV database. - CALDAV_URL=http://caldav-test:80 env_file: - env.d/development/backend.defaults volumes: - ./src/backend:/app - ./data/static:/data/static # Directory mount (see backend-dev) so atomic-rename rewrites of # translations.json are picked up without a container restart. - ./src/frontend/src/features/i18n:/data/i18n:ro - /app/.venv extra_hosts: - "host.docker.internal:host-gateway" networks: - lasuite - default depends_on: postgresql: condition: service_healthy restart: true redis: condition: service_started caldav-test: condition: service_healthy worker-dev: user: ${DOCKER_USER:-1000} image: calendars:backend-development networks: - default - lasuite command: [ "python", "worker.py", "-v", "2" ] environment: - DJANGO_CONFIGURATION=Development env_file: - env.d/development/backend.defaults - env.d/development/backend.local volumes: - ./src/backend:/app - ./data/static:/data/static # Directory mount (see backend-dev) so atomic-rename rewrites of # translations.json are picked up without a container restart. - ./src/frontend/src/features/i18n:/data/i18n:ro - /app/.venv frontend-dev: # Keep the host UID (file ownership on the bind-mounted sources) but force # the GID to 1000, the `node` group that owns the image's group-writable # node_modules. On macOS the host GID is 20 (staff), not 1000, so the # default ${DOCKER_USER} would leave Vite unable to write node_modules/.vite user: "${DOCKER_UID:-1000}:1000" build: context: src/frontend target: calendars-dev args: API_ORIGIN: "http://localhost:8931" image: calendars:frontend-development env_file: - env.d/development/frontend.defaults - env.d/development/frontend.local volumes: - ./src/frontend/:/home/frontend/ - /home/frontend/node_modules ports: - "8930:8930" node: image: node:24 user: "${DOCKER_USER:-1000}" environment: HOME: /tmp volumes: - ".:/app" # CalDAV Server caldav: build: context: src/caldav ports: - "8932:80" env_file: - env.d/development/caldav.defaults - env.d/development/caldav.local environment: # Allow shell override for dev (tests use the dedicated caldav-test service) CALDAV_CALLBACK_BASE_URL: ${CALDAV_CALLBACK_BASE_URL:-http://backend-dev:8000} volumes: - ./src/caldav/server.php:/var/www/sabredav/server.php - ./src/caldav/src:/var/www/sabredav/src - ./src/caldav/sql:/var/www/sabredav/sql - ./src/caldav/init-database.sh:/usr/local/bin/init-database.sh networks: - default - lasuite depends_on: postgresql: condition: service_healthy command: > sh -c " /usr/local/bin/init-database.sh || true && apache2-foreground " # Dedicated CalDAV server for tests. Points at a separate Postgres database # (caldav_test) so tests never read/write the developer's local CalDAV data. # Schema is self-provisioned by init-database.sh on first boot. caldav-test: build: context: src/caldav ports: - "8933:80" env_file: - env.d/development/caldav-test.defaults volumes: - ./src/caldav/server.php:/var/www/sabredav/server.php - ./src/caldav/src:/var/www/sabredav/src - ./src/caldav/sql:/var/www/sabredav/sql - ./src/caldav/init-database.sh:/usr/local/bin/init-database.sh networks: - default - lasuite depends_on: postgresql: condition: service_healthy healthcheck: # Probe the dedicated /caldav/healthz endpoint in server.php that # short-circuits before the DAV/auth chain. Probing the real /caldav/ # would trigger SabreDAV's NotAuthenticated on every interval and # the exception handler would log a full PHP stack trace each time. # -f makes curl exit non-zero on >=400 so a broken deployment is # actually surfaced as unhealthy. test: - "CMD-SHELL" - "curl -sf -o /dev/null http://localhost/caldav/healthz || exit 1" interval: 2s timeout: 3s retries: 30 command: > sh -c " /usr/local/bin/init-database.sh || true && apache2-foreground " # Keycloak - now using shared PostgreSQL keycloak: image: quay.io/keycloak/keycloak:26.3.2 volumes: - ./docker/auth/realm.json:/opt/keycloak/data/import/realm.json command: - start-dev - --features=preview - --import-realm - --hostname=http://localhost:8935 - --hostname-strict=false env_file: - env.d/development/keycloak.defaults - env.d/development/keycloak.local ports: - "8935:8080" depends_on: postgresql: condition: service_healthy networks: - lasuite - default networks: default: {} lasuite: name: lasuite-network driver: bridge external: true