Das Problem

Zammad-Konfiguration per Rails Console – klingt nach einer halben Stunde Arbeit. Wird dann gerne drei Stunden.

Der Zammad Docker Compose Stack bringt keinen „rails console"-Wrapper mit. Wer Einstellungen setzen will, die es nicht in die UI geschafft haben (SLA-Policies, Kalender, Trigger, TextModule), muss den richtigen Weg finden, einen Ruby-Skript im Container auszuführen. Die offizielle Doku hilft dabei… mäßig.

Dieser Artikel dokumentiert drei konkrete Fallen, die ich beim Setup von Zammad 6.5.2 getroffen habe, und wie man sie umgeht.


Umgebung

  • Zammad 6.5.2 (Docker Compose, offizielles zammad-docker-compose Repo)
  • Host: docker-hansolo (192.168.66.48)
  • Container-Prefix: zammad- (default aus Compose-Setup)

Falle 1: DATABASE_URL wird von docker compose exec nicht geerbt

Symptom

ssh root@192.168.66.48
cd /home/icke/zammad
docker compose exec zammad-railsserver rails runner 'puts Setting.get("fqdn")'
Could not load database configuration. No such file - config/database.yml

Oder, nach dem Erstellen einer Symlink-Workaround:

PG::ConnectionBad: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed: No such file or directory

Ursache

Der Zammad-Container startet via Entrypoint-Script (/docker-entrypoint.sh). Dieses Script setzt DATABASE_URL als Shell-Variable und exportiert sie im aktuellen Shell-Prozess – damit werden alle vom Entrypoint gestarteten Prozesse damit versorgt.

Aber docker compose exec öffnet eine neue Shell-Session im laufenden Container. Diese Session erbt nicht die Shell-Variablen des Entrypoint-Prozesses (Prozess-Umgebung != Shell-Umgebung des Entrypoints). Deswegen findet Rails die Datenbankverbindung nicht.

Das config/database.yml im Container-WORKDIR ist außerdem eine Template-Datei mit auskommentierten Feldern – sie funktioniert ohne DATABASE_URL nicht.

Lösung

DATABASE_URL explizit via -e an docker compose exec übergeben:

docker compose exec \
  -T \
  -e DATABASE_URL='postgres://zammad:zammad@zammad-postgresql:5432/zammad_production?pool=50' \
  zammad-railsserver \
  bundle exec rails runner /tmp/mein-skript.rb

Die Datenbank-URL ist im Standard-Setup immer gleich aufgebaut: zammad-postgresql ist der Service-Name aus dem Compose-File, zammad ist Username und Passwortname (Default), zammad_production ist die DB.

Anpassen wenn ihr eure Compose-Datei geändert habt.


Falle 2: Bang-Methoden werden von Bash escapet

Symptom

Ihr schreibt Ruby-Code inline direkt in den SSH-Befehl:

ssh root@192.168.66.48 "docker compose exec -T -e DATABASE_URL='...' \
  zammad-railsserver bundle exec rails runner \
  'Setting.set(\"fqdn\", \"helpdesk.darkside.local\"); Setting.save\!'"

Statt save! führt Rails dann save\! aus – oder der Befehl bricht mit einem Bash-Fehler ab:

bash: !': event not found

Ursache

Bash interpretiert ! in doppelt-gequoteten Strings als History-Expansion-Trigger. Selbst wenn ihr es escaped habt, ist das Verhalten je nach Bash-Konfiguration und SSH-Session inkonsistent.

Ruby-Methoden wie save!, create!, update!, first_or_create! enden alle auf !. Das ist in Ruby-Convention (Bang-Methoden = mutating oder raising). In Bash-Inline-Strings ist das ein Minefield.

Lösung

Kein Inline-Ruby in SSH-Strings. Immer .rb-Dateien nutzen:

1. Skript lokal schreiben (z.B. mit eurem Editor):

# /tmp/zammad_setup.rb
Setting.set("fqdn", "helpdesk.darkside.local")
Setting.set("http_type", "https")
Setting.set("organization", "Rebellion IT")
Setting.set("locale_default", "de-de")
Setting.set("system_init_done", true)
puts "Done: #{Setting.get('fqdn')}"

2. Auf den Host kopieren:

scp /tmp/zammad_setup.rb root@192.168.66.48:/tmp/

3. In den Container kopieren:

ssh root@192.168.66.48 "docker cp /tmp/zammad_setup.rb zammad-zammad-railsserver-1:/tmp/"

4. Ausführen:

ssh root@192.168.66.48 "cd /home/icke/zammad && \
  docker compose exec -T \
  -e DATABASE_URL='postgres://zammad:zammad@zammad-postgresql:5432/zammad_production?pool=50' \
  zammad-railsserver \
  bundle exec rails runner /tmp/zammad_setup.rb"

Etwas umständlicher, aber zuverlässig. Und man hat die Skripte noch lokal für spätere Anpassungen.


Falle 3: Das Calendar-Modell ist… speziell

Das Calendar-ActiveRecord-Modell in Zammad hat ein paar Eigenheiten, die nur durch Ausprobieren (oder diesen Artikel) zu finden sind.

3a: Kein active-Feld

