├── .ruby-version ├── .github ├── workflows │ ├── automerge.yml │ └── ci.yml └── dependabot.yml ├── LICENSE ├── final └── Dockerfile └── README.md /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.4.7 2 | -------------------------------------------------------------------------------- /.github/workflows/automerge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: pull_request_target 3 | 4 | permissions: 5 | contents: write 6 | pull-requests: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.actor == 'dependabot[bot]' }} 12 | steps: 13 | - name: Dependabot metadata 14 | id: metadata 15 | uses: dependabot/fetch-metadata@v2.4.0 16 | with: 17 | github-token: '${{ secrets.PAT }}' 18 | 19 | - name: Enable auto-merge for Dependabot PRs 20 | if: ${{ steps.metadata.outputs.update-type != 'version-update:semver-major' }} 21 | run: gh pr merge --auto --squash "$PR_URL" 22 | env: 23 | PR_URL: ${{github.event.pull_request.html_url}} 24 | GITHUB_TOKEN: ${{secrets.PAT}} 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2025 Georg Ledermann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /final/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | # check=error=true 3 | 4 | FROM ruby:3.4.7-alpine 5 | LABEL maintainer="georg@ledermann.dev" 6 | 7 | # Add basic packages 8 | RUN apk add --no-cache \ 9 | brotli-libs \ 10 | gcompat \ 11 | jemalloc \ 12 | postgresql-client \ 13 | tzdata 14 | 15 | # Configure Rails 16 | ENV RAILS_LOG_TO_STDOUT=true \ 17 | RAILS_SERVE_STATIC_FILES=true \ 18 | RAILS_ENV=production 19 | 20 | WORKDIR /app 21 | 22 | # Expose Puma port 23 | EXPOSE 3000 24 | 25 | # Enable jemalloc for reduced memory usage and latency 26 | ENV LD_PRELOAD=/usr/lib/libjemalloc.so.2 27 | 28 | # Write GIT meta data from arguments to env vars 29 | ONBUILD ARG COMMIT_SHA 30 | ONBUILD ARG COMMIT_TIME 31 | ONBUILD ARG COMMIT_VERSION 32 | ONBUILD ARG COMMIT_BRANCH 33 | 34 | ONBUILD ENV COMMIT_SHA=${COMMIT_SHA} 35 | ONBUILD ENV COMMIT_TIME=${COMMIT_TIME} 36 | ONBUILD ENV COMMIT_VERSION=${COMMIT_VERSION} 37 | ONBUILD ENV COMMIT_BRANCH=${COMMIT_BRANCH} 38 | 39 | # Add user 40 | ONBUILD RUN addgroup -g 1000 -S app && \ 41 | adduser -u 1000 -S app -G app 42 | 43 | # Copy app with gems from former build stage 44 | ONBUILD COPY --from=builder --chown=app:app /usr/local/bundle/ /usr/local/bundle/ 45 | ONBUILD COPY --from=builder --chown=app:app /app /app 46 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: bundler 9 | directory: '/builder' 10 | schedule: 11 | interval: daily 12 | time: '01:00' 13 | timezone: Europe/Berlin 14 | open-pull-requests-limit: 10 15 | versioning-strategy: lockfile-only 16 | allow: 17 | - dependency-type: direct 18 | - dependency-type: indirect 19 | labels: 20 | - 'dependencies' 21 | - 'ruby' 22 | groups: 23 | rails: 24 | patterns: 25 | - 'actioncable' 26 | - 'actionmailbox' 27 | - 'actionmailer' 28 | - 'actionpack' 29 | - 'actiontext' 30 | - 'actionview' 31 | - 'activejob' 32 | - 'activemodel' 33 | - 'activerecord' 34 | - 'activestorage' 35 | - 'activesupport' 36 | - 'rails' 37 | - 'railties' 38 | - 'globalid' 39 | - 'i18n' 40 | - 'mail' 41 | - 'rack' 42 | - 'rackup' 43 | turbo: 44 | patterns: 45 | - 'turbo-rails' 46 | - 'stimulus-rails' 47 | 48 | - package-ecosystem: 'github-actions' 49 | directory: '/' 50 | schedule: 51 | interval: 'daily' 52 | time: '01:00' 53 | timezone: Europe/Berlin 54 | labels: 55 | - 'dependencies' 56 | - 'gh-action' 57 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build images 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | workflow_dispatch: 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | env: 16 | TAG_NAME: 3.4.7-alpine 17 | 18 | jobs: 19 | build: 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - name: Set up QEMU 24 | uses: docker/setup-qemu-action@v3 25 | with: 26 | platforms: linux/amd64,linux/arm64 27 | 28 | - name: Set up Docker Buildx 29 | uses: docker/setup-buildx-action@v3 30 | 31 | - name: Login to ghcr.io 32 | uses: docker/login-action@v3 33 | if: github.ref == 'refs/heads/main' 34 | with: 35 | registry: ghcr.io 36 | username: ${{ github.repository_owner }} 37 | password: ${{ secrets.GITHUB_TOKEN }} 38 | 39 | - name: Build and push BUILDER image 40 | uses: docker/build-push-action@v6 41 | with: 42 | context: '{{defaultContext}}:builder' 43 | platforms: linux/amd64,linux/arm64 44 | provenance: false 45 | push: ${{ github.ref == 'refs/heads/main' }} 46 | tags: | 47 | ghcr.io/${{ github.repository_owner }}/rails-base-builder:${{ env.TAG_NAME }} 48 | ghcr.io/${{ github.repository_owner }}/rails-base-builder:latest 49 | cache-from: type=gha 50 | cache-to: type=gha,mode=max 51 | 52 | - name: Build and push FINAL image 53 | uses: docker/build-push-action@v6 54 | with: 55 | context: '{{defaultContext}}:final' 56 | platforms: linux/amd64,linux/arm64 57 | provenance: false 58 | push: ${{ github.ref == 'refs/heads/main' }} 59 | tags: | 60 | ghcr.io/${{ github.repository_owner }}/rails-base-final:${{ env.TAG_NAME }} 61 | ghcr.io/${{ github.repository_owner }}/rails-base-final:latest 62 | cache-from: type=gha 63 | cache-to: type=gha,mode=max 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build images](https://github.com/ledermann/docker-rails-base/actions/workflows/ci.yml/badge.svg)](https://github.com/ledermann/docker-rails-base/actions/workflows/ci.yml) 2 | 3 | # DockerRailsBase 4 | 5 | Building Docker images usually takes a long time. This repo contains base images with preinstalled dependencies for [Ruby on Rails](https://rubyonrails.org/), so building a production image will be **2-3 times faster**. 6 | 7 | ## What? 8 | 9 | When using the official Ruby image, building a Docker image for a typical Rails application requires lots of time for installing dependencies - mainly OS packages, Ruby gems, Ruby gems with native extensions (Nokogiri etc.) and Node modules. This is required every time the app needs to be deployed to production. 10 | 11 | I was looking for a way to reduce this time, so I created base images that contain most of the dependencies used in my applications. 12 | 13 | And while I'm at it, I also moved as much as possible from the app-specific Dockerfile into the base image by using [ONBUILD](https://docs.docker.com/engine/reference/builder/#onbuild) triggers. This makes the Dockerfile in my apps small and simple. 14 | 15 | ## Performance 16 | 17 | I compared building times using a typical Rails application. This is the result on my local machine: 18 | 19 | - Based on official Ruby image: **4:50 min** 20 | - Based on DockerRailsBase: **1:57 min** 21 | 22 | As you can see, using DockerRailsBase is more than **2 times faster** compared to the official Ruby image. It saves nearly **3min** on every build. 23 | 24 | Note: Before I started timing, the base image was not available on my machine, so it was downloaded first, which took some time. If the base image is already available, the building time is only 1:18min (**3 times faster**). 25 | 26 | # Requirements 27 | 28 | This repo is based on the following assumptions: 29 | 30 | - Your Docker host is compatible with [Alpine Linux 3.22](https://www.alpinelinux.org/posts/Alpine-3.22.0-released.html), which requires Docker 20.10.0 or later 31 | - Your app is compatible with [Ruby 3.4 for Alpine Linux](https://github.com/docker-library/ruby/blob/master/3.4/alpine3.22/Dockerfile) 32 | - Your app uses Ruby on Rails 7.1 or later (including Rails 8.1) 33 | - Your app uses PostgreSQL, SQLite or MySQL/MariaDB 34 | - Your app installs Node modules with [Yarn](https://yarnpkg.com/) or [Bun](https://bun.sh/) (automatically detected) 35 | - Your app bundles JavaScript with `rails assets:precompile`. This works with [Vite Ruby](https://github.com/ElMassimo/vite_ruby), [Webpacker](https://github.com/rails/webpacker), [Asset pipeline (Sprockets)](https://github.com/rails/sprockets-rails) and others. 36 | 37 | If your project differs from this, I suggest to fork this project and create your own base image. 38 | 39 | ## How? 40 | 41 | It uses [multi-stage building](https://docs.docker.com/develop/develop-images/multistage-build/) to build a very small production image. There are two Dockerfiles in this repo, one for the first stage (called `builder`) and one for the resulting stage (called `final`). 42 | 43 | ### Builder stage 44 | 45 | The `builder` stage installs Ruby gems and Node modules. It also includes Git, Node.js and some build tools - all we need to compile assets. 46 | 47 | - Based on [ruby:3.4.7-alpine](https://github.com/docker-library/ruby/blob/master/3.4/alpine3.22/Dockerfile) 48 | - Adds packages needed for installing gems and compiling assets: Git, Node.js, PostgreSQL client and build tools 49 | - Adds some default Ruby gems (Rails 8.0 etc., see [Gemfile](./builder/Gemfile)) 50 | - Via ONBUILD triggers it installs missing gems and Node modules, then compiles the assets 51 | - Automatically detects whether to use Yarn or Bun based on lock files: 52 | - If `bun.lockb` or `bun.lock` exists: Installs Bun and uses it for package installation 53 | - If `yarn.lock` exists: Uses Yarn (via corepack) 54 | - If no lock file exists: Falls back to Yarn 55 | 56 | See [builder/Dockerfile](./builder/Dockerfile) 57 | 58 | ### Final stage 59 | 60 | The `final` stage builds the production image, which includes just the bare minimum. 61 | 62 | - Based on [ruby:3.4.7-alpine](https://github.com/docker-library/ruby/blob/master/3.4/alpine3.22/Dockerfile) 63 | - Adds packages needed for production: postgresql-client, tzdata, file 64 | - Via ONBUILD triggers it mainly copies the app and gems from the `builder` stage 65 | 66 | See [final/Dockerfile](./final/Dockerfile) 67 | 68 | ### Staying up-to-date 69 | 70 | Using [Dependabot](https://dependabot.com/), every updated Ruby gem results in an updated image. 71 | 72 | ### How to use for your Rails application 73 | 74 | #### Building the Docker image 75 | 76 | Add this `Dockerfile` to your application: 77 | 78 | ```Dockerfile 79 | FROM ghcr.io/ledermann/rails-base-builder:3.4.7-alpine AS builder 80 | FROM ghcr.io/ledermann/rails-base-final:3.4.7-alpine 81 | USER app 82 | # Optional: Enable YJIT 83 | # ENV RUBY_YJIT_ENABLE=1 84 | CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"] 85 | ``` 86 | 87 | Yes, this is the complete `Dockerfile` of your Rails app. It's so simple because the work is done by ONBUILD triggers. 88 | 89 | Now build the image: 90 | 91 | ```bash 92 | $ docker build . 93 | ``` 94 | 95 | #### Building the Docker image with BuildKit 96 | 97 | ``` 98 | docker buildx build . 99 | ``` 100 | 101 | You can use private npm/Yarn/Bun packages by mounting the config file: 102 | 103 | ``` 104 | docker buildx build --secret id=npmrc,src=$HOME/.npmrc . 105 | ``` 106 | 107 | or for Yarn: 108 | 109 | ``` 110 | docker buildx build --secret id=yarnrc,src=$HOME/.yarnrc.yml . 111 | ``` 112 | 113 | Note: For Bun, the `.npmrc` file is also used for authentication. 114 | 115 | In a similar way you can provide a configuration file for Bundler: 116 | 117 | ``` 118 | docker buildx build --secret id=bundleconfig,src=$HOME/.bundle/config . 119 | ``` 120 | 121 | #### Continuous integration (CI) 122 | 123 | Example to build the application's image with GitHub Actions and push it to the GitHub Container Registry: 124 | 125 | ```yaml 126 | deploy: 127 | runs-on: ubuntu-latest 128 | 129 | steps: 130 | - uses: actions/checkout@v4 131 | with: 132 | fetch-depth: 0 133 | 134 | - name: Fetch tag annotations 135 | # https://github.com/actions/checkout/issues/290 136 | run: git fetch --tags --force 137 | 138 | - name: Login to GitHub Container Registry 139 | uses: docker/login-action@v3 140 | with: 141 | registry: ghcr.io 142 | username: ${{ github.repository_owner }} 143 | password: ${{ secrets.GITHUB_TOKEN }} 144 | 145 | - name: Build the image 146 | run: | 147 | export COMMIT_TIME=$(git show -s --format=%cI ${GITHUB_SHA}) 148 | export COMMIT_SHA=${GITHUB_SHA} 149 | export COMMIT_VERSION=$(git describe) 150 | export COMMIT_BRANCH=$(git rev-parse --abbrev-ref HEAD) 151 | docker buildx build --build-arg COMMIT_TIME --build-arg COMMIT_SHA --build-arg COMMIT_VERSION --build-arg COMMIT_BRANCH -t ghcr.io/user/repo:latest . 152 | 153 | - name: Push the image 154 | run: docker push ghcr.io/user/repo:latest 155 | ``` 156 | 157 | ## Available Docker images 158 | 159 | Both Docker images (`builder` and `final`) are regularly published at ghcr.io and tagged with the current Ruby version: 160 | 161 | - https://github.com/ledermann/docker-rails-base/pkgs/container/rails-base-builder 162 | - https://github.com/ledermann/docker-rails-base/pkgs/container/rails-base-final 163 | 164 | Beware: The published images are **not** immutable. When a dependency (e.g. Ruby gem) is updated, the images will be republished using the **same** tag. 165 | 166 | When a new Ruby version comes out, a new tag is introduced and the images will be published using this tag and the former images will not be updated anymore. Here is a list of the tags that have been used in this repo so far: 167 | 168 | | Ruby version | Tag | First published | 169 | | ------------ | ------------ | --------------- | 170 | | 3.4.7 | 3.4.7-alpine | 2025-10-09 | 171 | | 3.4.6 | 3.4.6-alpine | 2025-09-17 | 172 | | 3.4.5 | 3.4.5-alpine | 2025-07-16 | 173 | | 3.4.4 | 3.4.4-alpine | 2025-05-16 | 174 | | 3.4.3 | 3.4.3-alpine | 2025-04-15 | 175 | | 3.4.2 | 3.4.2-alpine | 2025-02-16 | 176 | | 3.4.1 | 3.4.1-alpine | 2024-12-28 | 177 | | 3.3.6 | 3.3.6-alpine | 2024-11-06 | 178 | | 3.3.5 | 3.3.5-alpine | 2024-09-05 | 179 | | 3.3.4 | 3.3.4-alpine | 2024-07-10 | 180 | | 3.3.3 | 3.3.3-alpine | 2024-06-13 | 181 | | 3.3.2 | 3.3.2-alpine | 2024-05-31 | 182 | | 3.3.1 | 3.3.1-alpine | 2024-04-23 | 183 | | 3.3.0 | 3.3.0-alpine | 2023-12-27 | 184 | | 3.2.2 | 3.2.2-alpine | 2023-03-31 | 185 | | 3.2.1 | 3.2.1-alpine | 2023-02-10 | 186 | | 3.2.0 | 3.2.0-alpine | 2023-01-13 | 187 | | 3.1.3 | 3.1.3-alpine | 2022-11-26 | 188 | | 3.1.2 | 3.1.2-alpine | 2022-04-13 | 189 | | 3.1.1 | 3.1.1-alpine | 2022-02-19 | 190 | | 3.1.0 | 3.1.0-alpine | 2022-01-08 | 191 | | 3.0.3 | 3.0.3-alpine | 2021-11-24 | 192 | | 3.0.2 | 3.0.2-alpine | 2021-07-08 | 193 | | 3.0.1 | 3.0.1-alpine | 2021-04-06 | 194 | | 3.0.0 | 3.0.0-alpine | 2021-02-15 | 195 | | 2.7.2 | 2.7.2-alpine | 2020-10-10 | 196 | | 2.7.1 | 2.7.1-alpine | 2020-05-20 | 197 | | 2.6.6 | - | 2020-04-01 | 198 | | 2.6.5 | - | 2020-01-24 | 199 | 200 | The latest Docker images are also tagged as `latest`. However, it is not recommended to use this tag in your Rails application, because updating an app to a new Ruby version usually requires some extra work. 201 | 202 | ## FAQ 203 | 204 | ### Why not simply use layer caching? 205 | 206 | Docker supports layer caching, so for building images it performs just the needed steps: If there is a layer from a former build and nothing has changed, it will be used. But for dependencies, this means: If a single Ruby gem in the application was updated or added, the step with `bundle install` is run again, so **all** gems will be installed again. 207 | 208 | Using a prebuilt image improves installing dependencies a lot, because only the different/updated dependencies will be installed - all existing ones will be reused. 209 | 210 | ### What if my app requires slightly different dependencies? 211 | 212 | This doesn't matter: 213 | 214 | - A missing Alpine package can be installed with `apk add` inside your app's Dockerfile. 215 | - A missing Node module (or version) will be installed with `rails assets:precompile` via the ONBUILD trigger. 216 | - A missing Ruby gem (or version) will be installed with `bundle install` via the ONBUILD trigger. 217 | 218 | ### There are gems included that my app doesn't need. Will they bloat the resulting image? 219 | 220 | No. In the build stage there is a `bundle clean --force`, which uninstalls all gems not referenced in the app's Gemfile. 221 | 222 | ### My app does not need to compile assets (e.g. API only). Can I use this project? 223 | 224 | Yes! If your app doesn't have JavaScript dependencies, simply don't include a `package.json` file. The JavaScript installation will be automatically skipped. 225 | 226 | If your app doesn't compile assets at all (e.g. API-only), add this file to define a dummy task: 227 | 228 | ```ruby 229 | # lib/tasks/precompile.rake 230 | namespace :assets do 231 | desc 'Precompile assets' 232 | task precompile: :environment do 233 | puts 'No need to precompile assets' 234 | end 235 | end 236 | ``` 237 | 238 | Note: Apps using ImportMaps still need to run `assets:precompile` but don't require a `package.json` file. 239 | --------------------------------------------------------------------------------