Zammad containers fail to start on Docker Swarm

Infos:

  • Used Zammad version: 5.6.1, 5.6.2 and 5.6.2-22
  • Used Zammad installation type: Docker-compose (swarm)
  • Operating system: Rocky Linux 9.6
  • containerd.io=1.7.27-3.1.el9
  • docker-buildx-plugin=0.27.0-1.el9
  • docker-ce=28.4.0-1.el9
  • docker-ce-cli=28.4.0-1.el9
  • docker-compose-plugin=2.39.2-1.el9
  • keepalived=2.2.8-4.el9_5
  • nfs-utils=2.5.4-34.el9
  • policycoreutils-python-utils=3.6-2.1.el9

Expected behavior:

  • All containers should start, and run normally

Actual behavior:

  • zammad-railsserver, zammad-init, zammad-websocket and zammad-scheduler are not running. They stop after 1 second of starting.

Steps to reproduce the behavior:

This is my zammad.yml

version: "3.8"

services:
  # -----------------------------------------------------------------------------
  # Zammad Rails application (main web UI)
  # - Runs the Zammad web app.
  # - Reads the database password from a shared Docker Secret (exported before start).
  # - Exposed via Traefik on redacted.com with TLS and middleware.
  # -----------------------------------------------------------------------------
  zammad-railsserver:
    image: zammad/zammad:6.5.2-22
    environment:
      REDIS_URL: redis://redis:6379                # Internal Redis endpoint
      ELASTICSEARCH_URL: http://elasticsearch:9200 # Internal Elasticsearch endpoint
      POSTGRESQL_DB: zammad                        # DB name used by Zammad
      POSTGRESQL_HOST: postgres                    # DB host inside the internal network
      POSTGRESQL_USER: zammad                      # DB user
      POSTGRESQL_PASS: ${ZAMMAD_DB_PASSWORD}       # DB password from external secret (injected at stack deploy time)  
      DATABASE_POOL_SIZE: 20                       # Rails DB connection pool size
      RAILS_ENV: production                        # Rails environment
      RAILS_TRUSTED_PROXIES: "['10.0.2.0/24']"     # Trust proxy CIDR(s) for correct IP handling
      ZAMMAD_WEBSOCKET_ALLOWED_ORIGINS: "https://redacted.com"  # CORS for websockets
      RAILS_SERVE_STATIC_FILES: "true"             # Let Rails serve static assets
    networks: [traefik-public, zammad-services-internal]    # Public ingress via Traefik + internal app net
    volumes:
      - zammad-data:/var/lib/zammad                # App data storage
      - zammad-log:/var/log/zammad                 # App logs
      - zammad-config:/etc/zammad                  # App configuration files
    deploy:
      mode: replicated
      replicas: 1
      labels:
        # Traefik router for main UI
        - traefik.enable=true
        - traefik.http.routers.zammad.rule=Host(`redacted.com`)
        - traefik.http.routers.zammad.entrypoints=websecure
        - traefik.http.routers.zammad.tls=true
        - traefik.http.routers.zammad.service=zammad-service
        - traefik.http.routers.zammad.middlewares=zammad-headers,zammad-compress
        # Traefik router for ActionCable (/cable) on the same backend port
        - traefik.http.routers.zammad-cable.rule=Host(`redacted.com`) && PathPrefix(`/cable`)
        - traefik.http.routers.zammad-cable.entrypoints=websecure
        - traefik.http.routers.zammad-cable.tls=true
        - traefik.http.routers.zammad-cable.service=zammad-service
        - traefik.http.routers.zammad-cable.middlewares=zammad-headers,zammad-compress
        # Backend service port exposed by the container (Rails)
        - traefik.http.services.zammad-service.loadbalancer.server.port=3000
        # Forwarded headers so Rails knows it’s behind TLS and on the right host
        - traefik.http.middlewares.zammad-headers.headers.customrequestheaders.X-Forwarded-Proto=https
        - traefik.http.middlewares.zammad-headers.headers.customrequestheaders.X-Forwarded-Ssl=on
        - traefik.http.middlewares.zammad-headers.headers.customrequestheaders.X-Forwarded-Port=443
        - traefik.http.middlewares.zammad-headers.headers.customrequestheaders.X-Forwarded-Host=redacted.com
        # Gzip compression middleware
        - traefik.http.middlewares.zammad-compress.compress=true
      placement:
        constraints:
          - node.role == manager
    ports:
      - target: 3000
        published: 3000
        protocol: tcp

  # -----------------------------------------------------------------------------
  # Zammad init (one-off initialization/migrations)
  # - Initializes the database (migrations, seeds) at first run or during upgrades.
  # - Uses the same DB connection parameters and shared password secret.
  # - Shares the same app volumes to persist configuration and logs.
  # -----------------------------------------------------------------------------
  zammad-init:
    image: zammad/zammad:6.5.2-22
    environment:
      REDIS_URL: redis://redis:6379
      ELASTICSEARCH_URL: http://elasticsearch:9200
      POSTGRESQL_DB: zammad
      POSTGRESQL_HOST: postgres
      POSTGRESQL_USER: zammad
      POSTGRESQL_PASS: ${ZAMMAD_DB_PASSWORD}
      DATABASE_POOL_SIZE: 20
      RAILS_ENV: production
    networks: [traefik-public, zammad-services-internal]
    volumes:
      - zammad-data:/var/lib/zammad
      - zammad-log:/var/log/zammad
      - zammad-config:/etc/zammad
    deploy:
      placement:
        constraints:
          - node.role == manager
     
  # -----------------------------------------------------------------------------
  # Zammad WebSocket server (ActionCable / live updates)
  # - Handles real-time features like notifications and live updates.
  # - Exposed via a dedicated Traefik router on /ws to port 6042.
  # - Uses the same shared database password secret.
  # -----------------------------------------------------------------------------
  zammad-websocket:
    image: zammad/zammad:6.5.2-22
    environment:
      REDIS_URL: redis://redis:6379
      ELASTICSEARCH_URL: http://elasticsearch:9200
      POSTGRESQL_DB: zammad
      POSTGRESQL_HOST: postgres
      POSTGRESQL_USER: zammad
      POSTGRESQL_PASS: ${ZAMMAD_DB_PASSWORD}
      DATABASE_POOL_SIZE: 20
      RAILS_ENV: production
      ZAMMAD_WEBSOCKET_ALLOWED_ORIGINS: "https://redacted.com"
    networks: [traefik-public, zammad-services-internal]
    volumes:
      - zammad-data:/var/lib/zammad
      - zammad-log:/var/log/zammad
      - zammad-config:/etc/zammad
    deploy:
      placement:
        constraints:
          - node.role == manager
      labels:
        - traefik.enable=true
        # Dedicated WebSocket router and backend port
        - traefik.http.routers.zammad-websocket.rule=Host(`redacted.com`) && PathPrefix(`/ws`)
        - traefik.http.routers.zammad-websocket.entrypoints=websecure
        - traefik.http.routers.zammad-websocket.tls=true
        - traefik.http.routers.zammad-websocket.service=zammad-websocket-service
        - traefik.http.services.zammad-websocket-service.loadbalancer.server.port=6042

  # -----------------------------------------------------------------------------
  # Zammad scheduler (background jobs)
  # - Runs asynchronous/background tasks (e.g., email fetching, indexing, etc.).
  # - Not exposed externally; communicates with DB, Redis, and Elasticsearch.
  # - Uses the same shared database password secret.
  # -----------------------------------------------------------------------------
  zammad-scheduler:
    image: zammad/zammad:6.5.2-22
    environment:
      REDIS_URL: redis://redis:6379
      ELASTICSEARCH_URL: http://elasticsearch:9200
      POSTGRESQL_DB: zammad
      POSTGRESQL_HOST: postgres
      POSTGRESQL_USER: zammad
      POSTGRESQL_PASS: ${ZAMMAD_DB_PASSWORD}
      DATABASE_POOL_SIZE: 20
      RAILS_ENV: production
    networks: [zammad-services-internal]
    volumes:
      - zammad-data:/var/lib/zammad
      - zammad-log:/var/log/zammad
      - zammad-config:/etc/zammad
    deploy:
      placement:
        constraints:
          - node.role == manager

  # -----------------------------------------------------------------------------
  # Redis (in-memory cache / queues)
  # - Used by Zammad for caching and job queues.
  # - Internal-only (no public exposure). Persistence via volume "zammad-redis-data".
  # -----------------------------------------------------------------------------
  redis:
    image: redis:6
    networks: [zammad-services-internal]
    volumes:
      - zammad-redis-data:/data

  # -----------------------------------------------------------------------------
  # PostgreSQL (primary database)
  # - Official image handles initialization. Password read via the shared Docker Secret.
  # - Data persisted on "zammad-postgres-data".
  # -----------------------------------------------------------------------------
  postgres:
    image: postgres:13
    environment:
      POSTGRES_USER: zammad
      POSTGRES_PASSWORD: ${ZAMMAD_DB_PASSWORD}
      POSTGRES_DB: zammad
      PGDATA: /var/lib/postgresql/data
    networks: [zammad-services-internal]
    volumes:
      - zammad-postgres-data:/var/lib/postgresql/data
    deploy:
      placement:
        constraints:
          - node.role == manager

  # -----------------------------------------------------------------------------
  # Elasticsearch (full-text search backend)
  # - Single-node mode for this deployment.
  # - Data persisted on "zammad-elasticsearch-data".
  # -----------------------------------------------------------------------------
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:7.17.6
    environment:
      - discovery.type=single-node       # No cluster discovery, single-node mode
      - bootstrap.memory_lock=true       # Prevent swapping to improve performance
      - ES_JAVA_OPTS=-Xms512m -Xmx512m   # JVM heap sizing (adjust as needed)
      - node.store.allow_mmap=false    # Disable mmapfs to avoid permission issues in some environments
    ulimits:
      memlock: {soft: -1, hard: -1}     # Allow memory locking for the ES process
    networks:
      zammad-services-internal:
        aliases: [zammad-elasticsearch]  # Optional alias inside the internal network
    volumes:
      - zammad-elasticsearch-data:/usr/share/elasticsearch/data
    deploy:
      placement:
        constraints:
          - node.role == manager

