Twenty Small Reasons That Deploying a Rails App Is Hard

Twenty Small Reasons That Deploying a Rails App Is Hard

Diagram of deployment friction points between development laptop and production server

Deploying a Rails application is not a single difficult task. It is twenty small tasks, each individually reasonable, that combine into something that routinely eats entire weekends. The difficulty is not any one piece—it is the interaction effects, the order dependencies and the error messages that point somewhere other than the actual problem. This article catalogues the real friction points that trip up Rails developers moving from development to production, drawn from years of watching deployments go sideways. If you are looking for the step-by-step walkthrough, see the Deploy Rails on Your Own Server learning path. What follows is the honest map of what makes it hard.

1. Ruby version mismatches

Your laptop runs Ruby 3.3.1. The server has Ruby 3.2.0 from the system package manager. Your Gemfile.lock was generated with one, and bundle install runs on the other. Native extensions compile against the wrong headers. The error message mentions a C library you have never heard of.

2. Bundler version conflicts

Bundler has its own versioning. The Gemfile.lock records which Bundler version created it. If the server has a different Bundler version, you get a warning or an error, depending on how different the versions are. The fix is simple (install the right Bundler) but the error message rarely says that clearly.

3. Native extension compilation

Gems like pg, nokogiri, ffi and sassc require C libraries to be present on the server before they can compile. The error output is a wall of C compiler messages that ends with "Failed to build gem native extension." The actual useful information—which package you need to install—is buried twenty lines up.

4. Database configuration mismatch

database.yml in development uses localhost with no password. Production needs a hostname, a username, a password and probably a different port. The connection failure message just says "could not connect to server" without telling you which of those four things is wrong.

5. Missing environment variables

Your app needs RAILS_MASTER_KEY, DATABASE_URL, REDIS_URL, SECRET_KEY_BASE and half a dozen API keys. Forgetting one produces a boot failure or, worse, a runtime error that only surfaces when a specific feature is used.

6. Asset precompilation

bin/rails assets:precompile must succeed on the server or during the build step. It requires Node.js (for some asset pipelines), the correct JavaScript packages, and enough memory to run the compilation. An asset precompilation failure can produce error messages that look like JavaScript errors, Ruby errors or memory errors, depending on which stage fails.

7. The secret key base problem

Rails requires SECRET_KEY_BASE to be set in production. If it is not set, sessions and encrypted credentials break. If it changes between deploys, all existing sessions are invalidated. The error message when it is missing is clear, but the error message when it changes is not—users just get logged out.

8. File permissions

The deploy user needs to read the application code, write to tmp/, write to log/, and read the socket file that Puma creates. If any of these permissions are wrong, the failure mode is different each time: a permission denied error, a silent failure, or a 502 from Nginx.

9. Puma socket vs TCP

Puma can bind to a Unix socket or a TCP port. Nginx can proxy to either. But the upstream configuration in Nginx must match what Puma is actually listening on. If Puma binds to a socket and Nginx is configured for TCP (or vice versa), you get a 502 Bad Gateway with no useful detail.

10. Nginx configuration syntax

Nginx configuration files are sensitive to semicolons, braces and directive nesting. A missing semicolon produces an error message that points to a line several lines below the actual problem. Nginx also fails silently on some misconfigurations—the server starts, but requests get routed incorrectly.

11. SSL certificate setup

Let's Encrypt has made certificates free, but the setup still involves DNS verification, Certbot installation, Nginx integration and renewal scheduling. The first certificate usually works. The renewal that silently fails three months later is the one that takes down the site.

12. Database migrations in production

Running db:migrate on a production database is nerve-wracking because some migrations lock tables. An add_index on a large table can lock writes for minutes. A rename_column can break the application during the migration window. The safe approach requires knowing which operations lock and which do not—information that is not in the default Rails documentation.

Server roles and their responsibilities in a Rails deployment

13. Background job process management

Starting Sidekiq manually works until you close the terminal. Running it in screen works until the server reboots. A proper systemd unit file is the right answer, but writing one requires understanding systemd concepts (units, targets, environment files) that are outside most Rails developers' usual domain.

14. Log management

Rails logs everything to a single file that grows without bound. In production, this file will consume all available disk space if you do not configure log rotation. A full disk causes cascading failures: the database cannot write WAL files, Puma cannot write to the socket, and Nginx cannot write access logs.

15. Memory limits

A Rails application with Puma workers, Sidekiq processes and PostgreSQL running on the same server consumes significant memory. A 1GB VPS is tight. 2GB is comfortable for small applications. Running out of memory triggers the Linux OOM killer, which may kill your database process instead of Puma, producing symptoms that look like database failures rather than memory problems.

16. DNS propagation

You have configured the server, deployed the application and confirmed it works by IP address. Then you point the domain at the server and it does not resolve. DNS propagation takes time—minutes to hours. During this window, some users see the old server and some see the new one, which makes debugging reports unreliable.

17. Firewall rules

The server has a firewall (it should). But the firewall blocks port 80 and 443 because you only opened port 22 during initial setup. The application works over SSH but is unreachable from a browser. The error looks like a DNS problem or a server-down problem, not a firewall problem.

18. CORS and mixed content

Your application serves HTTPS but references an HTTP asset, or an API endpoint, or a WebSocket connection. The browser blocks the mixed content silently. Your JavaScript fails. The server logs show nothing because the browser blocked the request before it was sent.

19. Time zone configuration

The server is in UTC. Your application assumes the local time zone. Scheduled jobs run at the wrong time. Log timestamps do not match user-reported times. The config.time_zone setting in Rails and the system time zone on the server are independent, and they must both be set correctly.

20. The deployment script itself

After solving all of the above, you need a repeatable process for deploying updates. Capistrano, Kamal, a custom shell script, or a CI/CD pipeline—each has its own learning curve and failure modes. The first automated deploy always surfaces issues that manual deploys concealed.

Why it adds up

None of these problems is individually difficult. A senior developer who has done it before can work through all twenty in an afternoon. But a developer encountering them for the first time, without a clear guide, faces a combinatorial puzzle where each problem's solution can create the next problem.

The answer is not to avoid deployment. It is to work through the problems systematically, with good notes, and build the knowledge that makes the next deployment routine instead of heroic.