Free

Debuggare Rails in produzione con Claude, senza SSH né redeploy

Diagnosi + fix dati Rails produzione: kamal exec, `?` larghezza piena, Base64, continuità thread.


I 400/500 in produzione non assomigliano per niente al locale. In locale rilanci i test, editi, rilanci. In produzione, un utente ha già cliccato "Paga" e sta fissando una schermata bianca.

Tre strade:

  1. SSH sul server — funziona, ma la shell dentro il container potrebbe non avere rails console, le tue modifiche non hanno audit, un errore di digitazione e sei nei guai
  2. Redeploy — pushi un fix, minimo 10+ minuti, possibile breve indisponibilità, e molti problemi non sono problemi di codice (sono dati o credentials)
  3. Eseguire Rails runner dentro il container in esecuzione — niente restart, niente redeploy, niente SSH, precisione da bisturi

Questo articolo tratta la terza strada. Due casi reali: un a larghezza piena infilato nei credentials che ha rotto i pagamenti x402 con 500, e un tweet a metà thread il cui status: :failed va riprovato mantenendo la continuità del thread. Strada facendo, Claude cercherà di sbagliare in 4 direzioni diverse per default — bisogna intercettare ognuna.


Lo strumento: kamal app exec --reuse

Kamal è il tool di deploy Rails di 37signals. Ha un comando:

kamal app exec --reuse 'bin/rails runner "..."'

--reuse significa: non tirare su un nuovo container, esegui dentro il container web già in esecuzione. Niente nuovo build, niente docker pull, niente restart, niente re-iniezione di ENV. Il comando gira, il container torna a gestire le richieste.

L'output scorre via stdout fino al tuo terminale — in sostanza un puts dentro una console Rails di produzione, senza SSH, tmux né lasciare il portatile.

Sessione tipica:

$ 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

Andata e ritorno in 3-6 secondi. Due ordini di grandezza più veloce di un redeploy.

Caso 1: Il a larghezza piena che ha causato 500 in produzione

Commit eba9ac9.

Sintomo: Poche ore dopo aver rilasciato i pagamenti x402, ogni richiesta di pagamento tornava 500. Log pieni di Net::HTTPBadResponse dal client HTTP. In locale, perfetto.

Diagnosi: Fai stampare a Claude la config x402 in produzione prima:

kamal app exec --reuse 'bin/rails runner "puts X402.configuration.wallet_address.inspect"'

Output:

"0xAbC123...def?"

C'è un di troppo in coda — un punto interrogativo a larghezza piena (U+FF1F), non il ? a mezza larghezza. L'IME di qualcuno è scattato durante un'edit di config/credentials/production.yml.enc, e quel carattere si è infilato dentro.

Il config/credentials.yml.enc locale (decrittato con la master.key dev/test) non ce l'ha — in Rails 8 production e dev sono credentials cifrati separati, contenuti non condivisi.

Fix: Non puoi fare SSH ed editare il file direttamente (cifrato) né scaricarlo e editarlo in locale (la master.key non è sul portatile). La mossa è far scrivere a Claude uno script Ruby usa-e-getta e iniettarlo via EDITOR= in credentials:edit:

# script/fix_prod_wallet.rb
content = File.read(ARGV[0])
# Rimuovi il punto interrogativo a larghezza piena in fondo
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

Flusso di credentials:edit: decritta → scrivi su file temp → invoca $EDITOR → ricritta → cancella temp. Cambiare EDITOR con il nostro script Ruby automatizza l'edit, senza bisogno di guardare il cifrato in locale.

Dopo, git commit + kamal deploy una volta — questo deploy è obbligatorio perché production.yml.enc è cambiato. Ma la diagnosi non è costata un deploy.

Regola: Quando la produzione si rompe, leggi prima con kamal app exec --reuse. Non tirare a indovinare, non redeployare per primo.

Caso 2: Riprovare un XQueue::Tweet fallito a metà thread