# -----------------------------------------------------------------------------
# Networks
# - traefik-public: external ingress network that Traefik listens on.
# - zammad-services-internal: internal overlay network for service-to-service communication.
# -----------------------------------------------------------------------------
networks:
  traefik-public:  { external: true }
  zammad-services-internal:
    driver: overlay

# -----------------------------------------------------------------------------
# Volumes
# - External so they are managed outside of this stack (e.g., NFS-backed).
# - Provide persistence for DB, ES, Redis, and Zammad assets/logs/config.
# -----------------------------------------------------------------------------
volumes:
  zammad-postgres-data:      { external: true }
  zammad-elasticsearch-data: { external: true }
  zammad-redis-data:         { external: true }
  zammad-data:               { external: true }
  zammad-log:                { external: true }
  zammad-config:             { external: true }

And this is the pipeline:

stages:
  - prepare
  - deploy

variables:
  APP_NAME: "zammad"
  STACK_NAME: "zammad_services"

create_network_zammad_internal:
  stage: prepare
  tags:
    - docker-manager-1
  environment:
    name: dev
  script:
    - echo "$GITLAB_PASSWORD_M_1" | sudo -S docker network inspect zammad-services-internal >/dev/null 2>&1 || echo "$GITLAB_PASSWORD_M_1" | sudo -S docker network create --driver overlay --attachable --scope swarm zammad-services-internal
  only:
    - dev

