Rails + Kamal: The 8 Deployment Errors I Fought (And How I Won)

Introduction: The Infrastructure Battles Start

When I chose to front-load the deployment of my Wuthering Waves Planner, the goal is simple: get it live, get it stable. But between my local machine and the virtual private server, I hit a myriad of deployment errors that tested every single assumption I had with deploying an app.

This blog post documents my challenges of deploying a Rails 8 application powered by Kamal and Postgres. From the lurking IPv6-vs-IPv4 conflict to encountering my first tussle with Solid Queue, I encountered- and solved- the kind of issues that seemed unsurmountable at first.

My goal here is to give you a bit of guidance. If you’re using Kamal, Rails 8, or wrestling with containerized Postgres database, this is a look at the hardest errors I’ve come across to date.


I. The Core Challenge: Bridging the Local/Remote Divide

The first set of errors centered around a fundamental disconnect between my local development environment and the Kamal deployment target. The symptoms were subtle, but the root cause boiled down to networking and configuration assumptions.

A. The Silent Killer: IPv6 Ambiguity

Symptom:

PG::ConnectionBad: connection to server at "::1" failed

On my Linux machine, the simple alias localhost resolved to the IPv6 address ::1. Unfortunately, the Postgres container within my local Docker setup was only configured to listen on IPv4 (127.0.0.1). The connections were never meeting.

Solution:

Explicitly setting the host in config/database.yml to host: '127.0.0.1' for development and test environments forced the connection over the correct network protocol. Never assume localhost in Docker.

## config/database.yml

development:
  <<: *default
  database: wuwa_planner_development
  host: '127.0.0.1'

test:
  <<: *default
  database: wuwa_planner_test
  host: '127.0.0.1'

B. The Inevitable Password Reset

The next roadblock I faced was a simple one, yet difficult to find: Postgres was refusing a valid password.

Symptom:

FATAL: password authentication failed for user "postgres"

I knew the password was correctly configured on my side, but I did change it at some point. This pointed me to the old-password vs. new-password conundrum. The root cause was Stale Docker Volume Data. A previous, failed attempt to initialize the container had written the old, encrypted password hash into the local Postgres volume. The new password, tough correctly configured in the .env file, was being ignored.

Solution:

The only clean path was to perform a complete teardown of the persistent data:

docker-compose down # Stop and remove the containers
docker volume ls | grep wuwa # To find the right volume
docker volume rm wuwa-planner_pg_data # <--- Delete the stale volume
docker-compose up # Rebuild and force Postgres to re-initialize

C. Proper Hostname: Hyphens Over Underscores

Kamal and Docker are incredibly sensitive to naming conventions. I get confused since I default to underscore (since I work with Ruby). A seemingly harmless naming choice nearly derailed DNS resolution.

Symptom:

URI::InvalidURIError: the scheme postgres does not accept registry part: ...@wuwa_planner-db (or bad hostname?)

The initial Docker service name was service: wuwa_planner (using an underscore). This naming convention (default with Rails) proved internally invalid for DNS resolution in Docker components during deployment.

Solution:

Standardized the entire project on hyphens (wuwa-planner) for the Docker service and image names. This small change guaranteed a valid, predictable DNS hostname (wuwa-planner-db) for my DATABASE_URL.


II. Secrets, Dependencies, and URL Fragility

The next phase introduced two classic production headaches: URL-safe passwords and ensuring my environment variables actually make it into the container.

A. The Non-URL-Safe Password

Symptom:

URI::InvalidURIError: bad URI: postgres://rails:geKtByV9zr@NP9F&#q#!6Fb&gp!aK#Ye@...

