TECH

Ship with Confidence: Rails Production Checklist

Ship with Confidence: Rails Production Checklist

You finally click Deploy. The terminal scrolls, CI is all green, and for a few seconds, you feel unstoppable. You open the app in production, click around, and everything seems fine. Then an admin page loads in 12 seconds, a user reports they never got their confirmation email, and you notice a warning about "Not secure" in the browser bar. That's the moment you realize: "working on my machine" is very different from "ready for the real world".

The goal of this article is simple: help you build a Rails app that does not crumble under real traffic, real users, and real-world threats. By the end, you will know what to put in place, so your next deploy feels like a confident release, not a production experiment.

You Cannot Fix What You Cannot See

A production app without visibility is like debugging with your eyes closed. When something goes wrong in production, you don't want to be guessing; you want a clear story of what happened, when, and why. That's where centralized logging and error tracking come in.

Modern APM platforms handle everything in one place — performance monitoring, logs, error tracking, and even infrastructure metrics — so you don't need separate apps for each. Services like New Relic, Datadog, Sentry, AppSignal, or any other Swiss Army knife give you a unified dashboard where you can search across requests, filter by environment, correlate errors with traffic spikes, and get instant notifications when exceptions occur.

When a user reports "the app is slow," you can open your dashboard and immediately see which endpoint is dragging, which query is misbehaving, and when the slowdown started. The stack traces, context, and metrics are integrated, so debugging transitions from hours of guesswork to minutes of targeted fixes.

Keep the Ground From Shifting: SSL, Backups, and CDN

Once you can see what is happening, you need to make sure the ground under your app is stable. That starts with SSL. In the 2020s-era web, running a production app without HTTPS is basically broadcasting that security is optional. With SSL in place, you encrypt traffic between your users and your server, protect authentication data, and earn the trust of modern browsers (no more scary "Not secure" banners). Tools like Let's Encrypt and automated certificate renewal make it painless to keep certificates valid, so there is no excuse to ship plain HTTP to production.

Right next to SSL on the "non-negotiable" list are automated backups. A database without a tested backup is a horror story waiting to happen. Backups should be automatic, frequent enough for your business needs, and most importantly, restorable. It is not enough to know a backup job runs; you should have gone through at least one restore dry-run so you're not learning under pressure at 2 a.m. Losing data once will teach you this lesson the hard way; it is much better to practice recovery when nothing is on fire.

Finally, think about how you serve static assets like images, CSS, and JavaScript. Offloading them to a CDN does three big things at once: it reduces load on your app servers, speeds up delivery by using edge locations closer to your users, and often improves perceived performance dramatically. When your assets are cached globally, first-page loads feel snappier, even on slower connections. That difference can be the line between a user sticking around or bouncing before your app even shows what it can do.

Stop Reinventing Security

security

Security is one of those topics everyone agrees is important, right up until it delays a feature. The fastest way to get most of it right is to stand on the shoulders of tools that already exist. For authentication, libraries like Devise solve the hard problems of sign-in, password resets, confirmations, and session management. For authorization, something like Pundit lets you declare who can do what in a clear, testable way instead of scattering permission checks all over controllers and views.

Once the basics are covered, start thinking like the internet is out to get you, because sometimes it is. That's where Rack::Attack shines. It lets you rate-limit requests, block abusive IPs, throttle login attempts, and slow down suspicious traffic before it ever hits your app logic. For example, you can easily cap how many sign-in attempts a single IP can make in a minute or block repeated requests to a sensitive endpoint. This protects you not just from malicious attacks, but also from accidental or misbehaving clients hammering your app.

On the edge side, services like Cloudflare add another defensive wall in front of your infrastructure. They can help absorb basic DDoS attempts, cache static content, and filter obviously bad traffic before it ever reaches your server. Combined with Rack::Attack, you get two layers: one at the network edge and one inside your app stack. That layered approach means a random botnet or scraper spike is less likely to knock you offline or run up your hosting bill, and you don't spend your weekend blocking IPs by hand.

Forms are bot magnets. Add reCAPTCHA, Invisible Captcha, or a custom honey pot to contact forms, signups, and anything that accepts user input. A few extra seconds for humans prevents your database from filling up with "viagra@example.com" spam accounts. Test it thoroughly — users hate solving puzzles only to hit a 500 error anyway.

Static Analysis: Let the Robots Catch What You Miss

Before you even think about production, let static analysis tools do the heavy lifting. Rubocop enforces a consistent code style and catches common Ruby pitfalls. Brakeman scans for security vulnerabilities like mass assignment or outdated Rails patterns. Bundler Audit flags gems with known security issues. Run these regularly — they find problems you didn't even know existed and save you from "it worked on my machine" surprises.

Performance Isn't Optional

A page that technically works but loads in eight seconds might as well be broken. Performance problems often start invisibly: an extra includes missing here, a forgotten order there, a "temporary" query in a controller that joined a few more tables over time. Tools like Bullet or Prosopite help you detect N+1 queries before they turn into major slowdowns, highlighting places where each record triggers its own query instead of fetching data in bulk.

Database indexes are another huge lever. When you know a column is used for searching, filtering, or joining, give it an index so your queries don't degrade as the table grows. Pair that with external performance monitoring mentioned at the beginning, and you'll actually see which endpoints and queries are your bottlenecks. Instead of guessing which part of the app is slow, you can open a dashboard and find out in seconds.

On the UI side, don't ask a browser to render thousands of records at once. Adding pagination to heavy lists does not just save bandwidth; it keeps pages usable and responsive. And any work that does not need to block a response — sending emails, generating PDFs, syncing with third-party APIs — belongs in background jobs. When long-running tasks move to Sidekiq, Solid Queue, or another job processor, your users experience instant responses while the heavy lifting happens behind the scenes. The result is an app that feels fast and responsive, even under load.