prepare_zammad_volumes:
  stage: prepare
  tags:
    - docker-manager-1
  environment:
    name: dev
  script:
    - echo "$GITLAB_PASSWORD_M_1" | sudo -S mkdir -p /mnt/data-docker-1/zammad-services/postgres
    - echo "$GITLAB_PASSWORD_M_1" | sudo -S mkdir -p /mnt/data-docker-1/zammad-services/elasticsearch
    - echo "$GITLAB_PASSWORD_M_1" | sudo -S mkdir -p /mnt/data-docker-1/zammad-services/redis
    - echo "$GITLAB_PASSWORD_M_1" | sudo -S mkdir -p /mnt/data-docker-1/zammad-services/data
    - echo "$GITLAB_PASSWORD_M_1" | sudo -S mkdir -p /mnt/data-docker-1/zammad-services/log
    - echo "$GITLAB_PASSWORD_M_1" | sudo -S mkdir -p /mnt/data-docker-1/zammad-services/config
    - echo "$GITLAB_PASSWORD_M_1" | sudo -S ls -la /mnt/data-docker-1/zammad-services
  only:
    - dev

create_zammad_volumes_manager_1:
  stage: prepare
  tags:
    - docker-manager-1
  needs:
    - prepare_zammad_volumes
  environment:
    name: dev
  script:
    - echo "$GITLAB_PASSWORD_M_1" | sudo -S docker volume create --driver local --opt type=nfs --opt o=addr=$NFS_IP,rw,nfsvers=4 --opt device=:/volume1/data-docker-1/zammad-services/postgres zammad-postgres-data || true
    - echo "$GITLAB_PASSWORD_M_1" | sudo -S docker volume create --driver local --opt type=nfs --opt o=addr=$NFS_IP,rw,nfsvers=4 --opt device=:/volume1/data-docker-1/zammad-services/elasticsearch zammad-elasticsearch-data || true
    - echo "$GITLAB_PASSWORD_M_1" | sudo -S docker volume create --driver local --opt type=nfs --opt o=addr=$NFS_IP,rw,nfsvers=4 --opt device=:/volume1/data-docker-1/zammad-services/redis zammad-redis-data || true
    - echo "$GITLAB_PASSWORD_M_1" | sudo -S docker volume create --driver local --opt type=nfs --opt o=addr=$NFS_IP,rw,nfsvers=4 --opt device=:/volume1/data-docker-1/zammad-services/data zammad-data || true
    - echo "$GITLAB_PASSWORD_M_1" | sudo -S docker volume create --driver local --opt type=nfs --opt o=addr=$NFS_IP,rw,nfsvers=4 --opt device=:/volume1/data-docker-1/zammad-services/log zammad-log || true
    - echo "$GITLAB_PASSWORD_M_1" | sudo -S docker volume create --driver local --opt type=nfs --opt o=addr=$NFS_IP,rw,nfsvers=4 --opt device=:/volume1/data-docker-1/zammad-services/config zammad-config || true
  only:
    - dev

