Skip to content

Reference: Messages compose.yml

name: messages

services:
  postgresql:
    image: postgres:16
    restart: unless-stopped
    environment:
      POSTGRES_DB: messages
      POSTGRES_USER: messages
      POSTGRES_PASSWORD: <db-password>
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U messages -d messages"]
      interval: 1s
      timeout: 2s
      retries: 300
    volumes: [messages_db:/var/lib/postgresql/data]
    networks: [internal]

  redis:
    image: redis:7
    restart: unless-stopped
    networks: [internal]

  opensearch:
    image: opensearchproject/opensearch:2.19.2
    restart: unless-stopped
    environment:
      - discovery.type=single-node
      - bootstrap.memory_lock=true
      - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m"
      - "DISABLE_INSTALL_DEMO_CONFIG=true"
      - "DISABLE_SECURITY_PLUGIN=true"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9200"]
      interval: 5s
      timeout: 5s
      retries: 60
      start_period: 30s
    ulimits:
      memlock: {soft: -1, hard: -1}
      nofile: {soft: 65536, hard: 65536}
    volumes: [messages_opensearch:/usr/share/opensearch/data]
    networks: [internal]

  minio:
    image: minio/minio:latest
    restart: unless-stopped
    command: server /data --console-address ":9001"
    environment:
      MINIO_ROOT_USER: messages
      MINIO_ROOT_PASSWORD: <minio-password>
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
      interval: 5s
      timeout: 5s
      retries: 20
      start_period: 10s
    volumes: [messages_minio:/data]
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.messages-minio.rule=Host(`s3.messages.<domain>`)"
      - "traefik.http.routers.messages-minio.entrypoints=websecure"
      - "traefik.http.routers.messages-minio.tls=true"
      - "traefik.http.routers.messages-minio.tls.certresolver=letsencrypt"
      - "traefik.http.services.messages-minio.loadbalancer.server.port=9000"
    networks: [proxy, internal]

  minio-init:
    image: minio/mc:latest
    restart: "no"
    depends_on: {minio: {condition: service_healthy}}
    entrypoint: ["/bin/sh", "-c"]
    command:
      - |
        mc alias set myminio http://minio:9000 messages <minio-password>
        mc mb myminio/messages-media --ignore-existing
        mc mb myminio/msg-imports --ignore-existing
        mc mb myminio/msg-blobs --ignore-existing
    networks: [internal]

  kc-postgresql:
    image: postgres:16
    restart: unless-stopped
    environment:
      POSTGRES_DB: keycloak
      POSTGRES_USER: keycloak
      POSTGRES_PASSWORD: <kc-db-password>
    volumes: [kc_db:/var/lib/postgresql/data]
    networks: [internal]

  keycloak:
    image: quay.io/keycloak/keycloak:latest
    restart: unless-stopped
    command: [start, --import-realm]
    environment:
      KC_BOOTSTRAP_ADMIN_USERNAME: admin
      KC_BOOTSTRAP_ADMIN_PASSWORD: <kc-admin-password>
      KC_DB: postgres
      KC_DB_URL_HOST: kc-postgresql
      KC_DB_URL_DATABASE: keycloak
      KC_DB_PASSWORD: <kc-db-password>
      KC_DB_USERNAME: keycloak
      KC_HOSTNAME: https://auth.messages.<domain>
      KC_PROXY_HEADERS: xforwarded
      KC_HTTP_ENABLED: "true"
    volumes:
      - ./keycloak-realm.json:/opt/keycloak/data/import/realm.json:ro
      - ../calendars/keycloak-realm.json:/opt/keycloak/data/import/calendars-realm.json:ro
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.messages-keycloak.rule=Host(`auth.messages.<domain>`)"
      - "traefik.http.routers.messages-keycloak.entrypoints=websecure"
      - "traefik.http.routers.messages-keycloak.tls=true"
      - "traefik.http.routers.messages-keycloak.tls.certresolver=letsencrypt"
      - "traefik.http.services.messages-keycloak.loadbalancer.server.port=8080"
    depends_on: [kc-postgresql]
    networks: [proxy, internal]

  backend:
    image: ghcr.io/suitenumerique/messages-backend:main
    restart: unless-stopped
    command: ["gunicorn", "messages.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "2", "--timeout", "90"]
    env_file: .env
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request as u; u.urlopen('http://localhost:8000/__heartbeat__/', timeout=2)"]
      interval: 10s
      timeout: 5s
      retries: 20
      start_period: 30s
    depends_on:
      postgresql: {condition: service_healthy}
      redis: {condition: service_started}
      opensearch: {condition: service_healthy}
      minio: {condition: service_healthy}
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.messages-backend.rule=Host(`api.messages.<domain>`)"
      - "traefik.http.routers.messages-backend.entrypoints=websecure"
      - "traefik.http.routers.messages-backend.tls=true"
      - "traefik.http.routers.messages-backend.tls.certresolver=letsencrypt"
      - "traefik.http.services.messages-backend.loadbalancer.server.port=8000"
    networks:
      proxy:
      internal:
      messages_static: {ipv4_address: 172.29.0.10}

  worker:
    image: ghcr.io/suitenumerique/messages-backend:main
    restart: unless-stopped
    command: ["python", "worker.py"]
    env_file: .env
    depends_on: {backend: {condition: service_healthy}}
    networks: [internal]

  frontend:
    image: ghcr.io/suitenumerique/messages-frontend:main
    restart: unless-stopped
    env_file: .env
    depends_on: {backend: {condition: service_healthy}}
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.messages-frontend.rule=Host(`messages.<domain>`)"
      - "traefik.http.routers.messages-frontend.entrypoints=websecure"
      - "traefik.http.routers.messages-frontend.tls=true"
      - "traefik.http.routers.messages-frontend.tls.certresolver=letsencrypt"
      - "traefik.http.services.messages-frontend.loadbalancer.server.port=8080"
    networks:
      proxy:
      internal:
      messages_static: {ipv4_address: 172.29.0.20}

  mta-in:
    image: ghcr.io/suitenumerique/messages-mta-in:main
    restart: unless-stopped
    user: root
    environment:
      MDA_API_BASE_URL: http://backend:8000/api/v1.0/
      MDA_API_SECRET: <mda-secret>
      MDA_API_TIMEOUT: 5
      MAX_INCOMING_EMAIL_SIZE: 30000000
      MYHOSTNAME: mx.<domain>
    ports: ["25:25"]
    depends_on: {backend: {condition: service_healthy}}
    networks: [internal]

  mta-out:
    image: ghcr.io/suitenumerique/messages-mta-out:main
    restart: unless-stopped
    user: root
    entrypoint: ["/bin/sh", "-c", "mkdir -p /var/spool/postfix/etc && chown -R postfix:postfix /var/spool/postfix /var/spool/postfix/public /var/spool/postfix/private 2>/dev/null; chown postfix:postdrop /var/spool/postfix/public 2>/dev/null; exec /usr/local/bin/entrypoint.sh"]
    environment:
      MYHOSTNAME: mx.<domain>
      SMTP_USERNAME: messages
      SMTP_PASSWORD: <mta-password>
      SMTP_RELAY_HOST: smtp-relay.brevo.com:587
      SMTP_RELAY_USERNAME: <brevo-username>
      SMTP_RELAY_PASSWORD: <brevo-password>
    depends_on: {backend: {condition: service_healthy}}
    networks: [internal]

volumes:
  messages_db:
  messages_minio:
  messages_opensearch:
  kc_db:

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