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-composeRepo) - 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-1kann abweichen – prüfen mitdocker 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
docker compose execerbt keine Entrypoint-Variablen – immer explizit via-eübergeben!in Bash-Strings ist gefährlich –.rb-Dateien statt Inline-Code verwenden- Modell-Felddoku ist oft veraltet –
ClassName.column_namesist die Wahrheit - Kryptische ActiveRecord-Fehler oft durch
nilwo{}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.