create_zammad_volumes_manager_2:
  stage: prepare
  tags:
    - dev-docker-manager-2
  needs:
    - prepare_zammad_volumes
  environment:
    name: dev
  script:
    - echo "$GITLABRUNNER_PASSWORD_MANAGER_2" | sudo -S docker volume create --driver local --opt type=nfs --opt o=addr=$NFS_IP,rw,nfsvers=4 --opt device=:/volume1/data-docker-1/zammad-services/postgres zammad-postgres-data || true
    - echo "$GITLABRUNNER_PASSWORD_MANAGER_2" | sudo -S docker volume create --driver local --opt type=nfs --opt o=addr=$NFS_IP,rw,nfsvers=4 --opt device=:/volume1/data-docker-1/zammad-services/elasticsearch zammad-elasticsearch-data || true
    - echo "$GITLABRUNNER_PASSWORD_MANAGER_2" | sudo -S docker volume create --driver local --opt type=nfs --opt o=addr=$NFS_IP,rw,nfsvers=4 --opt device=:/volume1/data-docker-1/zammad-services/redis zammad-redis-data || true
    - echo "$GITLABRUNNER_PASSWORD_MANAGER_2" | sudo -S docker volume create --driver local --opt type=nfs --opt o=addr=$NFS_IP,rw,nfsvers=4 --opt device=:/volume1/data-docker-1/zammad-services/data zammad-data || true
    - echo "$GITLABRUNNER_PASSWORD_MANAGER_2" | sudo -S docker volume create --driver local --opt type=nfs --opt o=addr=$NFS_IP,rw,nfsvers=4 --opt device=:/volume1/data-docker-1/zammad-services/log zammad-log || true
    - echo "$GITLABRUNNER_PASSWORD_MANAGER_2" | sudo -S docker volume create --driver local --opt type=nfs --opt o=addr=$NFS_IP,rw,nfsvers=4 --opt device=:/volume1/data-docker-1/zammad-services/config zammad-config || true
  only:
    - dev

