From db8adcbc18602a6687ddabce152fa593ca43f319 Mon Sep 17 00:00:00 2001 From: Mateus Pereira Date: Wed, 29 Jul 2026 09:35:43 -0300 Subject: [PATCH 01/14] Update ignored files by docker --- .dockerignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.dockerignore b/.dockerignore index b7edad4..87ed4ab 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,7 @@ .git -.circleci +.github +.DS_Store +coverage log/* tmp/* !log/.keep From d38d970fb59568fe5b3712b7cd68b29465142333 Mon Sep 17 00:00:00 2001 From: Mateus Pereira Date: Wed, 29 Jul 2026 09:37:02 -0300 Subject: [PATCH 02/14] Set docker syntax directive This just ensures BuildKit uses the latest version of Dockerfile syntax --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index a96077a..6bf78a8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,4 @@ +# syntax=docker/dockerfile:1 FROM ruby:4.0.4 RUN dpkg --add-architecture i386 \ From e2c8f6ecde190fe4dea391b14985df7a6a10e394 Mon Sep 17 00:00:00 2001 From: Mateus Pereira Date: Wed, 29 Jul 2026 09:40:19 -0300 Subject: [PATCH 03/14] Configure bundler settings in the environment This lets us drop the options in the command and also guarantees these options are set when running bundler commands inside the container. Using an absolute path for BUNDLE_GEMFILE also guarantees bundler commands work properly in subdirectories inside the container --- Dockerfile | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6bf78a8..09053ee 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,17 +20,21 @@ RUN dpkg --add-architecture i386 \ chromium-driver \ && rm -rf /var/lib/apt/lists/* -RUN gem install bundler -v 2.2.21 - WORKDIR /app -COPY Gemfile Gemfile.lock ./ -RUN bundle _2.2.21_ install --jobs=4 --retry=3 +ENV BUNDLE_GEMFILE=/app/Gemfile \ + BUNDLE_JOBS=4 \ + BUNDLE_RETRY=3 + +COPY Gemfile Gemfile.lock .ruby-version ./ +RUN --mount=type=cache,target=/usr/local/bundle/cache,sharing=locked \ + bundle install # Dual-boot: Gemfile.next targets the Rails version we're upgrading to. # Remove this block (and Gemfile.next / Gemfile.next.lock) once the upgrade lands. COPY Gemfile.next Gemfile.next.lock ./ -RUN BUNDLE_GEMFILE=Gemfile.next bundle _2.2.21_ install --jobs=4 --retry=3 +RUN --mount=type=cache,target=/usr/local/bundle/cache,sharing=locked \ + BUNDLE_GEMFILE=/app/Gemfile.next bundle install COPY . . From c55ab042b97c6360ff5be5ebd440f3874715f3bf Mon Sep 17 00:00:00 2001 From: Mateus Pereira Date: Wed, 29 Jul 2026 09:42:40 -0300 Subject: [PATCH 04/14] Copy entrypoint before app files This prevents cache busting the entrypoint copy on changes to the application code. --- Dockerfile | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 09053ee..2285774 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,10 +36,9 @@ COPY Gemfile.next Gemfile.next.lock ./ RUN --mount=type=cache,target=/usr/local/bundle/cache,sharing=locked \ BUNDLE_GEMFILE=/app/Gemfile.next bundle install -COPY . . +COPY --chmod=0755 docker/entrypoint.sh /usr/local/bin/entrypoint.sh -COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh -RUN chmod +x /usr/local/bin/entrypoint.sh +COPY . . EXPOSE 3000 From 79f1da930a6d0c845c0955e9ee65097524b49d47 Mon Sep 17 00:00:00 2001 From: Mateus Pereira Date: Wed, 29 Jul 2026 09:45:35 -0300 Subject: [PATCH 05/14] Tie web_next dependency on service_healthy service_started doesn't prevent race conditions since the web service may have started and db operations might still be working. Rails provides an `/up` endpoint which guarantees the _app_ is up. Only after that is it safe to run the entrypoint script --- Dockerfile | 3 +++ docker-compose.yml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 2285774..da01235 100644 --- a/Dockerfile +++ b/Dockerfile @@ -42,5 +42,8 @@ COPY . . EXPOSE 3000 +HEALTHCHECK --interval=10s --timeout=3s --start-period=60s --retries=6 \ + CMD curl -fsS http://localhost:3000/up || exit 1 + ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"] diff --git a/docker-compose.yml b/docker-compose.yml index ff7f614..2bc2370 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -58,7 +58,7 @@ services: db: condition: service_healthy web: - condition: service_started + condition: service_healthy stdin_open: true tty: true From 109794488d6cbe4db16abdec2d9708bc55e1743d Mon Sep 17 00:00:00 2001 From: Mateus Pereira Date: Wed, 29 Jul 2026 09:46:09 -0300 Subject: [PATCH 06/14] Use absolute path for BUNDLE_GEMFILE in web_next --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 2bc2370..7f5671b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -53,7 +53,7 @@ services: DATABASE_HOST: db DATABASE_USERNAME: postgres DATABASE_PASSWORD: postgres - BUNDLE_GEMFILE: Gemfile.next + BUNDLE_GEMFILE: /app/Gemfile.next depends_on: db: condition: service_healthy From f274c77caea67834efdfc76a0ba01a6311e8a6b0 Mon Sep 17 00:00:00 2001 From: Mateus Pereira Date: Wed, 29 Jul 2026 09:47:33 -0300 Subject: [PATCH 07/14] Build web_next image Attempting to reuse the web image presumes it already exists locally. Furthermore, it's better to have them build separately since their dependencies can be different --- docker-compose.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 7f5671b..b6b24aa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,12 +34,7 @@ services: tty: true web_next: - # Reuse the image built for `web` (compose tags it -web, i.e. - # audit-web) instead of building a second image. Only BUNDLE_GEMFILE - # differs, so a separate build is unnecessary. (Not using `extends` here: - # it merges array fields like `ports` instead of overriding them, which - # would leak web's 3000:3000 mapping into this service too.) - image: audit-web + build: . platform: linux/amd64 # Distinct pidfile: web and web_next share the same bind-mounted /app, so # they'd otherwise race on tmp/pids/server.pid and refuse to boot together. From 587017b305ed302c38e132d505d2226034c0f65f Mon Sep 17 00:00:00 2001 From: Mateus Pereira Date: Tue, 4 Aug 2026 09:13:44 -0300 Subject: [PATCH 08/14] Remove build-essential and nodejs packages --- Dockerfile | 2 -- 1 file changed, 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index da01235..73eafbb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,6 @@ FROM ruby:4.0.4 RUN dpkg --add-architecture i386 \ && apt-get update -qq \ && apt-get install -y --no-install-recommends \ - build-essential \ libpq-dev \ postgresql-client \ imagemagick \ @@ -13,7 +12,6 @@ RUN dpkg --add-architecture i386 \ libxext6 \ xfonts-75dpi \ xfonts-base \ - nodejs \ libc6:i386 \ libstdc++6:i386 \ chromium \ From 9f782228dfe2e447a03b982109c8baceda067c16 Mon Sep 17 00:00:00 2001 From: Mateus Pereira Date: Tue, 4 Aug 2026 09:14:17 -0300 Subject: [PATCH 09/14] Setup BUNDLE_GEMFILE as a build argument and environment variable --- Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 73eafbb..ca93207 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,9 @@ RUN dpkg --add-architecture i386 \ WORKDIR /app -ENV BUNDLE_GEMFILE=/app/Gemfile \ +ARG BUNDLE_GEMFILE=/app/Gemfile + +ENV BUNDLE_GEMFILE=${BUNDLE_GEMFILE} \ BUNDLE_JOBS=4 \ BUNDLE_RETRY=3 From 0f886e103d7f95502b70aa421319e24615ae6aec Mon Sep 17 00:00:00 2001 From: Mateus Pereira Date: Tue, 4 Aug 2026 09:14:47 -0300 Subject: [PATCH 10/14] Only copy the needed files for setup in the image --- Dockerfile | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/Dockerfile b/Dockerfile index ca93207..80673fd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,24 +26,10 @@ ENV BUNDLE_GEMFILE=${BUNDLE_GEMFILE} \ BUNDLE_JOBS=4 \ BUNDLE_RETRY=3 -COPY Gemfile Gemfile.lock .ruby-version ./ +COPY Gemfile Gemfile.lock Gemfile.next Gemfile.next.lock .ruby-version ./ RUN --mount=type=cache,target=/usr/local/bundle/cache,sharing=locked \ bundle install -# Dual-boot: Gemfile.next targets the Rails version we're upgrading to. -# Remove this block (and Gemfile.next / Gemfile.next.lock) once the upgrade lands. -COPY Gemfile.next Gemfile.next.lock ./ -RUN --mount=type=cache,target=/usr/local/bundle/cache,sharing=locked \ - BUNDLE_GEMFILE=/app/Gemfile.next bundle install - -COPY --chmod=0755 docker/entrypoint.sh /usr/local/bin/entrypoint.sh - -COPY . . - EXPOSE 3000 -HEALTHCHECK --interval=10s --timeout=3s --start-period=60s --retries=6 \ - CMD curl -fsS http://localhost:3000/up || exit 1 - -ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"] From e6aa44b5bf773b85c0ac952c2c989b8be90e6596 Mon Sep 17 00:00:00 2001 From: Mateus Pereira Date: Tue, 4 Aug 2026 09:15:30 -0300 Subject: [PATCH 11/14] Modify web_next Provides a build context and pass in the needed build args --- docker-compose.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index b6b24aa..8c45a2f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,7 +34,10 @@ services: tty: true web_next: - build: . + build: + context: . + args: + BUNDLE_GEMFILE: /app/Gemfile.next platform: linux/amd64 # Distinct pidfile: web and web_next share the same bind-mounted /app, so # they'd otherwise race on tmp/pids/server.pid and refuse to boot together. From 57049adf341e9bfd920b7607c498697f7b6b3f35 Mon Sep 17 00:00:00 2001 From: Mateus Pereira Date: Tue, 4 Aug 2026 09:15:50 -0300 Subject: [PATCH 12/14] Remove dependency on web from web_next --- docker-compose.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 8c45a2f..39223cc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -55,8 +55,6 @@ services: depends_on: db: condition: service_healthy - web: - condition: service_healthy stdin_open: true tty: true From ca65751a8af5a4823b559f721e17028f05814338 Mon Sep 17 00:00:00 2001 From: Mateus Pereira Date: Tue, 4 Aug 2026 09:16:01 -0300 Subject: [PATCH 13/14] Add docker scripts --- bin/docker/run | 10 +++++++++ bin/docker/setup | 55 ++++++++++++++++++++++++++++++++++++++++++++++++ bin/docker/start | 9 ++++++++ 3 files changed, 74 insertions(+) create mode 100644 bin/docker/run create mode 100755 bin/docker/setup create mode 100755 bin/docker/start diff --git a/bin/docker/run b/bin/docker/run new file mode 100644 index 0000000..592c693 --- /dev/null +++ b/bin/docker/run @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -e +cd "$(dirname "$0")/../.." +if [ "$(basename "$BUNDLE_GEMFILE")" = "Gemfile.next" ]; then + CONTAINER=web_next +else + CONTAINER=web +fi +exec docker compose run --rm "$CONTAINER" "$@" + diff --git a/bin/docker/setup b/bin/docker/setup new file mode 100755 index 0000000..0f18cdd --- /dev/null +++ b/bin/docker/setup @@ -0,0 +1,55 @@ +#!/usr/bin/env ruby +require "pathname" +require "fileutils" +include FileUtils + +# path to your application root. +APP_ROOT = Pathname.new File.expand_path("../..", __dir__) + +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + +CONTAINER = ENV["BUNDLE_GEMFILE"] == "Gemfile.next" ? "web_next" : "web" +DOCKER_PREFIX = if ENV["CI"] || ENV["RAILS_ENV"] == "test" + # run as the non-root CI user so files created by commands are owned correctly + "docker compose run --user 3434:3434 #{CONTAINER}" +else + # local development: run with the container default user + "docker compose run #{CONTAINER}" +end + +# explicit root-run prefix (used only when we must perform privileged actions) +DOCKER_ROOT_PREFIX = "docker compose run --user root #{CONTAINER}" + +chdir APP_ROOT do + # This script is a starting point to setup your application. + # Add necessary setup steps to this file. + + puts "== Copy .env ==" + # We use .env.local because we are using DotenvValidator and there's a known issue with docker-compose: [link](https://github.com/fastruby/dotenv_validator#if-you-use-docker-compose-read-this) + # The symlink is created because `.env.local` is not visible in testing environments but + # we don't want to maintain 2 separate files. This follows [this table](https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use) + unless File.exist?(".env.local") + cp ".env.sample", ".env.local" + system! "ln -s .env.local .env.test" + end + + puts "== Setup Database ==" + puts "\n== Copying sample files ==" + unless File.exist?("config/database.yml") + cp "config/database.yml.sample", "config/database.yml" + end + + puts "== Build images ==" + system! "docker compose build" + + # Some images / mounts can make the rails bin non-executable for the non-root user. + if ENV["CI"] + # run migrations as root to avoid exec permission issues, then restore ownership + system! "#{DOCKER_ROOT_PREFIX} rails db:create db:migrate" + system! "#{DOCKER_ROOT_PREFIX} sh -c 'chown -R 3434:3434 /code || true'" + else + system! "#{DOCKER_PREFIX} rails db:create db:migrate" + end +end diff --git a/bin/docker/start b/bin/docker/start new file mode 100755 index 0000000..3575151 --- /dev/null +++ b/bin/docker/start @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -e +cd "$(dirname "$0")/../.." +if [ "$(basename "$BUNDLE_GEMFILE")" = "Gemfile.next" ]; then + CONTAINER=web_next +else + CONTAINER=web +fi +exec docker compose up "$CONTAINER" "$@" From ca161bc033f38ec65d9134095a92f8c0cb4021cc Mon Sep 17 00:00:00 2001 From: Mateus Pereira Date: Tue, 4 Aug 2026 09:16:10 -0300 Subject: [PATCH 14/14] Update README instructions --- README.md | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 5274896..3bed386 100644 --- a/README.md +++ b/README.md @@ -12,17 +12,25 @@ You can see it working in https://audit.fastruby.io ## Getting started (Docker) -The easiest way to run the app locally is with Docker Compose, which builds the app image and a Postgres database for you: +First, run: - docker compose up --build +```bash +bin/docker/setup && \ +BUNDLE_GEMFILE=Gemfile.next bin/docker/setup +``` -This starts: +This will build both sets of images for each version of Rails. Then, to start the containers: -- `db` — Postgres 16 -- `web` — the app on http://localhost:3000, running against the default `Gemfile` (currently Rails 8.1) -- `web_next` — the same image, but with `BUNDLE_GEMFILE=Gemfile.next`, on http://localhost:3001 (see "Dual-boot Rails upgrades" below) +```bash +# For the current version of Rails +bin/docker/start -`docker/entrypoint.sh` copies `config/database.yml.sample` / `.env.sample` into place and runs `rails db:prepare` on boot, so no manual DB setup is needed. +# For the next version of Rails +BUNDLE_GEMFILE=Gemfile.next bin/docker/start +``` + +Keep in mind that if you change any of the dependencies of the application, you will need to +run the corresponding setup command. ## Getting started (without Docker) @@ -35,7 +43,7 @@ You should be able to go to http://localhost:3000 and see the landing page. Inside Docker: - docker compose run --rm -e RAILS_ENV=test -e DATABASE_HOST=db -e DATABASE_USERNAME=postgres -e DATABASE_PASSWORD=postgres web bin/rails test + bin/docker/run bin/rails test Without Docker: