Code Explanation
Anatomy of a Quadlet unit
Every .container file in this repo follows the same shape:
[Unit]
After=zammad.pod ... # start order
Requires=zammad.pod ... # hard dependency
PartOf=zammad.pod # stop/restart together with the pod
[Container]
Image=...
ContainerName=...
Pod=zammad.pod # joins the shared pod network namespace
Exec=... # command run inside the Zammad image for this role
EnvironmentFile=.env
[Service]
Restart=always
RestartSec=...
[Install]
WantedBy=default.target # lets systemctl enable start it at boot/loginPartOf=zammad.pod means stopping or restarting the pod stops/restarts the container with it.
Pod=zammad.pod puts every container in the same network namespace, so they can reach each other over
localhost instead of needing container-to-container DNS.
The shared infrastructure
zammad.network— a dedicated bridge network namedzammadthat everything else attaches to via the pod.zammad.pod— the pod that groups every container into one network namespace and publishes port8080:8080to the host.Requires=zammad.networkties its lifecycle to the network.
The Zammad services
All of the ghcr.io/zammad/zammad containers run the same image, just with a different Exec= command —
Zammad’s image is a multi-role image where the entrypoint script decides what process to run.
| Unit | Role |
|---|---|
zammad-init.container | One-shot (Type=oneshot, RemainAfterExit=yes) — runs zammad-init to create/migrate the database schema before anything else that needs it starts. |
zammad-railsserver.container | The main Rails application server (zammad-railsserver) — serves the web UI and API. |
zammad-websocket.container | Zammad’s websocket service, used for real-time UI updates (new tickets, live agent status, etc). |
zammad-scheduler.container | Background job scheduler — email fetching, SLA/escalation checks, and other periodic Zammad jobs. |
zammad-nginx.container | The nginx frontend bundled in the Zammad image. Talks to the Rails server and websocket service over 127.0.0.1 (safe because they all share the pod’s network namespace), and is the only one that needs the host-published port. |
zammad-backup.container | Runs zammad-backup on Zammad’s internal schedule (BACKUP_TIME/HOLD_DAYS from .env), writing to BACKUP_DIR. |
The datastores
| Unit | Role |
|---|---|
zammad-postgresql.container | PostgreSQL — Zammad’s primary datastore. Data persists in the postgresql-data named volume. |
zammad-redis.container | Redis — caching and background job queueing. Data persists in redis-data. |
zammad-memcached.container | Memcached, capped at 256M (Exec=memcached -m 256M) — session/object caching. |
zammad-elasticsearch.container | Elasticsearch, running in discovery.type=single-node mode — powers Zammad’s full-text ticket search. Data persists in elasticsearch-data; heap size is controlled by ELASTICSEARCH_JAVA_OPTS in .env. |
zammad-storage is a separate named volume, mounted into every Zammad-image container
(railsserver/init/scheduler/websocket/nginx/backup) at /opt/zammad/storage, so uploaded
attachments and other application state are shared and consistent across all of them.
Startup order in practice
zammad.network→zammad.pod(network namespace + published port exist)zammad-postgresql,zammad-redis,zammad-memcached,zammad-elasticsearch(no app-level dependencies besides the pod)zammad-init(needs PostgreSQL up; runs once, then stays “active” viaRemainAfterExit)zammad-railsserver,zammad-scheduler,zammad-websocket(need Postgres/Redis/Memcached, and logicallyzammad-initto have already prepared the schema)zammad-nginx(needs the Rails server up, since it proxies to it)
.env.example
The environment file is grouped by concern:
- PostgreSQL — database name/user/password/port and connection pool size.
- Caching & Queues — Memcached server list, Redis URL.
- Application Settings —
ZAMMAD_FQDN, HTTP scheme. - Nginx — published port, max upload size.
- Elasticsearch — enable flag, credentials, namespace, reindex behavior, JVM heap options.
- Timezone —
TZused across all containers. - Backup Settings — backup directory, schedule, retention (
HOLD_DAYS). - Resource Limits, Proxy/External Access, Cloudflare Tunnel, and External Networks — all optional, commented out by default, for more advanced deployments (reverse proxy in front of nginx, Cloudflare Tunnel instead of a published port, or attaching services to a network managed outside this repo).