create_zammad_volumes_manager_3:
  stage: prepare
  tags:
    - dev-docker-manager-3
  needs:
    - prepare_zammad_volumes
  environment:
    name: dev
  script:
    - echo "$GITLABRUNNER_PASSWORD_MANAGER_3" | sudo -S docker volume create --driver local --opt type=nfs --opt o=addr=$NFS_IP,rw,nfsvers=4 --opt device=:/volume1/data-docker-1/zammad-services/postgres zammad-postgres-data || true
    - echo "$GITLABRUNNER_PASSWORD_MANAGER_3" | sudo -S docker volume create --driver local --opt type=nfs --opt o=addr=$NFS_IP,rw,nfsvers=4 --opt device=:/volume1/data-docker-1/zammad-services/elasticsearch zammad-elasticsearch-data || true
    - echo "$GITLABRUNNER_PASSWORD_MANAGER_3" | sudo -S docker volume create --driver local --opt type=nfs --opt o=addr=$NFS_IP,rw,nfsvers=4 --opt device=:/volume1/data-docker-1/zammad-services/redis zammad-redis-data || true
    - echo "$GITLABRUNNER_PASSWORD_MANAGER_3" | sudo -S docker volume create --driver local --opt type=nfs --opt o=addr=$NFS_IP,rw,nfsvers=4 --opt device=:/volume1/data-docker-1/zammad-services/data zammad-data || true
    - echo "$GITLABRUNNER_PASSWORD_MANAGER_3" | sudo -S docker volume create --driver local --opt type=nfs --opt o=addr=$NFS_IP,rw,nfsvers=4 --opt device=:/volume1/data-docker-1/zammad-services/log zammad-log || true
    - echo "$GITLABRUNNER_PASSWORD_MANAGER_3" | sudo -S docker volume create --driver local --opt type=nfs --opt o=addr=$NFS_IP,rw,nfsvers=4 --opt device=:/volume1/data-docker-1/zammad-services/config zammad-config || true
  only:
    - dev

deploy_zammad_services:
  stage: deploy
  tags:
    - docker-manager-1
  needs:
    - prepare_zammad_volumes
    - create_zammad_volumes_manager_1
    - create_zammad_volumes_manager_2
    - create_zammad_volumes_manager_3
    - create_network_zammad_internal
  environment:
    name: dev
  script:
    - docker stack deploy -c stacks/zammad.yml ${STACK_NAME}
  only:
    - dev

Output of logs:
docker service logs -f zammad_services_elasticsearch

docker-manager-2    | {"type": "server", "timestamp": "2025-10-29T10:05:25,960Z", "level": "INFO", "component": "o.e.i.g.DatabaseNodeService", "cluster.name": "docker-cluster", "node.name": "52da6114a126", "message": "successfully reloaded changed geoip database file [/tmp/elasticsearch-9314814840401091095/geoip-databases/JJQ5eXwgSRms-lPT80pOpg/GeoLite2-City.mmdb]", "cluster.uuid": "hdjxxXgzRCChNIWQQuBuMg", "node.id": "JJQ5eXwgSRms-lPT80pOpg"  }

