diff --git a/README.md b/README.md index e42b380..e604246 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,15 @@ Run installers to add common frameworks and libraries to your Rails app, for exa bin/rails thermite:install:koi ``` +Available installers: + +```shell +bin/rails thermite:install:active_storage +bin/rails thermite:install:docker +bin/rails thermite:install:solid_cable +bin/rails thermite:install:solid_queue +``` + ## Contributing Bug reports and pull requests are welcome on GitHub at https://github.com/katalyst/thermite. diff --git a/lib/generators/thermite/install/docker/USAGE b/lib/generators/thermite/install/docker/USAGE new file mode 100644 index 0000000..dc8b78c --- /dev/null +++ b/lib/generators/thermite/install/docker/USAGE @@ -0,0 +1,8 @@ +Description: + Installs the Katalyst Docker container setup. + +Example: + bin/rails generate thermite:install:docker + + This creates a root Dockerfile, bin/docker-entrypoint, .dockerignore, + and removes the legacy Koi 4-era docker directory (if it exists) diff --git a/lib/generators/thermite/install/docker/docker_generator.rb b/lib/generators/thermite/install/docker/docker_generator.rb new file mode 100644 index 0000000..3ab3777 --- /dev/null +++ b/lib/generators/thermite/install/docker/docker_generator.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require "rails" +require "rails/generators" + +module Thermite + module Install + class DockerGenerator < Rails::Generators::Base + def self.source_root + File.expand_path("templates", __dir__) + end + + def copy_docker_files + template "Dockerfile.tt", "Dockerfile" + copy_file ".dockerignore" + copy_file "bin/docker-entrypoint" + chmod "bin/docker-entrypoint", 0o755 & ~File.umask, verbose: false + end + + def remove_legacy_docker_directory + remove_dir "docker" if Pathname(destination_root).join("docker").directory? + end + + private + + def application_name + if defined?(Rails.application) && Rails.application + Rails.application.class.module_parent_name.to_s.underscore.dasherize + else + File.basename(destination_root).underscore.dasherize + end + end + end + end +end diff --git a/lib/generators/thermite/install/docker/templates/.dockerignore b/lib/generators/thermite/install/docker/templates/.dockerignore new file mode 100644 index 0000000..9612375 --- /dev/null +++ b/lib/generators/thermite/install/docker/templates/.dockerignore @@ -0,0 +1,37 @@ +# See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files. + +# Ignore git directory. +/.git/ + +# Ignore bundler config. +/.bundle + +# Ignore all environment files (except templates). +/.env* +!/.env*.erb + +# Ignore all default key files. +/config/master.key +/config/credentials/*.key + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/.keep + +# Ignore storage (uploaded files in development and any SQLite databases). +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/.keep + +# Ignore assets. +/node_modules/ +/app/assets/builds/* +!/app/assets/builds/.keep +/public/assets diff --git a/lib/generators/thermite/install/docker/templates/Dockerfile.tt b/lib/generators/thermite/install/docker/templates/Dockerfile.tt new file mode 100644 index 0000000..00bb947 --- /dev/null +++ b/lib/generators/thermite/install/docker/templates/Dockerfile.tt @@ -0,0 +1,119 @@ +# syntax=docker/dockerfile:1 +# check=error=true + +ARG APPLICATION_NAME="<%= application_name %>" +ARG RUBY_VERSION=4.0 +FROM ruby:$RUBY_VERSION-slim AS base + +ARG PACKAGES="curl libvips libjemalloc2 postgresql-client" + +# Rails app lives here +WORKDIR /rails + +# Install base packages +RUN apt-get update -qq && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends $PACKAGES && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +ARG APPLICATION_NAME +ARG APPLICATION_VERSION +ARG APPLICATION_REVISION + +# Set production environment +ENV APPLICATION_NAME="${APPLICATION_NAME}" \ + BUNDLE_DEPLOYMENT="1" \ + BUNDLE_PATH="/usr/local/bundle" \ + BUNDLE_WITHOUT="development:test" \ + EXECJS_RUNTIME="Disabled" \ + RAILS_ENV="production" \ + PUMA_SSL="1" \ + TZ="Australia/Adelaide" + +# Create a rails group and user for non-privileged access +RUN groupadd --system --gid 1000 rails && \ + useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \ + echo "$APPLICATION_NAME ($APPLICATION_VERSION) [$APPLICATION_REVISION]" > /etc/motd && \ + echo "PS1='\u@\${APPLICATION_NAME}-\${RAILS_ENV}:\w\$ '" >> /etc/bash.bashrc && \ + echo "cat /etc/motd" >> /etc/bash.bashrc + +# Build rails gems and assets in a throw-away build stage to reduce size of final image +FROM base AS build + +# Install packages needed to build gems +ARG BUILD_PACKAGES="build-essential git libcurl4-openssl-dev libpq-dev libyaml-dev pkg-config zlib1g-dev" +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y $BUILD_PACKAGES && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# 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 --link . . + +# Precompile bootsnap code for faster boot times +RUN bundle exec bootsnap precompile app/ lib/ + +# If set, assets will be compiled into a subdirectory of public/ with this prefix +ARG CDN_ASSET_PREFIX="" + +# Precompile assets for production +RUN DATABASE_URL="postgresql://localhost" \ + SECRET_KEY_BASE_DUMMY=1 \ + ./bin/rails assets:precompile + +# END OF BUILD ## + +# Copy assets to the CDN +FROM amazon/aws-cli:latest AS cdn + +ARG AWS_DEFAULT_REGION="ap-southeast-2" +ARG CDN_ASSET_PREFIX="" +ARG S3_ASSETS_BUCKET="" + +COPY --from=build /rails/public public +RUN --mount=type=secret,id=aws-key-id,env=AWS_ACCESS_KEY_ID \ + --mount=type=secret,id=aws-secret-key,env=AWS_SECRET_ACCESS_KEY \ + --mount=type=secret,id=aws-session-token,env=AWS_SESSION_TOKEN \ + if [ -n "$CDN_ASSET_PREFIX" ] && [ -n "$S3_ASSETS_BUCKET" ]; then \ + aws s3 sync --only-show-errors --cache-control "public, max-age=31536000, immutable" \ + public/$CDN_ASSET_PREFIX s3://$S3_ASSETS_BUCKET/$CDN_ASSET_PREFIX; \ + fi + +# END OF CDN ## + +# Assemble everything into a single layer +FROM base AS assemble + +ARG APPLICATION_VERSION +ARG APPLICATION_REVISION + +COPY --link . . +COPY --from=build /rails/tmp/cache/bootsnap tmp/cache/bootsnap +COPY --from=cdn /aws/public public +RUN echo $APPLICATION_VERSION > VERSION && \ + echo $APPLICATION_REVISION > REVISION && \ + mkdir -p db log ssl storage tmp/pids tmp/sockets && \ + chown -R rails:rails db log ssl storage tmp + +# Final stage for app image +FROM base + +ARG CDN_ASSET_PREFIX + +# Puma and Rails configuration for a small server setup with puma in single mode with 5 workers +ENV CDN_ASSET_PREFIX=$CDN_ASSET_PREFIX \ + WEB_CONCURRENCY=0 \ + RAILS_MAX_THREADS=5 + +# Assemble layers +COPY --from=build --link "${BUNDLE_PATH}" "${BUNDLE_PATH}" +COPY --from=assemble --link /rails . + +USER 1000:1000 +EXPOSE 4443 +ENTRYPOINT ["/rails/bin/docker-entrypoint"] +CMD ["./bin/rails", "server"] diff --git a/lib/generators/thermite/install/docker/templates/bin/docker-entrypoint b/lib/generators/thermite/install/docker/templates/bin/docker-entrypoint new file mode 100644 index 0000000..e4f6b65 --- /dev/null +++ b/lib/generators/thermite/install/docker/templates/bin/docker-entrypoint @@ -0,0 +1,21 @@ +#!/bin/bash -e + +# Enable jemalloc for reduced memory usage and latency. +if [ -z "${LD_PRELOAD+x}" ]; then + LD_PRELOAD=$(find /usr/lib -name libjemalloc.so.2 -print -quit) + export LD_PRELOAD +fi + +# If running the rails server then create or migrate existing database +if [ "${@: -2:1}" == "./bin/rails" ] && [ "${@: -1:1}" == "server" ]; then + ./bin/rails db:prepare + + # create a self-signed certificate for puma + mkdir -p ~/.ssl + openssl req -x509 -newkey rsa:2048 -nodes -days 730 \ + -subj '/CN=docker/' \ + -keyout ~/.ssl/docker.key \ + -out ~/.ssl/docker.crt +fi + +exec "${@}" diff --git a/spec/generators/thermite/install/docker_generator_spec.rb b/spec/generators/thermite/install/docker_generator_spec.rb new file mode 100644 index 0000000..4e4fd5a --- /dev/null +++ b/spec/generators/thermite/install/docker_generator_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require "rails_helper" + +require "generators/thermite/install/docker/docker_generator" + +RSpec.describe Thermite::Install::DockerGenerator do + it "completes successfully" do + expect { run_generator }.not_to raise_error + end + + it "writes the Docker setup" do + run_generator + + assert_file "Dockerfile" + assert_file ".dockerignore" + assert_file "bin/docker-entrypoint" + end + + it "removes the legacy docker directory when present" do + mkdir_p File.join(destination_root, "docker") + + run_generator + + assert_no_file "docker" + end +end