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 diff --git a/Dockerfile b/Dockerfile index a96077a..80673fd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,9 @@ +# syntax=docker/dockerfile:1 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 \ @@ -12,31 +12,24 @@ RUN dpkg --add-architecture i386 \ libxext6 \ xfonts-75dpi \ xfonts-base \ - nodejs \ libc6:i386 \ libstdc++6:i386 \ chromium \ 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 - -# 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 +ARG BUNDLE_GEMFILE=/app/Gemfile -COPY . . +ENV BUNDLE_GEMFILE=${BUNDLE_GEMFILE} \ + BUNDLE_JOBS=4 \ + BUNDLE_RETRY=3 -COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh -RUN chmod +x /usr/local/bin/entrypoint.sh +COPY Gemfile Gemfile.lock Gemfile.next Gemfile.next.lock .ruby-version ./ +RUN --mount=type=cache,target=/usr/local/bundle/cache,sharing=locked \ + bundle install EXPOSE 3000 -ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"] 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: 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" "$@" diff --git a/docker-compose.yml b/docker-compose.yml index ff7f614..39223cc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,12 +34,10 @@ 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: + 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. @@ -53,12 +51,10 @@ 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 - web: - condition: service_started stdin_open: true tty: true