docker service logs -f zammad_services_postgres

zammad_services_postgres.1.763b9ibavk3e@dev-docker-manager-3    |
zammad_services_postgres.1.763b9ibavk3e@dev-docker-manager-3    | PostgreSQL Database directory appears to contain a database; Skipping initialization
zammad_services_postgres.1.763b9ibavk3e@dev-docker-manager-3    |
zammad_services_postgres.1.763b9ibavk3e@dev-docker-manager-3    | 2025-10-29 10:04:24.684 UTC [1] LOG:  starting PostgreSQL 13.22 (Debian 13.22-1.pgdg13+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit
zammad_services_postgres.1.763b9ibavk3e@dev-docker-manager-3    | 2025-10-29 10:04:24.686 UTC [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432
zammad_services_postgres.1.763b9ibavk3e@dev-docker-manager-3    | 2025-10-29 10:04:24.687 UTC [1] LOG:  listening on IPv6 address "::", port 5432
zammad_services_postgres.1.763b9ibavk3e@dev-docker-manager-3    | 2025-10-29 10:04:24.691 UTC [1] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
zammad_services_postgres.1.763b9ibavk3e@dev-docker-manager-3    | 2025-10-29 10:04:24.712 UTC [26] LOG:  database system was shut down at 2025-10-29 10:03:00 UTC
zammad_services_postgres.1.763b9ibavk3e@dev-docker-manager-3    | 2025-10-29 10:04:24.770 UTC [1] LOG:  database system is ready to accept connections

docker service logs -f zammad_services_redis

zammad_services_redis.1.s28g20nsxs7j@dev-docker-manager-2    | 1:M 29 Oct 2025 10:04:22.062 * Loading RDB produced by version 6.2.20
zammad_services_redis.1.s28g20nsxs7j@dev-docker-manager-2    | 1:M 29 Oct 2025 10:04:22.062 * RDB age 83 seconds
zammad_services_redis.1.s28g20nsxs7j@dev-docker-manager-2    | 1:M 29 Oct 2025 10:04:22.062 * RDB memory usage when created 0.77 Mb
zammad_services_redis.1.s28g20nsxs7j@dev-docker-manager-2    | 1:M 29 Oct 2025 10:04:22.062 # Done loading RDB, keys loaded: 0, keys expired: 0.
zammad_services_redis.1.s28g20nsxs7j@dev-docker-manager-2    | 1:M 29 Oct 2025 10:04:22.063 * DB loaded from disk: 0.003 seconds
zammad_services_redis.1.s28g20nsxs7j@dev-docker-manager-2    | 1:M 29 Oct 2025 10:04:22.063 * Ready to accept connections

docker service logs -f zammad_services_zammad-init
None

docker service logs -f zammad_services_zammad-railsserver
None

docker service logs -f zammad_services_zammad-scheduler
None

docker service logs -f zammad_services_zammad-websocket
None

docker service ps zammad_services_elasticsearchs --no-trunc
Running

docker service ps zammad_services_postgres --no-trunc
Running

docker service ps zammad_services_redis --no-trunc
Running

docker service ps zammad_services_zammad-init --no-trunc
Restarting

docker service ps zammad_services_zammad-railsserver --no-trunc
Restarting

docker service ps zammad_services_zammad-scheduler --no-trunc
Ready

docker service ps zammad_services_zammad-websocket --no-trunc
Restarting

I don’t claim to be the best at this, but I’m doing my best. I’m also trying to keep the pipeline as system-engineer-oriented as possible, so that my colleagues can easily take it over. For the company, this is the first step towards working with GitLab and implementing a pipeline.

Additionally, the Swarm itself is functioning properly and is already running other containers without issues. However, for some reason, I haven’t been able to get Zammad working in Swarm. It did work about a month ago, but since this week I’ve been running into problems.

Please note that all variable names, devices, and data storage references in this text have been anonymised.