Skip to content

Calendars Compose Configuration

Calendars requires three services: backend, worker, and frontend. It also needs a CalDAV server (SabreDAV/PHP) for calendar storage.

All services join the messages_internal network to share PostgreSQL, Redis, and reach Keycloak.

compose.yml

name: calendars

services:
  backend:
    image: ghcr.io/suitenumerique/calendars-backend:main
    restart: unless-stopped
    volumes:
      - ./translations.json:/data/translations.json:ro
    env_file: .env
    command: ["gunicorn", "calendars.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "2", "--timeout", "90"]
    extra_hosts:
      - "auth.messages.<domain>:host-gateway"
    healthcheck:
      test: ["CMD", "pgrep", "gunicorn"]
      interval: 10s
      timeout: 5s
      retries: 20
      start_period: 20s
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.calendars-backend.rule=Host(`api.calendars.<domain>`)"
      - "traefik.http.routers.calendars-backend.entrypoints=websecure"
      - "traefik.http.routers.calendars-backend.tls=true"
      - "traefik.http.routers.calendars-backend.tls.certresolver=letsencrypt"
      - "traefik.http.services.calendars-backend.loadbalancer.server.port=8000"
    networks:
      proxy:
      messages_internal:
      messages_static: {ipv4_address: 172.29.0.30}

  worker:
    image: ghcr.io/suitenumerique/calendars-backend:main
    restart: unless-stopped
    command: ["python", "worker.py", "-v", "2"]
    env_file: .env
    extra_hosts:
      - "auth.messages.<domain>:host-gateway"
    depends_on: {backend: {condition: service_healthy}}
    networks: [messages_internal]

  caldav:
    image: calendars-caldav:latest
    restart: unless-stopped
    command: >
      sh -c "
        /usr/local/bin/init-database.sh || true &&
        apache2-foreground
      "
    environment:
      PGHOST: postgresql
      PGPORT: 5432
      PGDATABASE: caldav
      PGUSER: messages
      PGPASSWORD: <db-password>
      CALDAV_DB_SCHEMA: sabre
      CALDAV_BASE_URI: /caldav/
      CALDAV_INBOUND_API_KEY: <caldav-api-key>
      CALDAV_OUTBOUND_API_KEY: <caldav-api-key>
      CALDAV_INTERNAL_API_KEY: <caldav-api-key>
      CALDAV_CALLBACK_BASE_URL: http://172.29.0.30:8000
    networks:
      messages_internal:
      messages_static: {ipv4_address: 172.29.0.35}

  frontend:
    image: ghcr.io/suitenumerique/calendars-frontend:main
    restart: unless-stopped
    command: ["caddy", "run", "--config", "/etc/caddy/Caddyfile.prod", "--adapter", "caddyfile"]
    env_file: .env
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile.prod:ro
    depends_on: {backend: {condition: service_healthy}}
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.calendars-frontend.rule=Host(`calendars.<domain>`)"
      - "traefik.http.routers.calendars-frontend.entrypoints=websecure"
      - "traefik.http.routers.calendars-frontend.tls=true"
      - "traefik.http.routers.calendars-frontend.tls.certresolver=letsencrypt"
      - "traefik.http.services.calendars-frontend.loadbalancer.server.port=8080"
    networks:
      proxy:
      messages_internal:
      messages_static: {ipv4_address: 172.29.0.31}

networks:
  proxy: {external: true}
  messages_internal: {external: true}
  messages_static: {external: true}

Key Points

  1. Backend healthcheck uses pgrep gunicorn — the Calendars backend doesn't have a /__heartbeat__/ endpoint
  2. CalDAV callback URL uses static IP 172.29.0.30:8000 — avoids DNS conflicts with backend hostname
  3. Custom Caddyfile mounted at /etc/caddy/Caddyfile.prod — adds API, CalDAV, and RSVP reverse proxy rules
  4. translations.json mounted from the host — required for email invitation generation
  5. extra_hosts resolves Keycloak domain to the Docker host gateway