Prod-Rails-Diagnose + Datenfixes: kamal exec, vollbreites `?`, Base64, Thread-Kontinuität.
Produktions-400/500 haben nichts mit lokal gemein. Lokal lässt du Tests neu laufen, editierst, lässt neu laufen. In Produktion hat der Nutzer schon auf „Bezahlen" geklickt und starrt auf einen weißen Bildschirm.
Drei Wege:
rails console, deine Änderungen haben kein Audit, ein Tippfehler und du hast ein ProblemDieser Artikel behandelt den dritten Weg. Zwei reale Fälle: ein in die Credentials geratenes ? in voller Breite, das x402-Zahlungen mit 500er-Fehlern lahmlegte, und ein Tweet in der Mitte eines Threads mit status: :failed, der wiederholt werden muss ohne die Thread-Kontinuität zu brechen. Unterwegs wird Claude per Default in 4 falsche Richtungen ziehen wollen — jede muss abgefangen werden.
kamal app exec --reuseKamal ist das Rails-Deploy-Tool von 37signals. Es hat einen Befehl:
kamal app exec --reuse 'bin/rails runner "..."'
--reuse bedeutet: keinen neuen Container hochziehen, sondern im laufenden Web-Container ausführen. Kein neuer Build, kein docker pull, kein Neustart, keine ENV-Neueinspeisung. Befehl läuft, Container macht weiter mit Requests.
Die Ausgabe fließt per stdout zurück auf dein Terminal — praktisch puts in einer produktiven Rails-Konsole, ohne SSH, tmux oder den Laptop zu verlassen.
Typische Sitzung:
$ kamal app exec --reuse 'bin/rails runner "puts User.count"'
Launching command with version abc123 from existing container...
INFO [ok] Finished in 3.8 seconds with exit status 0
App Host: deploy.how2claude.com
12847
Hin und zurück in 3-6 Sekunden. Zwei Größenordnungen schneller als ein Redeploy.
? in voller Breite, das Produktion-500er verursachteCommit eba9ac9.
Symptom: Einige Stunden nach dem Livegang von x402-Zahlungen kam jeder Zahlungs-Request mit 500 zurück. Logs voll mit Net::HTTPBadResponse vom HTTP-Client. Lokal lief alles perfekt.
Diagnose: Claude soll zuerst die x402-Konfiguration in Produktion ausgeben:
kamal app exec --reuse 'bin/rails runner "puts X402.configuration.wallet_address.inspect"'
Ausgabe:
"0xAbC123...def?"
Am Ende ein zusätzliches ? — ein Fragezeichen in voller Breite (U+FF1F), nicht das halbe ?. Jemandes IME hat mitten beim Editieren von config/credentials/production.yml.enc umgeschaltet und dieses Zeichen hineingemogelt.
Das lokale config/credentials.yml.enc (entschlüsselt mit dem Dev/Test-Master-Key) hat das nicht — in Rails 8 sind Production und Dev separate verschlüsselte Credentials, Inhalte werden nicht geteilt.
Fix: Du kannst nicht per SSH die Datei direkt bearbeiten (verschlüsselt) und nicht runterziehen und lokal bearbeiten (der Master-Key ist nicht auf deinem Laptop). Der Zug ist, Claude ein einmaliges Ruby-Skript schreiben zu lassen und per EDITOR= in credentials:edit einzuschleusen:
# script/fix_prod_wallet.rb
content = File.read(ARGV[0])
# Vollbreitiges Fragezeichen am Ende entfernen
content.gsub!(/(wallet_address: 0x[0-9a-fA-F]+)?\s*$/, '\1')
File.write(ARGV[0], content)
EDITOR="ruby script/fix_prod_wallet.rb" \
bin/rails credentials:edit --environment production
Ablauf von credentials:edit: entschlüsseln → in temporäre Datei schreiben → $EDITOR aufrufen → neu verschlüsseln → Temp löschen. EDITOR durch unser Ruby-Skript ersetzen und die Bearbeitung ist automatisiert, ohne den Chiffretext lokal zu beäugen.
Danach git commit + kamal deploy einmal — dieser Deploy ist nötig, weil production.yml.enc sich geändert hat. Aber die Diagnose hat keinen Deploy gekostet.
Regel: Wenn Produktion kaputt geht, erst lesen mit kamal app exec --reuse. Nicht raten, nicht zuerst neu deployen.
Symptom: Nach Artikelveröffentlichung landen Tweets in der Tabelle x_queue_tweets. Einer davon — der 2. eines 4er-Threads — endet mit status: :failed (X-API-Ratenbegrenzung, Inhaltsvalidierung, was auch immer). Wiederholung erfordert, die Thread-Kontinuität zum 1. Tweet zu wahren.
Den fehlgeschlagenen finden:
kamal app exec --reuse 'bin/rails runner "
XQueue::Tweet.where(status: :failed).each do |t|
puts \"#{t.id}: thread=#{t.thread_id} pos=#{t.thread_position} content=#{t.content.inspect}\"
end
"'
Ergibt id=87, thread_id=15, thread_position=2.
Falle 1: Shell-Escaping. Tweet-Inhalt hat oft Anführungszeichen, Zeilenumbrüche, Backticks. Wenn du schreibst:
# fliegt um die Ohren — Shell frisst Anführungszeichen und Backslashes
kamal app exec --reuse 'bin/rails runner "t = XQueue::Tweet.find(87); t.update!(content: \"...\")"'
Der Base64-Tanz — lokal kodieren, Base64-String übergeben, im Runner dekodieren:
# Lokal kodieren
echo -n 'Umgeschriebener Tweet-Inhalt...' | base64
# => VW1nZXNjaHJpZWJlbmVyIFR3ZWV0LUluaGFsdC4uLg==
# Übergeben
kamal app exec --reuse "bin/rails runner \"
t = XQueue::Tweet.find(87)
t.update!(content: Base64.decode64('VW1nZXNjaHJpZWJlbmVyIFR3ZWV0LUluaGFsdC4uLg=='), status: :scheduled)
puts t.status
\""
Base64-Strings sind reines ASCII, shell-sicher.
Falle 2: Thread-Kontinuität. XQueue::PostTweetJob.perform_later(87) postet einen eigenständigen Tweet — verkettet nicht zum Tweet #1 — weil die X-API reply_to_tweet_id braucht und der Job per Default keinen solchen Wert führt.
x_tweet_id des Vorgänger-Tweets holen (erfolgreiche Sendungen füllen dieses Feld):
kamal app exec --reuse "bin/rails runner \"
t = XQueue::Tweet.find(87)
prev = XQueue::Tweet.where(thread_id: t.thread_id, thread_position: t.thread_position - 1).first
puts 'prev x_tweet_id: ' + prev&.x_tweet_id.to_s
\""
# => prev x_tweet_id: 1834567890123456789
Mit Reply-Ziel in die Queue legen:
kamal app exec --reuse 'bin/rails runner "
XQueue::PostTweetJob.perform_later(87, reply_to_tweet_id: \"1834567890123456789\")
puts \"enqueued\"
"'
Der polling_interval des Workers ist 0,1 Sekunden — er schnappt den Job fast sofort. Ein paar Sekunden später kamal app exec, um zu sehen, wie der Status von scheduled auf posted umkippt und x_tweet_id gefüllt wird — Thread durchgängig.
Regel: Datenoperationen in Produktion müssen Businessebenen-Constraints respektieren, nicht nur „Datensatz erfolgreich aktualisiert". Thread-Kontinuität ist ein Business-Constraint; Rails runner prüft das nicht für dich.
Bei dieser Art Arbeit ist Claudes erster Reflex oft falsch. Jeden einzeln erwischen:
„Ich mach SSH und schau mal rein..."
Umleiten: kamal app exec --reuse schlägt SSH — im Container, Rails-Umgebung geladen, auditiert (Kamal-Logs halten es fest), rührt die Host-Shell nicht an, keine Container-Drift-Sorgen (reuse garantiert die aktuelle Produktionsversion).
„Ich schreib eine Migration, die das vollbreitige
?aus wallet_address entfernt..."
Umleiten: Ein Credentials-Wert ändern braucht keine Migration (DB wurde nicht angefasst). Ein einmaliger Rails runner erledigt das in 10 Sekunden; eine Migration braucht Deploy und bleibt für immer im Schema. Migration nur, wenn der Fix möglicherweise wiederkommt; ein Tippfehler ist einmalig.
„Einfach
update!(content: "...")aufrufen..."
Umleiten: Jeder benutzergenerierte Inhalt (Tweets, Kommentare, eingegebenes Markdown) sollte über Base64 laufen. Shell-Parsing von Anführungszeichen, Backslash, $ und Backticks ist ein klassisches Minenfeld — Produktion ist der falsche Ort zum Üben.
perform_later ohne Business-Parameter„Ich mach einfach
PostTweetJob.perform_later(87)nochmal..."
Umleiten: Erst fragen „Steht dieser Datensatz in Beziehung zu anderen?" Threads haben reply_to-Beziehungen, Batch-Jobs haben batch_id, paginierte Jobs haben cursor — die Argumentliste eines Jobs ist der Träger dieser Business-Beziehungen. Ein Argument weglassen heißt eine Kette brechen.
Rails-Produktion mit Claude debuggen + Daten mutieren — 6 Regeln:
kamal app exec --reuse 'bin/rails runner "puts X"'. Problem lokalisieren, bevor irgendetwas geändert wird.kamal app exec --reuse ist das Standardwerkzeug, nicht SSH, nicht Redeploy. Im Container, Rails geladen, 3-6 Sekunden hin und zurück.EDITOR=ruby-script bin/rails credentials:edit --environment production. Ruby-Skript macht die Bearbeitung, kein Beäugen von Chiffretext lokal.echo -n 'X' | base64, im Runner Base64.decode64.Produktionsdebuggen ist nicht das Transplantieren deines Dev-Workflows in die Produktion — du hast keinen Iterationsraum und keine Fehlertoleranz. Was du wirklich nutzt, ist die Introspektionsfläche, die Produktion bereits bietet (Rails runner + kamal exec + credentials:edit), jeder Schritt die kleinstmögliche Änderung. Claude kann korrekten Ruby-Code schreiben — aber zu wissen „was direkt gemacht werden kann vs. was zuerst diagnostiziert werden muss" ist eine Entscheidung, die es nicht für dich trifft. Das ist deine Produktionsdisziplin.