diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 555b75c1..73749998 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -26,7 +26,7 @@ jobs: uses: docker/build-push-action@v5 with: context: . - file: ./production.Dockerfile + file: ./Dockerfile push: false load: true tags: ${{ env.TEST_TAG }} diff --git a/Dockerfile b/Dockerfile index 84984dca..7c8ef0ad 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,22 @@ ARG RUBY_VERSION=3.2.2 FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim as base -# OS Level Dependencies +WORKDIR /rails + +# Set production environment +ENV RAILS_ENV="production" \ + BUNDLE_DEPLOYMENT="1" \ + BUNDLE_JOBS="4" \ + BUNDLE_WITHOUT="development:test" \ + BUNDLE_PATH="/usr/local/bundle" + + +# Throw-away build stage to reduce size of final image +FROM base as build + +# Install packages needed to build gems +# NOTE: This example project intentionally does not require or install node.js + RUN --mount=type=cache,target=/var/cache/apt \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ --mount=type=tmpfs,target=/var/log \ @@ -17,20 +32,55 @@ RUN --mount=type=cache,target=/var/cache/apt \ less \ git \ libpq-dev \ - postgresql-client \ libvips \ - curl + pkg-config -ENV LANG=C.UTF-8 \ - BUNDLE_JOBS=4 \ - BUNDLE_RETRY=3 - -RUN gem update --system && gem install bundler +# Install application gems +COPY Gemfile Gemfile.lock ./ +RUN bundle install && \ + rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \ + bundle exec bootsnap precompile --gemfile -WORKDIR /usr/src/app +# Copy application code +COPY . . -ENTRYPOINT ["./bin/docker-entrypoint"] +# Precompile bootsnap code for faster boot times +RUN bundle exec bootsnap precompile app/ lib/ -EXPOSE 3000 +# Precompiling assets for production without requiring secret RAILS_MASTER_KEY +RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile -CMD ["bundle", "exec", "rails", "s", "-b", "0.0.0.0"] + +# Final stage for app image +FROM base + +# Install packages needed for deployment +RUN --mount=type=cache,target=/var/cache/apt \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + --mount=type=tmpfs,target=/var/log \ + rm -f /etc/apt/apt.conf.d/docker-clean; \ + echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache; \ + apt-get update -qq \ + && apt-get install -yq --no-install-recommends \ + curl \ + postgresql-client \ + libvips + +# Copy built artifacts: gems, application +COPY --from=build /usr/local/bundle /usr/local/bundle +COPY --from=build /rails /rails + +# Run and own only the runtime files as a non-root user for security +RUN useradd rails --create-home --shell /bin/bash && \ + chown -R rails:rails db log storage tmp +USER rails:rails + +# Entrypoint prepares the database. +ENTRYPOINT ["/rails/bin/docker-entrypoint-production"] + +HEALTHCHECK --interval=15s --timeout=3s --start-period=0s --start-interval=5s --retries=3 \ + CMD curl -f http://localhost:3000/up || exit 1 + +# Start the server by default, this can be overwritten at runtime +EXPOSE 3000 +CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"] diff --git a/README.md b/README.md index ee4ba7af..06398e69 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ docker compose up --build ### with [BuildKit](https://docs.docker.com/build/buildkit/) ``` -DOCKER_BUILDKIT=1 docker build --tag rails-on-docker --file production.Dockerfile . --load +DOCKER_BUILDKIT=1 docker build --tag rails-on-docker --load . ``` Test the image can be used and Rails starts up, use a fake key for testing purposes only: diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint index 329394d3..1308e63f 100755 --- a/bin/docker-entrypoint +++ b/bin/docker-entrypoint @@ -1,11 +1,17 @@ -#!/bin/bash -set -e +#!/bin/bash -e -# Remove a potentially pre-existing server.pid for Rails. -rm -f /usr/src/app/tmp/pids/server.pid +echo "entrypoint" +echo "${*}" -echo "bundle install..." -bundle check || bundle install --jobs 4 +# NOTE: Enable this as you need to get migrations working in your deployed +# (i.e. Production) environments. Match the condition check to what you have in +# the Dockerfile CMD. I use puma in the production.Dockerfile as that's the +# recommended option by the Puma team (https://github.com/puma/puma#rails), but +# the default Rails Dockerfile uses `./bin/rails server` +# +# If running the rails server then create or migrate existing database +# if [ "${*}" == "./bin/rails server" ]; then +# ./bin/rails db:prepare +# fi -# Then exec the container's main process (what's set as CMD in the Dockerfile). -exec "$@" +exec "${@}" diff --git a/bin/docker-entrypoint-development b/bin/docker-entrypoint-development new file mode 100755 index 00000000..329394d3 --- /dev/null +++ b/bin/docker-entrypoint-development @@ -0,0 +1,11 @@ +#!/bin/bash +set -e + +# Remove a potentially pre-existing server.pid for Rails. +rm -f /usr/src/app/tmp/pids/server.pid + +echo "bundle install..." +bundle check || bundle install --jobs 4 + +# Then exec the container's main process (what's set as CMD in the Dockerfile). +exec "$@" diff --git a/bin/docker-entrypoint-production b/bin/docker-entrypoint-production deleted file mode 100755 index 74d0683c..00000000 --- a/bin/docker-entrypoint-production +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash -e - -echo "entrypoint" -echo "${*}" - -# NOTE: Enable this as you need to get migrations working in your deployed -# (i.e. Production) environments. Match the condition check to what you have in -# the Dockerfile CMD. I use puma in the production.Dockerfile as that's the -# recommended option by the Puma team (https://github.com/puma/puma#rails), but -# the default Rails Dockerfile uses `./bin/rails server` -# -# If running the rails server then create or migrate existing database -# if [ "${*}" == "./bin/rails server" ]; then -# ./bin/rails db:prepare -# fi - -# Run database migrations when deploying to Render. It is not great, maybe there's a better way? -# https://community.render.com/t/release-command-for-db-migrations/247/6 -if [ -z "$RENDER" ]; then echo "var is unset"; else bin/rails db:migrate; fi - -exec "${@}" diff --git a/compose.yaml b/compose.yaml index 5f4174ae..efa354b0 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,6 +1,8 @@ services: web: - build: . + build: + context: ./ + dockerfile: development.Dockerfile command: bash -c "rm -f tmp/pids/server.pid && bin/rails s -p 3000 -b '0.0.0.0'" volumes: - .:/usr/src/app diff --git a/development.Dockerfile b/development.Dockerfile new file mode 100644 index 00000000..23c71eb5 --- /dev/null +++ b/development.Dockerfile @@ -0,0 +1,36 @@ +# syntax=docker/dockerfile:1 + +# Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile +ARG RUBY_VERSION=3.2.2 +FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim as base + +# OS Level Dependencies +RUN --mount=type=cache,target=/var/cache/apt \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + --mount=type=tmpfs,target=/var/log \ + rm -f /etc/apt/apt.conf.d/docker-clean; \ + echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache; \ + apt-get update -qq \ + && apt-get install -yq --no-install-recommends \ + build-essential \ + gnupg2 \ + less \ + git \ + libpq-dev \ + postgresql-client \ + libvips \ + curl + +ENV LANG=C.UTF-8 \ + BUNDLE_JOBS=4 \ + BUNDLE_RETRY=3 + +RUN gem update --system && gem install bundler + +WORKDIR /usr/src/app + +ENTRYPOINT ["./bin/docker-entrypoint-development"] + +EXPOSE 3000 + +CMD ["bundle", "exec", "rails", "s", "-b", "0.0.0.0"] diff --git a/production.Dockerfile b/production.Dockerfile deleted file mode 100644 index 7c8ef0ad..00000000 --- a/production.Dockerfile +++ /dev/null @@ -1,86 +0,0 @@ -# syntax=docker/dockerfile:1 - -# Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile -ARG RUBY_VERSION=3.2.2 -FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim as base - -WORKDIR /rails - -# Set production environment -ENV RAILS_ENV="production" \ - BUNDLE_DEPLOYMENT="1" \ - BUNDLE_JOBS="4" \ - BUNDLE_WITHOUT="development:test" \ - BUNDLE_PATH="/usr/local/bundle" - - -# Throw-away build stage to reduce size of final image -FROM base as build - -# Install packages needed to build gems -# NOTE: This example project intentionally does not require or install node.js - -RUN --mount=type=cache,target=/var/cache/apt \ - --mount=type=cache,target=/var/lib/apt,sharing=locked \ - --mount=type=tmpfs,target=/var/log \ - rm -f /etc/apt/apt.conf.d/docker-clean; \ - echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache; \ - apt-get update -qq \ - && apt-get install -yq --no-install-recommends \ - build-essential \ - gnupg2 \ - less \ - git \ - libpq-dev \ - libvips \ - pkg-config - -# Install application gems -COPY Gemfile Gemfile.lock ./ -RUN bundle install && \ - rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \ - bundle exec bootsnap precompile --gemfile - -# Copy application code -COPY . . - -# Precompile bootsnap code for faster boot times -RUN bundle exec bootsnap precompile app/ lib/ - -# Precompiling assets for production without requiring secret RAILS_MASTER_KEY -RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile - - -# Final stage for app image -FROM base - -# Install packages needed for deployment -RUN --mount=type=cache,target=/var/cache/apt \ - --mount=type=cache,target=/var/lib/apt,sharing=locked \ - --mount=type=tmpfs,target=/var/log \ - rm -f /etc/apt/apt.conf.d/docker-clean; \ - echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache; \ - apt-get update -qq \ - && apt-get install -yq --no-install-recommends \ - curl \ - postgresql-client \ - libvips - -# Copy built artifacts: gems, application -COPY --from=build /usr/local/bundle /usr/local/bundle -COPY --from=build /rails /rails - -# Run and own only the runtime files as a non-root user for security -RUN useradd rails --create-home --shell /bin/bash && \ - chown -R rails:rails db log storage tmp -USER rails:rails - -# Entrypoint prepares the database. -ENTRYPOINT ["/rails/bin/docker-entrypoint-production"] - -HEALTHCHECK --interval=15s --timeout=3s --start-period=0s --start-interval=5s --retries=3 \ - CMD curl -f http://localhost:3000/up || exit 1 - -# Start the server by default, this can be overwritten at runtime -EXPOSE 3000 -CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]