Skip to content

Ruby Docker

# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
name: CI
on: push
# When an engineer pushes changes to a branch, any current build on that same branch is cancelled in place of the new one.
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
id-token: write
checks: write
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
# Set N number of parallel jobs you want to run tests on.
# Use higher number if you have slow tests to split them on more parallel jobs.
# Remember to update shardTotal below to 0..N-1
shardTotal: [4]
# set N-1 indexes for parallel jobs
# When you run 2 parallel jobs then first job will have index 0, the second job will have index 1 etc
shardIndex: [0, 1, 2, 3]
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Run RSpec tests
run: ./bin/ci
env:
CI_SHARD_TOTAL: ${{ matrix.shardTotal }}
CI_SHARD_INDEX: ${{ matrix.shardIndex }}
- name: Upload coverage
uses: actions/upload-artifact@v4
with:
name: coverage-${{ matrix.shardIndex }}-${{ matrix.shardTotal }}
path: coverage-${{ matrix.shardIndex }}-${{ matrix.shardTotal }}
include-hidden-files: true
if-no-files-found: error
merge-coverage:
name: Merge Test Coverage
runs-on: ubuntu-latest
needs: [test]
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Download all coverage artifacts from test jobs
uses: actions/download-artifact@v4
with:
pattern: coverage-*
- name: Merge coverage results
run: |
mkdir -p coverage
bundle exec ruby -e "
require 'simplecov';
require 'simplecov_json_formatter';
SimpleCov.collate Dir['coverage-*/.resultset.json'], 'rails' do
enable_coverage :branch
formatter SimpleCov::Formatter::MultiFormatter.new([
SimpleCov::Formatter::JSONFormatter,
SimpleCov::Formatter::HTMLFormatter
])
end
"
- name: Upload merged coverage
uses: actions/upload-artifact@v4
with:
name: coverage-merged
path: coverage
include-hidden-files: true
if-no-files-found: error
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
- run: npm ci
- run: RAILS_ENV=production bundle exec rake assets:precompile
- name: Cache Docker layers
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache-app
key: ${{ runner.os }}-buildx-app-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-app
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build production Docker image
run: |
docker buildx build \
--file Dockerfile \
--tag $APPLICATION_IMAGE_BUILD \
--build-arg CODE_VERSION="${{github.sha}}" \
--compress \
--target prod \
--load \
--cache-from type=local,src=/tmp/.buildx-cache-app \
--cache-to type=local,dest=/tmp/.buildx-cache-app-new,mode=max \
.
- name: APP::Move cache
run: |
rm -rf /tmp/.buildx-cache-app # Remove the old cache.
mv /tmp/.buildx-cache-app-new /tmp/.buildx-cache-app # Move the new cache to replace the old cache.
- name: Push docker image
if: github.actor != 'dependabot[bot]'
run: |
docker push $APPLICATION_IMAGE_BUILD
docker logout
#!/usr/bin/env ruby
tests = Dir["spec/**/*_spec.rb"]
tests = tests.
sort.
# Add randomization seed based on SHA of each commit
shuffle(random: Random.new(ENV["GITHUB_SHA"].to_i(16))).
select.
with_index do |el, i|
i % ENV["CI_SHARD_TOTAL"].to_i == ENV["CI_SHARD_INDEX"].to_i
end
exec "bundle exec rspec #{tests.join(" ")}"
.git/
tmp/
log/
node_modules/
spec/
.env*
coverage/
# STAGE: runtime
#
# Shares common runtime environment for all other stages. It should be used
# to define common environment variables and system packages.
ARG ruby_version="..."
ARG operating_system="alpine..."
FROM ruby:${ruby_version}-${operating_system} AS runtime
ENV LANG="C.UTF-8" \
BUNDLE_PATH="/usr/local/bundle" \
BUNDLE_DEPLOYMENT="1" \
BUNDLE_FROZEN="1" \
RAILS_LOG_TO_STDOUT="1" \
LD_PRELOAD="/usr/lib/libjemalloc.so.2"
# Runtime dependencies:
RUN apk add --update --no-cache \
curl \
tzdata \
bash \
libffi \
yaml \
jemalloc
WORKDIR "/app"
# STAGE: runtime-bundler
#
# Provides build-time environment to install Ruby gems
FROM runtime AS runtime-bundler
RUN apk add --update --no-cache \
curl-dev \
build-base \
libffi-dev \
yaml-dev
# STAGE: build-prod
#
# Provides Ruby gems used on production
FROM runtime-bundler AS build-prod
COPY Gemfile* .ruby-version ./
# Pin bundler to the exact version in Gemfile.lock to avoid mismatches
# RUN gem install bundler -v "$(grep -A 1 'BUNDLED WITH' Gemfile.lock | tail -1 | tr -d ' ')" && \
# gem cleanup bundler
# Install Ruby gems used in production environment.
RUN bundle config without development test && \
bundle config jobs $(nproc) && \
bundle install && \
rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \
bundle exec bootsnap precompile --gemfile -j 0
# STAGE: prod
#
# Provides runtime for the application in production mode.
FROM runtime AS prod
ARG USERNAME=...
RUN adduser -D -H ${USERNAME} ${USERNAME}
USER ${USERNAME}:${USERNAME}
ARG CODE_VERSION
ENV CODE_VERSION=$CODE_VERSION
COPY --chown=${USERNAME}:${USERNAME} --from=build-prod ${BUNDLE_PATH} ${BUNDLE_PATH}
COPY --chown=${USERNAME}:${USERNAME} . .
RUN bundle exec bootsnap precompile -j 0 app/ lib/
HEALTHCHECK --interval=10s --timeout=3s --start-period=30s --retries=3 \
CMD curl -f http://0.0.0.0/up || exit 1
ENTRYPOINT ["bundle", "exec"]
CMD ["puma", "-C", "config/puma.rb"]
# frozen_string_literal: true
source 'https://rubygems.org'
ruby file: '.ruby-version'
gem 'bootsnap', require: false

On the first run (e.g. in entrypoint or init container), you can dump the schema cache to avoid querying the database for schema on every boot:

Terminal window
bin/rails db:schema:cache:dump

This generates db/schema_cache.yml, which Rails loads automatically, skipping schema introspection queries at startup. Make sure schema cache loading is enabled in your database config:

config/database.yml
production:
schema_cache_path: db/schema_cache.yml
use_schema_cache_dump: true
  • jemalloc - reduces memory fragmentation and RSS bloat
  • autotuner - auto-tunes GC settings based on runtime profiling
  • mini_mime - lightweight MIME type lookup (~42 KB vs ~8.7 MB for mime-types)
  • bootsnap - caches require paths and compiled ISeq/YAML
  • oj - fast JSON parser/serializer, drop-in replacement. Add gem "oj" to Gemfile, then create an initializer:

    config/initializers/oj.rb
    Oj.optimize_rails

    This monkey-patches JSON.parse, JSON.generate, to_json, and Active Support’s JSON encoder to use Oj

  • fast_blank - C extension for String#blank?

  • Unchecked:

  • RUBY_YJIT_ENABLE=1 (Ruby 3.2+) — enables YJIT JIT compiler. Note: Rails 7.2+ enables YJIT by default at boot if available, so this env var is unnecessary for recent Rails apps
  • GC tuning via RUBY_GC_* env vars (or let autotuner handle it)
  • MALLOC_ARENA_MAX=2 if not using jemalloc — limits glibc memory arenas
  • It is possible to do warm-up via healthcheck. One can add DB warm-up as well, but there are downsides to a “heavy” healthcheck