Servers: Treat Them Like Tires, Not Pets

server

Your server should be replaceable, not a snowflake config nightmare. Docker containers solve the dependency hell of "just the right version of Ruby/Node/Postgres" by bundling your entire app stack into portable images. No more fighting server environments or version mismatches — everything your app needs lives inside the container.

Tools like Kamal take this further by treating servers like cattle, not pets. Add a fresh Ubuntu server's IP to your config (no prep needed beyond SSH keys), and Kamal auto-provisions Docker, pulls your image, and deploys in minutes. Docker's layer caching makes subsequent deploys lightning fast, and you can use the same images for CI/CD pipelines.

When one server fails, spin up a new one — no 47 custom configs to recreate. Production apps scale horizontally across multiple identical containers, not vertically into a single fragile machine. This gives you portability across clouds or your own hardware — no waiting for Cloudflare's "just a brief outage" or AWS's "regional incident that totally wasn't our fault."

Make Email, Payments, and Files Boring

A surprising amount of production chaos comes from three areas: email, payments, and file handling. Each one can quietly fail in ways that only your users notice — unless you prepare.

For email, don't rely on synchronous delivery. Always send transactional emails through background jobs so a slow SMTP server does not block user requests. Use a dedicated "fast" queue for time-sensitive messages like confirmations or password resets so they never sit behind a long batch job. And actually test this flow in a staging environment wired to a real email provider so you know messages arrive quickly and with the correct formatting.

With payments, the bar is even higher. A payment flow that "should work" is not enough; you need to prove it. Run full end-to-end tests in your provider's sandbox and then complete at least one real payment before launch. Walk through the entire journey like a user: checkout, confirmation, emails, and refunds if needed. Each successful dry run is one less reason for a real customer to bounce or lose trust.

Files and images bring their own complexity. Storing uploads in an external service like S3 keeps your app servers stateless and your data durable across deploys and scaling events. But don't just configure it in code and assume it is fine. Test image processing and uploads thoroughly on staging with realistic files. Verify that thumbnails generate correctly, different formats are handled, and errors are gracefully reported to the user. When these flows are boring in staging, they are much more likely to be boring in production — and boring is exactly what you want here.

Tests, Documentation, and Migrations: Protect Future You

tests

Good tests are more than a checkbox; they're insurance against regressions. A well-structured spec suite lets you refactor core features without guessing whether you broke sign-up, checkout, or some obscure admin flow. Aim to cover the main user journeys and the most fragile business rules. When adding features, let tests catch broken assumptions before they reach production.

Traditional docs work great at launch, then rot as features change. The best documentation lives in your test suite. Well-written specs explain how business logic should behave, and they stay accurate because they break when code changes. Future you (and future teammates) will thank you when they can read a spec and instantly understand the checkout flow instead of hunting through outdated wiki pages.

Database changes are another area where a little discipline pays off massively. Using tools like Strong Migrations that help you avoid dangerous schema operations in production, such as adding columns with default values that lock entire tables or dropping columns still in use. Instead of praying every time you run rails db:migrate on production, you get clear guidance on how to roll out changes safely: adding columns in multiple steps, backfilling data, and only later enforcing constraints. Over time, this keeps your deploys smooth instead of turning migrations into mini-outages.

Don't Ship in the Dark: SEO, Analytics, and Staying Updated

Once your app is secure, observable, and relatively fast, think about how people discover and use it. Basic SEO hygiene goes a long way: set up proper meta tags so links look good when shared, configure a favicon so your app feels polished in browser tabs, and generate a sitemap so search engines can crawl your main pages. This is not about gaming search; it is about making sure your app is not invisible.

At the same time, wire in analytics from day one. Whether you use Google Analytics, a privacy-focused alternative, or a custom setup, you should be able to answer simple questions: How many people sign up? Where do they drop off? Which pages are actually used? Without that data, you're guessing which features matter and which bugs hurt the most.

Finally, keep your Ruby version and gems reasonably up to date. You don't have to chase every latest release, but launching with obviously outdated, vulnerable dependencies is asking for trouble. Use Dependabot to automate dependency updates and security patches — it creates pull requests for you to review and merge, keeping things current without manual hunting. Regular, small updates are much safer than massive, once-a-year upgrade marathons. Think of it as maintenance: a bit of effort now to avoid a painful overhaul later.

Launch Checklist

Before you ship, walk through this list and tick each box as you go.

Infrastructure

Security

Performance

Polish & Reliability

Checklist progress0/0 completed

Ship With Confidence, Not Hope

deploy

If you remember one thing from this post, let it be this: production rewards preparation, not bravery. Pick one area from the checklist you're weakest in — maybe error tracking, maybe backups, maybe security — and improve it today. Your future deploys (and your future self) will thank you.

Przeczytaj więcej na naszym blogu

Sprawdź bazę wiedzy zebraną i opracowaną przez doświadczonych
profesjonalistów.
bloglist_item
Tech

The buzzwords are Readability, Reusability, Maintainability. Here's the long version: Modern web applications can grow in complexity. We often need to manage workflows more complex than simple...

bloglist_item
Tech

Over the years I had to deal with applications and system that have a long history of already being "legacy". On top of that I met with clients/product owners that never want you to spend time...

bloglist_item
Tech

How many times have you searched for that one specific library that meets your needs? How much time have you spent customizing it to fit your project's requirements? I must admit, waaay too much....

ul. Powstańców Warszawy 5
15-129 Białystok
+48 668 842 999
SKONTAKTUJ SIĘ Z NAMI