Deploying a Ruby on Rails app with Docker is a great way to streamline development and production workflows. By Dockerizing your Rails app, you can ensure consistency across environments, simplify deployments, and reduce potential errors.
But how do you avoid common pitfalls and get the most out of Docker for your Rails application?
Ruby on Rails developers benefit immensely from Docker’s capabilities to handle environment setup, dependency management, and deployment. This guide explores the key steps for Rails Docker deployment, highlighting the best practices to follow for an efficient setup. No matter if you’re new to Docker or looking to refine your process, these tips will help you optimize performance and keep your app running smoothly in both development and production environments.
Deploying Ruby on Rails applications comes with challenges, such as managing Ruby, Node.js versions, and database dependencies (e.g., PostgreSQL). Docker simplifies this process by containerizing the application and bundling all its dependencies within a single portable unit.
A Docker container encapsulates everything needed to run the app, ensuring consistent behavior across local, staging, and production environments.
Rails Docker deployment offers following benefits:
1. Maintain consistency: Rails containerization provide an identical environment across different stages (development, testing, production).
2. Streamline environment setup: Every developer on your team can work within the same environment without configuring local machines individually.
3. Simplify scaling: Docker enables easy scaling of services like Redis, Sidekiq, and background jobs.
4. Enhance portability: Deploying across platforms (e.g., AWS, Heroku, on-premise servers) is seamless with Docker.
Before Docker: After Docker:
Now that we know what Dockerization is and why Rails containerization is necessary, let’s understand how you can set up Docker for Rails:
The first step in Dockerizing a Rails application is setting up a Dockerfile and Docker Compose to manage dependencies and services like databases and background workers.
A Dockerfile defines the environment in which your Rails application will run. Below is a basic Dockerfile for a Rails app:
# Use the official Ruby image as the base
FROM ruby:2.6.6 AS base
# Install system dependencies
RUN apt-get update -qq && \
apt-get install -y build-essential libpq-dev curl && \
curl -fsSL https://deb.nodesource.com/setup_16.x | bash - && \
apt-get install -y nodejs && \
npm install -g yarn && \
apt-get clean
# Set up the working directory
WORKDIR /app
# Copy the Gemfile and Gemfile.lock
COPY Gemfile Gemfile.lock ./
# Install gems
RUN gem install bundler -v 2.2.31
RUN bundle check || bundle install
# Copy the application code
COPY . ./
# Set up default environment variables
ENV RAILS_ENV development
Curious to know what has happened above? Well, here is the simple explanation of Rails Docker deployment:
● The official Ruby image is used as a base.
● Next, we installed system dependencies like libpq-dev (PostgreSQL libraries) and Node.js.
● Gems are installed using Bundler.
● The application code is copied into the Docker image.
● Environment variables are set to ensure Rails runs in the correct mode.
Rails applications often rely on multiple services, such as a database and background processing with Redis. Docker Compose simplifies the orchestration of these services by allowing them to be defined in a single docker-compose.yml file.
Here’s an example of a multi-service Docker Compose setup for a Rails app:
version: '3.8'
services:
app:
build:
context: .
target: base
ports:
- "3000:3000"
volumes:
- .:/app
depends_on:
- db
- redis
environment:
RAILS_ENV: development
DATABASE_HOST: db
DATABASE_USERNAME: postgres
DATABASE_PASSWORD: password
REDIS_HOST: redis
db:
image: postgres
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
redis:
image: redis
● App service: Defines the Rails app, mounts the local codebase, and exposes port 3000.
● Database service: Runs a PostgreSQL container with a specified username and password.
● Redis service: Handles Redis for background jobs or caching.
By using Docker Compose, you can spin up the entire application stack with a single command: docker-compose up.
To reduce build times, especially during development, Docker utilizes caching layers. A best practice is to copy the Gemfile early in the Dockerfile to minimize unnecessary rebuilds when code changes.
# Copy Gemfile first to utilize Docker layer caching
COPY Gemfile Gemfile.lock ./
RUN bundle install
This ensures that gems are only re-installed if the Gemfile changes, saving time during subsequent builds.
Learn advanced caching techniques for Ruby on Rails.
Environment variables are crucial for storing sensitive information like API keys or database credentials. Docker Compose allows you to define environment variables, but for production, it’s best to use Docker secrets or encrypted environment files.
Example for defining environment variables in docker-compose.yml:
services:
app:
environment:
RAILS_ENV: development
DATABASE_USERNAME: postgres
DATABASE_PASSWORD: password
For production, tools like dotenv or Docker's built-in secret management system should be used to securely manage sensitive data.
Docker Compose Rails offers multiple benefits. Scroll down to find the Docker for local development.
Docker helps prevent the infamous “works on my machine” problem by ensuring that the development environment mirrors the production environment. This eliminates inconsistencies that arise from developers using different local setups.
Key practices:
● Use volumes to mount the local code into the Docker container so changes are reflected immediately.
● Define services like PostgreSQL, Redis, and background workers in docker-compose.yml to simplify the setup process.
Example Docker Compose configuration for local development:
services:
app:
volumes:
- .:/app
command: "bin/rails server -b 0.0.0.0"
db:
image: postgres
redis:
image: redis
Here are the best practices you must consider for production deployment Docker Rails;
Multi-stage builds allow you to create efficient, slim production images by separating the build and runtime stages. For example, you can include build dependencies (like Node.js) in one stage and exclude them from the final production image.
FROM ruby:2.6.6 AS builder
# Install dependencies and build the app
FROM ruby:2.6.6 AS production
COPY --from=builder /app /app
This approach reduces the final image size and improves security by keeping only the essentials in the production environment.
For production, you can further minimize image size by excluding development and test gems:
RUN bundle install --without development test
Removing cache files and unnecessary libraries also helps to keep the image lean.
Production deployments require secure handling of sensitive data such as database credentials. Use Docker’s secret management capabilities or external secret management tools like AWS Secrets Manager for securely storing environment variables.
● Isolating Environments: Use different Docker Compose configurations (e.g., docker-compose.dev.yml vs docker-compose.prod.yml) for different environments.
● Database Migrations: Be sure to run migrations automatically upon starting the container in production (e.g., by using an entrypoint script).
● Monitoring and Logging: Ensure Docker logs are integrated with your application’s logging system for monitoring and debugging (use tools like the ELK stack).
Rails CI/CD Docker ensures consistency across development, testing, and production environments. Tools like GitHub Actions, CircleCI, and GitLab CI are commonly used for automating tests and deployment.
To set up a basic CI workflow with Docker in GitHub Actions:
name: CI
on:
push:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
services:
db:
image: postgres:13
ports:
- 5432:5432
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
steps:
- uses: actions/checkout@v2
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 2.7.7
- name: Install dependencies
run: |
gem install bundler
bundle install
- name: Set up the database
run: |
bin/rails db:create db:schema:load
- name: Run Tests
run: |
RAILS_ENV=test bin/rails test
The workflow sets up Ruby, installs dependencies, configures a PostgreSQL database, and runs the Rails test suite.
You can extend this workflow to automate deployment to production environments, ensuring your CI/CD pipeline is seamless and reliable.
Besides following all the practices for production deployment Docker Rails, make sure you don’t miss out on following information.
To speed up builds, make sure to leverage Docker’s layer caching. Docker caches layers during the build process, which can significantly speed up subsequent builds if nothing has changed in those layers.
Ensure that frequently-changing files (like the application code) are added later in the Dockerfile, and rarely-changing files (like Ruby gems) are added earlier. This way, Docker will only rebuild the final layers when you make changes to your code, and the earlier layers (which install dependencies) are cached.
For effective monitoring and debugging in a Dockerized environment, centralized logging is important. Instead of logging to local files, ensure your application logs to stdout/stderr, so Docker can capture and forward them to a logging system (such as ELK stack, Fluentd, or AWS CloudWatch).
# config/environments/production.rb
config.logger = Logger.new(STDOUT)
Docker allows you to define health checks to monitor the status of your containerized Rails application. This is useful in production environments where you want to ensure that your application is running correctly, and containers are restarted automatically if they become unresponsive.
For a Rails app, a typical health check might hit the /healthcheck endpoint, which could return a simple status:
services:
app:
build: .
ports:
- "3000:3000"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/healthcheck"]
interval: 30s
timeout: 10s
retries: 3
For performance, it's beneficial to store temporary data (such as cache or session data) in memory rather than on disk, especially in a containerized environment. Docker allows you to mount a tmpfs volume, which stores data directly in memory.
For example, in docker-compose.yml, you can add a tmpfs configuration for your temporary files:
services:
app:
tmpfs:
- /app/tmp/cache
This reduces disk I/O and can significantly improve the performance of your application.
Ensure you set resource limits on your containers to prevent a runaway container from consuming too much memory or CPU. In docker-compose.yml, you can specify resource limits like this:
services:
app:
build: .
ports:
- "3000:3000"
deploy:
resources:
limits:
cpus: "0.5"
memory: "512M"
This helps in optimizing resource usage and ensures your app doesn't impact the performance of other services running on the same host.
Security is crucial when deploying applications in Docker. Here are some security best practices for Rails apps in Docker:
RUN addgroup --system app && adduser --system --group app
USER app
Docker not only simplifies the deployment process for Ruby on Rails applications but also enhances productivity by reducing the complexity of managing environments and dependencies. By following these best practices, you’ll streamline your deployment pipeline and ensure a robust, scalable setup for your Rails app.
Work with future-proof technologies