The production password I generated using a strong password manager (Bitwarden) contained URL-reserved characters (@, #, &). These characters need to be URL-encoded, and failing to do so breaks the entire DATABASE_URL.

Solution:

I generated a new, alphanumeric-only password for the database and ensured the DATABASE_URL was built with the clean string. I encourage keeping secrets simple for URI use or ensure great URL encoding is applied.

B. The Case of the Missing Variable

Symptom:

ActiveRecord::ConnectionNotEstablished: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed

This looked like a networking issue again, but the root cause was actually simpler and more critical: Empty DATABASE_URL. The variable which was defined in the secrets file, was not being injected into the running container.

Solution:

I confirmed DATABASE_URL was explicitly listed under

## config/deploy.yml

env:
  secret:
    - DATABASE_URL

and correctly defined in my .kamal/secrets file. Deployment tools require explicit permission to inject secret; is what I learned.


III. Conquering Rails 8 and Solid Queue Stability

The final hurdle was specific to the latest Rails 8 stack (with its addition of the Solid gems) and its default configuration for background processing with Solid Queue.

A. Worker Crash Loop and Puma Shutdown

I finally ran bin/kamal deploy with no errors, which gave me great relief and it felt like I could start writing my first Models… However, when I visited the server’s IP Address, it was not over.

Symptom:

Detected Solid Queue has gone away, stopping Puma...

The environment was defaulting to SOLID_QUEUE_IN_PUMA: true. This attempts to run the background worker inside the main web server process. And any immediate worker crash (e.g., a missing config or initial error) forces the web server (Puma) to shut down prematurely, causing a crash loop.

Solution:

I disabled the embedded worker by explicitly setting SOLID_QUEUE_IN_PUMA: false in my config/deploy.yml. This separates the web process from the background worker, which allows the web app to stay up even if the worker fails.

B. Missing Tables: Migration on Production

While the app was a bit more stable, the database was still throwing errors about missing tables.

Symptom:

PG::UndefinedTable: ERROR: relation "solid_queue_recurring_tasks" does not exist

I knew I had set up the database, however the error meant the tables Solid Queue was expecting simply didn’t exist. I thought the db:prepare command alone would handle everything, but it seemed like it only set up the connection, and not the actual structure of the database on the production server. The database was connected, but it was simply empty!

Solution:

I had to manually tell the server to run the database migrations, which is what creates all the tables. Thus, the fix was running this specific command using Kamal to execute it remotely:

bin/kamal app exec 'bin/rails db:migrate'

Once that ran, all the necessary tables (including the weird Solid Queue ones) were created, and the application stopped throwing the UndefinedTable error. It was a huge “aha!” moment: just because the database is connected doesn’t mean it’s built yet.

C. The Final, Anti-Climactic Error

The server was up, the database was connected and properly migrated, but why could I still not see my beloved Rails page whenever I create new Rails apps? The final error was purely application logic.

Symptom:

ActionController::RoutingError (No route matches [GET] "/")

Expected, kind of silly actually. The application just didn’t have any root route defined yet. I was so focused on deployment I assumed the Rails::WelcomeController#index would work on production, the same it did on the development environment.

Solution:

Generated the Pages#home controller and set the root "pages#home" route in config/routes.rb. Deployment finally complete!


Conclusion: It’s All About the Fundamentals

What this whole experience taught me is that successful deployment isn’t really about writing crazy, complicated code. It’s more about mastering the basic rules of the infrastructure underneath. I learned that you have to be quite precise about the fundamentals: making sure the networking is exact, handling secrets carefully so they don’t break the URL, ensuring that the correct environment variables actually get injected, and most importantly, understand how things like my web server (Puma) and my background processor (Solid Queue) actually work together. I wanted to avoid disabling my Solid gems just for a quick fix. Everything relies on those foundations being stable. I might even encounter new errors on my next deployment lol.

For now, the Wuthering Waves Planner (while still a blank slate) is now live, a testament to overcoming the challenges of local development and remote production.

This video provides a helpful guide for fellow developers planning to attempt their first Rails 8 + Kamal deployment

While this article is what assisted me in setting up all three Solid gems in Rails 8 to use a single database in production