Docs · Deployment

First Deploy Checklist

Secrets, the master key, and what's required versus optional before you deploy.

The very first bin/kamal deploy needs less than you'd expect: a Rails master key and a container registry token. Everything else, Stripe, Resend, S3, is genuinely optional at this stage, since the app runs on its Local fakes until you configure real credentials. See Bring Your Own Keys for the full reference.

What secrets actually need to exist

Kamal's secrets file, .kamal/secrets, reads each value from a gitignored local file, falling back to an environment variable, which is how CI injects them. For a local bin/kamal deploy, the files worth creating are:

File Value
config/master.key Your Rails master key
.kamal/.registry-token A container registry token with write access, for pushing the built image
.kamal/.stripe-secret-key Stripe live secret key, optional
.kamal/.stripe-webhook-secret Stripe webhook signing secret, optional
.kamal/.resend-api-key Resend API key, optional

The very first deploy needs only the master key and the registry token. Set a real price id in config/deploy.yml before you turn on billing for real; see Stripe Setup.

Required before production will even boot

Rails will not boot in production without a secret key base; it signs sessions and cookies, and there's no safe default the way development and test have. This is the most common first-deploy failure precisely because it doesn't show up locally: dev and test each generate a temporary key automatically, so a fresh clone runs fine right up until the first real deploy fails. See Secret Key Base for how to generate one.

The Stripe webhook events, if you're turning on billing

If you enable real Stripe billing, your webhook endpoint needs specific events enabled, not just "all events." A handler exists for each of these, and an event that isn't enabled simply never fires, with no error anywhere to notice: entitlement quietly stops tracking reality.

Event What breaks without it
checkout.session.completed Purchases don't grant access (the return-URL redirect covers some of this, but only when the buyer actually lands there)
invoice.payment_succeeded Renewals don't extend access
invoice.payment_failed Failed renewals don't enter a past-due grace window
customer.subscription.updated Plan changes and cancellations don't sync; the UI keeps saying "renews"
customer.subscription.deleted Access isn't revoked when a subscription actually ends
charge.refunded A refunded buyer keeps access
charge.dispute.created A buyer who charges back keeps access
radar.early_fraud_warning.created A charge the card issuer already flagged as fraudulent turns into a full dispute you could have avoided

An early fraud warning is the issuer flagging a charge as fraudulent before the cardholder actually disputes it. Revoking access and refunding immediately in that window usually means no formal dispute ever gets filed, which avoids both a dispute fee and a mark against the dispute ratio card networks judge your account on. This is also why the Stripe webhook endpoint is exempt from the app's general per-IP rate limit: Stripe delivers from a small pool of IPs and can auto-disable an endpoint that keeps returning non-2xx responses, and a throttled webhook fails both silently and completely.

A daily job re-checks lapsed subscriptions against Stripe, so a single missed webhook delivery self-heals on its own. It can't cover refunds, disputes, or fraud warnings, though; none of those have a lapse date for the job to notice, which is exactly why those specific events need their own handlers rather than relying on the daily sweep.

Verify

bin/kamal setup
bin/kamal logs -f

Then confirm the app answers, both on the server directly and through your public domain, before you consider the deploy done.

Next

Get your domain pointed at the server: Edge and DNS Setup.