Self-Hosting Zammad (Part 2): Docker Compose & The CSRF Proxy Fix
Snapshot
In Part 1 of this series, we used Cloudflare Tunnels to bridge your home lab to the web without opening firewall ports. Now, we’re deploying the engine behind that URL: Zammad. This guide covers the Docker Compose deployment and the specific environment tweaks needed to kill the "CSRF Token" error for good.
The Requirement
Zammad is a distributed system, not a single app. To get it running, you aren't just starting one container; you're orchestrating a stack that includes PostgreSQL (Data), Elasticsearch (Search), and Redis (Cache).
Running this behind a tunnel adds a layer of complexity: Zammad is protective of its session security. If the app thinks it's on http://localhost:8080 but the user is hitting https://zammad.weitzman.info, it will block the login to "protect" you. We’re going to fix that.
The Architecture
- Host: Ubuntu 24.04 (Noble) on Proxmox
- Container Engine: Docker Compose
- Access: Cloudflare Tunnel (pointing to
http://localhost:8080)
Step 1: Deploying the Stack
We’ll use the official production-ready Docker Compose setup as our base.
1. Clone the Repository:
git clone https://github.com/zammad/zammad-docker-compose.git
cd zammad-docker-compose2. Initial Spin-up:
docker compose up -d3. The Wait: Zammad takes a few minutes to initialize its database and build Elasticsearch indices. You can tail the logs to see when the Rails server is ready:
docker compose logs -f zammad-railsserverOnce you see Starting Zammad, the services are live.
Step 2: The CSRF Proxy Fix
If you try to log in via your Cloudflare URL now, you’ll likely see a red error box: "CSRF token verification failed!"

The Solution: docker-compose.override.yml
Instead of hacking the main config, we’ll use an override file to inject the environment variables Zammad needs to understand it's behind a proxy.
1. Create the file:
nano docker-compose.override.yml2. Paste the following configuration:
services:
zammad-nginx:
environment:
- NGINX_SERVER_SCHEME=https
- NGINX_SERVER_NAME=zammad.weitzman.info
zammad-railsserver:
environment:
- ZAMMAD_HTTP_TYPE=https
- ZAMMAD_FQDN=zammad.weitzman.info
- RAILS_TRUSTED_PROXIES=127.0.0.1,172.16.0.0/12
zammad-worker:
environment:
- ZAMMAD_HTTP_TYPE=https
- ZAMMAD_FQDN=zammad.weitzman.info- RAILS_TRUSTED_PROXIES: Crucial. This tells Zammad to trust the X-Forwarded-For headers coming from your Docker network.
- NGINX_SERVER_SCHEME: Forces Zammad to generate internal links as https.
3. Apply and Force Recreate:
docker compose up -d --force-recreateStep 3: Initial Setup & The "Skip" Trick
Navigate to https://zammad.weitzman.info. You should now see the welcome wizard.
- Admin Setup: Create your first user.
- The Email Catch: During setup, Zammad will ask for Email Notification settings.
- Action: Select "Local MTA" and hit Skip.
- Reason: It is much cleaner to configure the Microsoft 365 Graph API (Modern Auth) from within the full dashboard settings than to try and jam it into the basic setup wizard.


Step 4: Connecting the Microsoft 365 Channel
This is what turns the app into a real helpdesk.
- Go to Settings (Cog icon) → Channels → Microsoft 365 Graph Email.
- Click Connect Microsoft 365 App.
- Use your Azure/Entra ID App Registration credentials (ensure your Redirect URI matches what Zammad displays in this menu).

Once authenticated, Zammad will begin "watching" your inbox. Any email to [email protected] becomes a ticket automatically.
Final Thoughts
By combining the Cloudflare Tunnel from Part 1 with this Docker Override config, you’ve created a production-grade helpdesk that is secure, proxied, and accessible from anywhere without a single open port on your router.
Pro-Tip: If your search results seem "stale" after the initial import, run a manual reindex:
docker compose exec zammad-railsserver zammad run rake searchindex:rebuildNext Up (Part 3): Implementing Cloudflare Access MFA to put a second lock on the front door.
Back to Projects