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-compose

2. Initial Spin-up:

docker compose up -d

3. 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-railsserver

Once 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 red CSRF error message in the Zammad login UI

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.yml

2. 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-recreate

Step 3: Initial Setup & The "Skip" Trick

Navigate to https://zammad.weitzman.info. You should now see the welcome wizard.

  1. Admin Setup: Create your first user.
  2. 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.
Email Notification Wizard StepConnect Channels Wizard Step

Step 4: Connecting the Microsoft 365 Channel

This is what turns the app into a real helpdesk.

  1. Go to Settings (Cog icon) → ChannelsMicrosoft 365 Graph Email.
  2. Click Connect Microsoft 365 App.
  3. Use your Azure/Entra ID App Registration credentials (ensure your Redirect URI matches what Zammad displays in this menu).
Microsoft 365 Graph Email Configuration Screen

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:rebuild

Next Up (Part 3): Implementing Cloudflare Access MFA to put a second lock on the front door.

Back to Projects