cal = Calendar.new
cal.active = true  # => NoMethodError: undefined method 'active=' for #<Calendar ...>

Das Calendar-Modell hat keine active-Spalte. Einfach weglassen.

3b: Day-Keys sind lowercase

Zammad-Dokumentation und einige Blog-Posts zeigen Wochentag-Keys wie 'Mon', 'Tue' etc. Das stimmt nicht (mehr?). In Zammad 6.5.2 müssen die Keys lowercase sein:

# FALSCH (NoMethodError oder wird stillschweigend ignoriert)
cal.business_hours = {
  'Mon' => { 'active' => true, 'timeframes' => [['09:00', '17:00']] },
  'Tue' => { 'active' => true, 'timeframes' => [['09:00', '17:00']] },
}

# RICHTIG
cal.business_hours = {
  'mon' => { 'active' => true, 'timeframes' => [['09:00', '17:00']] },
  'tue' => { 'active' => true, 'timeframes' => [['09:00', '17:00']] },
  'wed' => { 'active' => true, 'timeframes' => [['09:00', '17:00']] },
  'thu' => { 'active' => true, 'timeframes' => [['09:00', '17:00']] },
  'fri' => { 'active' => true, 'timeframes' => [['09:00', '17:00']] },
  'sat' => { 'active' => false, 'timeframes' => [['09:00', '17:00']] },
  'sun' => { 'active' => false, 'timeframes' => [['09:00', '17:00']] },
}

3c: public_holidays darf nicht nil sein

cal.save!
# => TypeError: no implicit conversion from nil to integer

Der Fehler ist komplett irreführend – er klingt nach einem Typ-Fehler in einem Zahlenfeld, ist aber tatsächlich ein fehlendes public_holidays. Das Feld muss explizit auf ein leeres Hash gesetzt werden:

cal.public_holidays = {}  # PFLICHT, nil ist nicht ok

Vollständiges funktionierendes Beispiel

# /tmp/zammad_calendar.rb

cal = Calendar.where(name: "Bürozeiten").first_or_initialize
cal.timezone       = "Europe/Berlin"
cal.business_hours = {
  'mon' => { 'active' => true,  'timeframes' => [['09:00', '17:00']] },
  'tue' => { 'active' => true,  'timeframes' => [['09:00', '17:00']] },
  'wed' => { 'active' => true,  'timeframes' => [['09:00', '17:00']] },
  'thu' => { 'active' => true,  'timeframes' => [['09:00', '17:00']] },
  'fri' => { 'active' => true,  'timeframes' => [['09:00', '17:00']] },
  'sat' => { 'active' => false, 'timeframes' => [['09:00', '17:00']] },
  'sun' => { 'active' => false, 'timeframes' => [['09:00', '17:00']] },
}
cal.public_holidays = {}   # nicht vergessen!
cal.default        = false
cal.updated_by_id  = 1
cal.created_by_id  = 1
cal.save!
puts "Kalender: ID=#{cal.id}"

Das vollständige Workflow-Pattern

Für alle Rails-Console-Arbeiten an Zammad Docker:

# 1. Skript lokal schreiben
cat > /tmp/zammad_mein_task.rb << 'RUBY'
# hier Ruby-Code
puts "Fertig"
RUBY

# 2. Auf Host + in Container kopieren
scp /tmp/zammad_mein_task.rb root@<DEIN-HOST>:/tmp/
ssh root@<DEIN-HOST> "docker cp /tmp/zammad_mein_task.rb zammad-zammad-railsserver-1:/tmp/"

# 3. Ausführen (DATABASE_URL explizit!)
ssh root@<DEIN-HOST> "cd /home/<DEIN-USER>/zammad && \
  docker compose exec -T \
  -e DATABASE_URL='postgres://zammad:zammad@zammad-postgresql:5432/zammad_production?pool=50' \
  zammad-railsserver \
  bundle exec rails runner /tmp/zammad_mein_task.rb"

Dabei gilt:

  • <DEIN-HOST> = IP oder Hostname des Docker-Hosts
  • <DEIN-USER> = User unter dem das Compose-Verzeichnis liegt
  • Container-Name zammad-zammad-railsserver-1 kann abweichen – prüfen mit docker ps | grep railsserver
  • DATABASE_URL-Passwort anpassen wenn ihr es geändert habt

Debugging-Helfer

Wenn ihr nicht sicher seid wie ein Modell heißt oder welche Felder es hat:

# Alle verfügbaren Spalten
puts Calendar.column_names.inspect

# Vorhandene Objekte anzeigen
Calendar.all.each { |c| puts "#{c.id}: #{c.name}" }

# Ein Objekt vollständig anzeigen
puts Setting.find_by(name: 'fqdn').inspect

Lessons Learned

  1. docker compose exec erbt keine Entrypoint-Variablen – immer explizit via -e übergeben
  2. ! in Bash-Strings ist gefährlich.rb-Dateien statt Inline-Code verwenden
  3. Modell-Felddoku ist oft veraltetClassName.column_names ist die Wahrheit
  4. Kryptische ActiveRecord-Fehler oft durch nil wo {} erwartet wird

Wer mehr Zammad-Konfiguration per Rails Console erledigen will: die Muster aus Falle 1 und 2 gelten universell für alle Zammad-Modelle.