The go-to resource for upgrading Ruby, Rails, and your dependencies.

Transitioning a Monolithic Rails App to Docker and AWS ECS/Fargate


A comprehensive guide for engineering leaders on containerizing large and complex Ruby on Rails applications using Docker and AWS ECS/Fargate.

The Break-Bulk Problem of Rails Deployments

Before the 1950s, loading a cargo ship was a labor-intensive process known as break-bulk shipping. Longshoremen loaded individual sacks, barrels, and crates one by one. It was slow, expensive, and error-prone. The invention of the intermodal shipping container changed everything by standardizing the external interface. Ships, trains, and trucks could now handle cargo universally without needing to know whether a container held electronics or coffee beans.

Deploying a legacy Ruby on Rails application to traditional virtual machines often feels like break-bulk shipping. We find ourselves carefully provisioning the correct Ruby version, installing system dependencies like libpq-dev, and configuring web servers individually for each host. As the application grows, managing these servers becomes a significant operational burden.

Docker, of course, provides the standardized shipping container for our software. Transitioning a monolithic Rails app to Docker, and orchestrating it with AWS Elastic Container Service (ECS) on Fargate, offers a predictable, scalable infrastructure framework. This migration allows us to package the entire Rails application — including its specific Ruby version and gem dependencies — into a portable artifact.

Why AWS ECS and Fargate?

There are three major approaches to deploying containerized applications in the cloud today.

The first is a managed Platform-as-a-Service (PaaS) like Heroku or Render. This is often the most straightforward approach and requires the least infrastructure knowledge. However, at a certain scale, PaaS solutions can become cost-prohibitive and limit your ability to configure underlying network architecture.

The second is Kubernetes (often via Amazon EKS). Kubernetes is incredibly powerful and has become an industry standard. It is, though, notoriously complex. As the old joke goes, “I had a problem and I said to myself, I’ll use Kubernetes. Now, I have two problems.” For a single monolithic Rails application, Kubernetes is often overkill and introduces significant maintenance overhead.

The third option is AWS Elastic Container Service (ECS) using Fargate. Fargate is a serverless compute engine for containers. It provides the granular control of a container orchestration system without the burden of managing the underlying EC2 instances or the complexity of a Kubernetes control plane. Generally speaking, ECS on Fargate hits the sweet spot for Rails monoliths: it offers robust scalability and network isolation without requiring a dedicated DevOps team to keep the cluster running.

Preparing the Ruby on Rails Codebase for Docker

Before we write a Dockerfile, we must ensure our Rails application is ready for a containerized environment. Legacy codebases often rely on local file systems or hardcoded configurations, which conflict with the ephemeral nature of containers.

First, we need to extract all configuration from initialization files into environment variables. You might use tools like dotenv for local development, but in production, these variables will be injected by AWS ECS.

Second, we must modify our logging strategy. Older Rails applications typically write logs to log/production.log. In a containerized environment, this data would be trapped inside the container and lost when it shuts down. We need to configure the Rails logger to write to standard output (STDOUT). This allows AWS CloudWatch to capture and aggregate our logs automatically.

Update your config/environments/production.rb file:

config.logger = ActiveSupport::Logger.new(STDOUT)

Finally, we need to assess our file storage strategy. If your application writes user uploads directly to the local disk, you must migrate to a cloud storage provider like Amazon S3 using Active Storage or CarrierWave. Containers are transient; any data written to the local file system is lost when the container terminates.

Crafting the Dockerfile for a Rails Application

The Dockerfile is the blueprint for your application environment. For a large application, we should utilize multi-stage builds. Multi-stage builds allow us to compile native extensions using a full build environment, but only copy the finished artifacts into a smaller, runtime-only image. Smaller images deploy faster and reduce storage costs.

Here is a practical foundation for a Rails Dockerfile. Notice how we use two distinct FROM instructions:

# Build stage
FROM ruby:3.2.2-slim AS builder

WORKDIR /app

# Install system dependencies required for building gems
RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y build-essential libpq-dev nodejs

COPY Gemfile Gemfile.lock ./
# We install gems, ignoring development and test groups
RUN bundle install --without development test --jobs 4 --retry 3

# Final runtime stage
FROM ruby:3.2.2-slim

WORKDIR /app

