Table of Contents
- Prerequisites
- Understanding Docker Basics
- Setting Up a Simple Ruby Project
- Creating a Dockerfile for Ruby
- Building and Running the Docker Image
- Development Workflow with Docker Compose
- Production-Ready Setup
- Troubleshooting Common Issues
- Conclusion
- References
Prerequisites
Before getting started, ensure you have the following installed:
- Docker Desktop: Available for Windows, macOS, and Linux. Docker Desktop includes Docker Engine, Docker CLI, and Docker Compose.
- Basic Command Line Knowledge: Familiarity with
cd,ls, andgit(optional but helpful). - Ruby Knowledge: Basic understanding of Ruby syntax and gems (Ruby’s package manager).
Understanding Docker Basics
If you’re new to Docker, let’s clarify a few key concepts:
- Image: A read-only template containing instructions to create a container. For Ruby, we’ll use pre-built images from Docker Hub.
- Container: A runnable instance of an image—like a lightweight, isolated “virtual machine” for your app.
- Dockerfile: A text file with instructions to build a custom Docker image (e.g., installing gems, copying app code).
- Docker Compose: A tool to define and run multi-container apps (e.g., a Ruby web app + PostgreSQL database).
Setting Up a Simple Ruby Project
Let’s start with a minimal Ruby application to demonstrate Docker setup. We’ll use Sinatra (a lightweight web framework) for simplicity, but the workflow applies to Rails, Rake tasks, or any Ruby app.
Step 1: Project Structure
Create a new directory for your project and navigate into it:
mkdir ruby-docker-demo && cd ruby-docker-demo
Your project will have these files:
ruby-docker-demo/
├── app.rb # Ruby application code
├── Gemfile # Gem dependencies
├── Gemfile.lock # Locked gem versions (auto-generated)
├── Dockerfile # Instructions to build the Docker image
└── docker-compose.yml # (Optional) For multi-container setups
Step 2: Define Dependencies with Gemfile
Create a Gemfile to specify your app’s dependencies. For a Sinatra app:
# Gemfile
source "https://rubygems.org"
gem "sinatra" # Web framework
gem "sinatra-reloader" # Auto-reload changes in development (optional)
Run bundle install locally to generate Gemfile.lock (this ensures Docker uses the exact gem versions):
bundle install
Note: You don’t need Ruby installed locally if you skip this step, but generating Gemfile.lock ensures reproducible builds.
Step 3: Write the Ruby App (app.rb)
Create a simple Sinatra app in app.rb:
# app.rb
require "sinatra"
require "sinatra/reloader" if development? # Auto-reload in development
get "/" do
"Hello, Ruby on Docker! 🐳"
end
# Run the app on port 4567 (Sinatra's default)
set :port, 4567
set :bind, "0.0.0.0" # Allow external access (required for Docker)
This app responds to GET / with a greeting and runs on port 4567.
Creating a Dockerfile for Ruby
A Dockerfile tells Docker how to build your image. Let’s break it down step by step.
Step 1: Choose a Ruby Base Image
Start with an official Ruby image from Docker Hub. Use tags to specify the Ruby version and image variant:
- Full Tag:
ruby:3.2.2(includes Ruby, system libraries, and build tools). - Slim Tag:
ruby:3.2.2-slim(smaller, removes non-essential tools). - Alpine Tag:
ruby:3.2.2-alpine(smallest, uses Alpine Linux; avoid if gems require native extensions).
For most apps, slim balances size and compatibility. Update your Dockerfile:
# Dockerfile
# Use the official Ruby 3.2.2 slim image as the base
FROM ruby:3.2.2-slim
Step 2: Set a Working Directory
Define a working directory inside the container where your app code will live:
# Set the working directory in the container
WORKDIR /app
Step 3: Copy Gems and Install Dependencies
Copy Gemfile and Gemfile.lock into the container first (this leverages Docker’s caching: if gems don’t change, Docker reuses the cached layer instead of re-installing gems every time you modify app.rb).
Add these lines to the Dockerfile:
# Copy Gemfiles to the container
COPY Gemfile Gemfile.lock ./
# Install system dependencies (if needed)
# Some gems require system libraries (e.g., pg for PostgreSQL needs libpq-dev)
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \ # Required to compile native gem extensions (e.g., nokogiri)
&& rm -rf /var/lib/apt/lists/* # Clean up to reduce image size
# Install gems
RUN bundle install --without development test # Skip dev/test gems in production
Why --without development test? In production, you won’t need gems like rspec or pry. Omit this flag in development.
Step 4: Copy Application Code
Copy the rest of your app code into the container:
# Copy the entire project into the container's working directory
COPY . .
Step 5: Expose Ports and Define the Startup Command
Expose the port your app runs on (Sinatra uses 4567 by default) and set the command to start the app:
# Expose port 4567 to the host machine
EXPOSE 4567
# Command to run the app
CMD ["ruby", "app.rb"]
Final Dockerfile
Putting it all together:
# Use an official Ruby runtime as the base image
FROM ruby:3.2.2-slim
# Set the working directory in the container
WORKDIR /app
# Install system dependencies (for native gem extensions)
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Copy Gemfiles and install gems
COPY Gemfile Gemfile.lock ./
RUN bundle install --without development test
# Copy the entire project into the container
COPY . .
# Expose the port the app runs on
EXPOSE 4567
# Start the application
CMD ["ruby", "app.rb"]
Building and Running the Docker Image
Now that your Dockerfile is ready, let’s build and run the image.
Step 1: Build the Docker Image
Use docker build to create an image from your Dockerfile. Tag it with a name (e.g., ruby-docker-demo):
docker build -t ruby-docker-demo .
-t ruby-docker-demo: Tags the image for easy reference..: Uses the current directory as the “build context” (Docker needs access toGemfile,app.rb, etc.).
The first build may take a few minutes as Docker downloads the base image and installs dependencies. Subsequent builds will be faster thanks to Docker’s layer caching.
Step 2: Run the Container
Start a container from your image with docker run. Map the container’s port 4567 to your host machine’s port 4567:
docker run -p 4567:4567 ruby-docker-demo
-p 4567:4567: Maps port4567on your host to port4567in the container.ruby-docker-demo: The name of the image to run.
Visit http://localhost:4567 in your browser—you’ll see:
Hello, Ruby on Docker! 🐳
Bonus: Customize the Run Command
-
Detached Mode: Run the container in the background with
-d:docker run -d -p 4567:4567 --name my-ruby-app ruby-docker-demoStop it later with
docker stop my-ruby-app. -
View Logs: Use
docker logs <container-name>:docker logs my-ruby-app -
Environment Variables: Pass variables with
-e:docker run -e "RACK_ENV=production" -p 4567:4567 ruby-docker-demo
Development Workflow with Docker Compose
For development, you’ll want to:
- Auto-reload code changes without rebuilding the image.
- Run additional services (e.g., PostgreSQL, Redis).
Docker Compose simplifies this with a docker-compose.yml file.
Step 1: Create docker-compose.yml
Add this file to your project:
# docker-compose.yml
version: "3.8"
services:
web:
build: . # Build from the current directory's Dockerfile
ports:
- "4567:4567" # Map host port 4567 to container port 4567
volumes:
- .:/app # Mount current directory into the container (auto-reload changes)
- bundle_data:/usr/local/bundle # Cache gems to avoid re-installing
environment:
- RACK_ENV=development # Tell Sinatra to use development mode
command: ruby app.rb # Override the Dockerfile's CMD (optional)
# Optional: Add a PostgreSQL database
db:
image: postgres:15 # Use PostgreSQL 15
environment:
- POSTGRES_USER=myuser
- POSTGRES_PASSWORD=mypassword
- POSTGRES_DB=mydb
volumes:
- postgres_data:/var/lib/postgresql/data # Persist database data
volumes:
bundle_data: # Named volume to cache gems
postgres_data: # Named volume to persist PostgreSQL data
Key Features Explained:
volumes: .:/app: Mounts your local project directory into the container. When you editapp.rb, the change is instantly reflected in the container (no rebuild needed!).bundle_dataVolume: Caches gems in a Docker-managed volume, sobundle installonly runs whenGemfilechanges.dbService: Adds a PostgreSQL container with persistent data (thanks topostgres_datavolume).
Step 2: Start the App with Docker Compose
Run all services (web + db) with:
docker-compose up
- Add
-dto run in detached mode:docker-compose up -d. - Rebuild the image (e.g., after changing
Dockerfile):docker-compose up --build. - Stop services:
docker-compose down(add-vto delete volumes, e.g.,docker-compose down -v).
Testing Auto-Reload
Edit app.rb (e.g., change the greeting to “Hello, Docker Compose! 🚢”) and refresh http://localhost:4567—the change will appear immediately (thanks to sinatra-reloader and the volume mount).
Production-Ready Setup
For production, optimize your Docker image for size, security, and reliability.
1. Multi-Stage Builds
Reduce image size by using a “builder” stage to compile gems, then copy only what’s needed to a smaller image.
Example Dockerfile for production:
# Stage 1: Build (install gems and compile native extensions)
FROM ruby:3.2.2-slim AS builder
WORKDIR /app
# Install build dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
libpq-dev \ # For PostgreSQL gems (e.g., pg)
&& rm -rf /var/lib/apt/lists/*
# Copy gems and install
COPY Gemfile Gemfile.lock ./
RUN bundle install --without development test
# Copy app code
COPY . .
# Stage 2: Production (smaller image, no build tools)
FROM ruby:3.2.2-slim
WORKDIR /app
# Install runtime dependencies (only what’s needed to run the app)
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq-dev \ # Keep if using PostgreSQL
&& rm -rf /var/lib/apt/lists/*
# Create a non-root user for security
RUN useradd -m appuser
USER appuser
# Copy gems and app code from the builder stage
COPY --from=builder /usr/local/bundle /usr/local/bundle
COPY --from=builder /app /app
# Set production environment
ENV RACK_ENV=production
EXPOSE 4567
CMD ["ruby", "app.rb"]
Why this works: The final image excludes build tools like build-essential, reducing size by ~50%.
2. Use Non-Root Users
Running containers as root is a security risk. The example above creates a appuser and switches to it with USER appuser.
3. Environment Variables
Avoid hardcoding secrets (e.g., database URLs) in Dockerfile or docker-compose.yml. Use environment variables or tools like Docker Secrets (for Docker Swarm) or Kubernetes Secrets (for Kubernetes).
Troubleshooting Common Issues
1. Gems Fail to Install
- Missing System Libraries: If a gem like
pg(PostgreSQL) fails to compile, install its system dependency (e.g.,libpq-devin Debian/Ubuntu). - Corrupted
Gemfile.lock: DeleteGemfile.lockand runbundle installlocally, then rebuild the image.
2. Port Already in Use
If you see Bind for 0.0.0.0:4567 failed: port is already allocated, another app is using port 4567. Change the host port mapping:
docker run -p 4568:4567 ruby-docker-demo # Use host port 4568 instead
3. Changes Not Reflected in Container
If edits to app.rb don’t show up:
- Ensure
volumes: .:/appis indocker-compose.yml. - Restart the container:
docker-compose restart web.
4. Database Connection Issues (e.g., with PostgreSQL)
- Use the service name as the hostname (e.g.,
dbfor the PostgreSQL container indocker-compose.yml). - Check if the database is ready: Use
depends_onwith a healthcheck (Docker Compose v3+):db: image: postgres:15 healthcheck: test: ["CMD-SHELL", "pg_isready -U myuser -d mydb"] interval: 5s timeout: 5s retries: 5 web: depends_on: db: condition: service_healthy
Conclusion
Docker simplifies Ruby development and deployment by ensuring consistency across environments. With a Dockerfile, you can package your app and dependencies into a portable image, and docker-compose lets you run multi-service apps (like Ruby + PostgreSQL) with a single command.
By following this guide, you’ve learned to:
- Build a Docker image for a Ruby app.
- Run and debug containers locally.
- Set up a development workflow with auto-reload.
- Optimize images for production with multi-stage builds and security best practices.
Start using Docker for your next Ruby project to eliminate “works on my machine” headaches!
References
- Official Ruby Docker Images - Docker Hub page for Ruby.
- Dockerfile Reference - Docker’s official guide to
Dockerfilesyntax. - Docker Compose Documentation - Learn more about multi-container setups.
- Sinatra Documentation - For building Ruby web apps.
- RubyGems Guide - Learn about
Gemfileandbundle.