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.