# Install runtime dependencies only (notice we omit build-essential)
RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y postgresql-client tzdata nodejs && \
    rm -rf /var/lib/apt/lists/*

# Copy the built gems from the builder stage
COPY --from=builder /usr/local/bundle /usr/local/bundle
COPY . .

# Precompile assets
RUN RAILS_ENV=production SECRET_KEY_BASE=dummy bundle exec rake assets:precompile

EXPOSE 3000
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]

This approach separates the build tools from the runtime environment. Strictly speaking, you do not have to precompile assets within the Dockerfile. If you use an external CDN, you might choose to push these assets to S3 during your Continuous Integration (CI) pipeline instead, keeping the container image strictly focused on application logic.

Designing the AWS Infrastructure Architecture

Deploying Docker containers requires a robust network architecture. AWS ECS with Fargate operates within an Amazon Virtual Private Cloud (VPC). We must define subnets and security groups to manage traffic flow securely.

We recommend deploying your Fargate tasks into private subnets. This means your Rails application servers will not have public IP addresses and cannot be reached directly from the internet.

To route internet traffic to your application, we place an Application Load Balancer (ALB) in the public subnets. The ALB receives incoming HTTP and HTTPS requests, terminates the SSL connection, and forwards the traffic to your ECS tasks in the private subnets.

This geographic and network isolation significantly improves your security posture by limiting direct exposure to the public internet.

Configuring the ECS Task Definition and Service

In AWS ECS, a Task Definition acts as the configuration manual for your Docker container. It specifies which Docker image to use from your registry, how much CPU and memory to allocate, and which environment variables to inject.

For a monolithic Rails application, we typically require at least two distinct task definitions:

  1. The Web Task: This runs your application server (e.g., Puma or Unicorn).
  2. The Worker Task: This runs your background job processor (e.g., Sidekiq, DelayedJob, or Solid Queue).

Both tasks can usually use the exact same Docker image. We change the behavior by overriding the CMD executed when the container starts. For instance, the worker task might override the default command with ["bundle", "exec", "sidekiq"].

An ECS Service manages the lifecycle of these tasks. It ensures the specified number of task instances are running and automatically registers web tasks with your Application Load Balancer. When we deploy a new version of our application, the ECS Service orchestrates a rolling update, starting new containers and draining connections from the old ones to prevent downtime.

Managing Secrets and Database Migrations

Hardcoding API keys or database credentials into your Docker image is a critical security vulnerability. Instead, we should utilize AWS Systems Manager (SSM) Parameter Store or AWS Secrets Manager.

ECS Task Definitions integrate natively with these services. We can configure our task to pull specific secrets from SSM and inject them as environment variables at runtime. This ensures our sensitive data remains encrypted at rest and is only accessible to authorized containers.

Handling database migrations requires a deliberate strategy. We should avoid running rake db:migrate automatically when a web container boots. If multiple containers start simultaneously during a deployment, they could all attempt to run migrations at the same time, leading to race conditions and corrupted database state.

Instead, there are a few major approaches to migrations:

The first option is to run a standalone, one-off ECS task specifically for database migrations as part of your CI/CD pipeline, before updating the main ECS Service. The second option is to run migrations manually from a bastion host or local machine with access to the database.

Generally speaking, automating the standalone task in CI/CD is the preferred approach for reliable, repeatable deployments.

Trade-offs and Long-Term Implications

Transitioning to Docker and AWS ECS/Fargate requires careful consideration of the long-term operational impact.

The primary trade-off is architectural complexity. Managing VPCs, Load Balancers, IAM roles, and Task Definitions is significantly more involved than deploying to a managed PaaS. Your engineering team must establish a firm understanding of AWS networking and infrastructure-as-code tools, like Terraform or AWS CDK, to maintain this environment sustainably.

Furthermore, while Fargate removes the need to patch EC2 instances, it abstractly shifts the responsibility. We must actively monitor our container resource utilization. Over-provisioning CPU and memory in our Task Definitions will quickly negate any cost savings we anticipated from moving away from traditional infrastructure.

Ultimately, containerizing a Rails monolith with ECS and Fargate provides a highly tunable, scalable deployment platform. By methodically addressing configuration, security, and deployment pipelines, we establish a resilient foundation for long-term product development.

Sponsored by Durable Programming

Need help maintaining or upgrading your Ruby on Rails application? Durable Programming specializes in keeping Rails apps secure, performant, and up-to-date.

Hire Durable Programming