Sintomo: Dopo la pubblicazione di un articolo, i tweet entrano nella tabella x_queue_tweets. Uno di loro — il 2° di un thread di 4 — finisce con status: :failed (rate limit dell'API X, validazione contenuto, qualsiasi cosa). Riprovare richiede di preservare la continuità del thread col 1° tweet.

Trovare il fallito:

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
"'

Risulta essere id=87, thread_id=15, thread_position=2.

Trappola 1: escape della shell. Il contenuto dei tweet spesso ha virgolette, a capo, backtick. Se scrivi:

# salta tutto — la shell mangia virgolette e backslash
kamal app exec --reuse 'bin/rails runner "t = XQueue::Tweet.find(87); t.update!(content: \"...\")"'

Il balletto Base64 — codifica in locale, passa la stringa base64, decodifica dentro il runner:

# Codifica in locale
echo -n 'Contenuto del tweet riscritto...' | base64
# => Q29udGVudXRvIGRlbCB0d2VldCByaXNjcml0dG8uLi4=

# Passa
kamal app exec --reuse "bin/rails runner \"
  t = XQueue::Tweet.find(87)
  t.update!(content: Base64.decode64('Q29udGVudXRvIGRlbCB0d2VldCByaXNjcml0dG8uLi4='), status: :scheduled)
  puts t.status
\""

Le stringhe Base64 sono solo ASCII, sicure per la shell.

Trappola 2: continuità del thread. XQueue::PostTweetJob.perform_later(87) pubblica un tweet autonomo — non si concatena al tweet #1 — perché l'API X ha bisogno di reply_to_tweet_id, e il Job per default non lo porta.

Trovare l'x_tweet_id del tweet precedente (gli invii andati a buon fine riempiono questo campo):

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

Accoda col target di reply:

kamal app exec --reuse 'bin/rails runner "
  XQueue::PostTweetJob.perform_later(87, reply_to_tweet_id: \"1834567890123456789\")
  puts \"enqueued\"
"'

Il polling_interval del worker è 0,1 secondi — pesca il job quasi subito. Qualche secondo dopo, kamal app exec per vedere lo status passare da scheduled a posted e x_tweet_id riempito — thread continuo.

Regola: Le operazioni sui dati in produzione devono rispettare i vincoli a livello business, non solo "record aggiornato con successo". La continuità del thread è un vincolo business; Rails runner non la controlla per te.

Le 4 direzioni in cui Claude sbanda per default (e come reindirizzare)

Facendo questo tipo di lavoro, il primo istinto di Claude è spesso sbagliato. Pesca ognuna:

1. Vuole fare SSH sul server

"Mi collego in SSH e do un'occhiata..."

Reindirizza: kamal app exec --reuse batte SSH — dentro il container, env Rails caricato, auditato (i log di kamal tengono traccia), non tocca la shell dell'host, niente drift del container (reuse garantisce la versione di produzione attuale).

2. Vuole scrivere una migration per sistemare i dati

"Scrivo una migration che rimuove il a larghezza piena da wallet_address..."

Reindirizza: Cambiare un valore di credentials non ha bisogno di migration (il DB non è stato toccato). Un Rails runner usa-e-getta lo fa in 10 secondi; una migration richiede un deploy e resta nello schema per sempre. Usa migration solo se la correzione potrebbe ripresentarsi; un errore di battitura è una cosa unica.

3. Infila caratteri speciali in stringhe di shell

"Mi basta chiamare update!(content: "...")..."

Reindirizza: Qualsiasi contenuto generato dall'utente (tweet, commenti, markdown inserito) deve passare per Base64. Il parsing shell di virgolette, backslash, $ e backtick è un campo minato classico — la produzione è il posto sbagliato per fare pratica.

4. perform_later senza parametri business

"Rilancio con PostTweetJob.perform_later(87)..."

Reindirizza: Chiedi prima "questo record è in relazione con altri?" I thread hanno relazioni reply_to, i job batch hanno batch_id, i job paginati hanno cursor — la lista degli argomenti di un Job è il portatore di quelle relazioni business. Salti un argomento, rompi una catena.

Checklist

Debuggare + mutare dati Rails in produzione con Claude — 6 regole:

  1. Leggi prima di scrivere. kamal app exec --reuse 'bin/rails runner "puts X"'. Localizza il problema prima di cambiare qualunque cosa.
  2. kamal app exec --reuse è lo strumento di default, né SSH né redeploy. Nel container, Rails caricato, andata e ritorno 3-6 secondi.
  3. Cambi di credentials via EDITOR=ruby-script bin/rails credentials:edit --environment production. Lo script Ruby fa l'edit, senza guardare cifrati in locale.
  4. L'escape della shell è un campo minato; instrada il contenuto speciale via Base64. echo -n 'X' | base64 in locale, Base64.decode64 nel runner.
  5. Rails runner non impone i vincoli business al posto tuo. reply_to del thread, batch_id, cursor di paginazione — accoda tutto insieme.
  6. Scrivi una migration solo se "questo potrebbe ricapitare". Correzioni dati una tantum via runner, due ordini di grandezza più veloci.

Debuggare in produzione non è trapiantare il tuo flusso di dev — non hai spazio per iterare, non hai tolleranza all'errore. Quello che usi davvero è la superficie di introspezione che la produzione già offre (Rails runner + kamal exec + credentials:edit), ogni passo facendo il cambiamento più piccolo possibile. Claude può scrivere Ruby corretto — ma sapere "cosa si può fare in diretta vs. cosa va diagnosticato prima" è una decisione che non prende al posto tuo. Quella è la tua disciplina di produzione.