Rails 7: Default Dockerfile for Alpine Linux

Getting started with Rails on Docker has been made much easier with Rails 7.1, with the inclusion of a default Dockerfile. However, the default image that's built results in a large image (~730MB for a demo Rails app I spun up).

One of the benefits of the Alpine Linux images is that they are very minimal, resulting in much smaller images for the same application.

So, I set about rewriting the default Dockerfile that's generated by Rails to use the ruby:3.3.1-alpine3.18 image, and discovered a few trip-hazards along the way.

Getting Started

In this tutorial, we'll spin up a default Rails app (using SQLite as a database) and update the Dockerfile and requirements to use Alpine. To get started, create a new app and the traditional blog-post scaffold:

# versions: ruby = 3.3.1, rails ~> 7.1.3
$ rails new demo-app
$ cd demo-app
$ bin/rails g scaffold post title:string body:text
$ bin/rails db:migrate
$ bin/rails s

Open localhost:3000/posts to ensure that everything is running smoothly. At this stage, we can also create a new docker-compose.yml file to simplify building images and creating containers:

# docker-compose.yml
services:
  app:
    build: .
    platform: linux/amd64
    environment:
      - RAILS_ENV=development
    ports:
      - 3000:3000
    volumes:
      - .:/rails
    command: ["bin/rails", "server", "-b", "0.0.0.0"]
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/healthz"]
      interval: 30s
      timeout: 10s
      retries: 5

We can now build the default image for the application using docker compose, and see the resulting image is around 730MB in size.

$ docker compose build
$ docker images

# REPOSITORY     TAG     ...     SIZE
# demo-app-app   latest  ...     730MB

Switching to Alpine

Alpine Linux is a lightweight linux distribution, making it well-suited to Docker images.

To switch our application to use Alpine instead of the default Debian-based ruby image, we need to make some changes to Dockerfile base image, and the packages:

# Dockerfile
- FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim as base
+ FROM registry.docker.com/library/ruby:$RUBY_VERSION-alpine3.18 as base

  # Install packages needed to build gems
- RUN apt-get update -qq && \
-    apt-get install --no-install-recommends -y build-essential git libvips pkg-config
+ RUN apk add --update --no-cache build-base git vips sqlite-libs tzdata

  # Install packages needed for deployment
- RUN apt-get update -qq && \
-    apt-get install --no-install-recommends -y curl libsqlite3-0 libvips && \
-    rm -rf /var/lib/apt/lists /var/cache/apt/archives
+ RUN apk add --update --no-cache curl vips sqlite
# ...

# Run and own only the runtime files as a non-root user for security
- RUN useradd rails --create-home --shell /bin/bash && \
+ RUN adduser rails --disabled-password --shell /bin/ash && \

We also need to make a slight change to the default docker-entrypoint file, as bash is not available in Alpine. We'll use the default ash shell instead:

# /rails/bin/docker-entrypoint
- #!/bin/bash -e
+ #!/bin/ash -e

With the files updated, you can now build the new image and see the reduced image size (in this case, a reduction of around 69.5%):

$ docker compose build
$ docker images

# REPOSITORY     TAG     ...     SIZE
# demo-app-app   latest  ...     223MB

Next, restart your container using the new image:

$ docker compose up -d
$ open http://localhost:3000/posts/

You should now be presented with the posts index. Note that because our app is using the default sqlite3 database, the database file (storage/development.sqlite3) has been copied into the container. This is because the local directory is mounted into the container by Docker Compose.

For production, however, these database files won't be present. You can simulate this by removing the bind mount from docker-compose.yml:

# docker-compose.yml
services:
  app:
    # ...
    # volumes:     <- comment out these lines
    #  - .:/rails  <- 

Now recreate your Docker Compose stack:

$ docker compose up -d
$ open http://localhost:3000/

In development mode, you will now see the Pending Migration error, with an option to run the migration. Click the button to run the pending migration, and your app will continue to operate as normal.

Remember that when you remove your container (e.g. with docker compose down), then the database file will also be deleted. For this reason, it is recommended to revert the previous change to docker-compose.yml, which is fine for development and testing:

# docker-compose.yml
services:
  app:
    # ...
    volumes:
     - .:/rails
$ docker compose up -d

All done! Your rails app is now running on the much smaller Alpine linux base image.

👋 Thanks for reading - I hope you enjoyed this post. If you find it helpful and want to support further writing and tutorials like this one, please consider supporting my work with a coffee!

Support ☕️