diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..79af7b0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,19 @@ +* + +!/assets/ +!/bin/ +!/config/ +!/lib/ +!/public/ +!/script/console +!/script/server +!/views +!/.ruby-version +!/config.ru +!/Gemfile +!/Gemfile.lock +!/package.json +!/package-lock.json +!/Rakefile + +**/*.key diff --git a/.node-version b/.node-version new file mode 100644 index 0000000..8e35034 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +24.14.1 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b28db5c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,78 @@ +# syntax=docker/dockerfile:1 +# check=error=true + +# This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand: +# docker build --tag dev-training-web --build-arg RUBY_VERSION="$(cat .ruby-version)" --build-arg NODE_VERSION=$(cat .node-version) . +# docker run --interactive --tty --publish 80:80 --env MASTER_KEY="$(cat config/dev-training-web.key)" dev-training-web + +# Make sure RUBY_VERSION matches the Ruby version in .ruby-version +ARG RUBY_VERSION=OVERRIDE_ME +FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base + +# App lives here +WORKDIR /app + +# Install base packages +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y curl libjemalloc2 && \ + ln -s /usr/lib/$(uname -m)-linux-gnu/libjemalloc.so.2 /usr/local/lib/libjemalloc.so && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Set production environment variables and enable jemalloc for reduced memory usage and latency +ENV RACK_ENV="production" \ + BUNDLE_DEPLOYMENT="1" \ + BUNDLE_PATH="/usr/local/bundle" \ + BUNDLE_ONLY="default production" \ + NODE_ENV="production" \ + LD_PRELOAD="/usr/local/lib/libjemalloc.so" + + +# Throw-away build stage to reduce size of final image +FROM base AS build + +# Install packages needed to build gems +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y build-essential && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Install JavaScript dependencies +ARG NODE_VERSION=OVERRIDE_ME +ENV PATH=/usr/local/node/bin:$PATH +RUN curl -sL https://github.com/nodenv/node-build/archive/master.tar.gz | tar xz -C /tmp/ && \ + /tmp/node-build-master/bin/node-build "${NODE_VERSION}" /usr/local/node && \ + rm -rf /tmp/node-build-master + +# Install application gems +COPY .ruby-version Gemfile Gemfile.lock ./ + +RUN bundle install && \ + rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git + +# Install node modules +COPY package.json package-lock.json ./ +RUN npm ci + +# Copy application code +COPY . . + +# Precompiling assets for production without requiring secret RAILS_MASTER_KEY +RUN ./bin/rake assets:precompile + +RUN rm -rf node_modules + + +# Final stage for app image +FROM base + +# Copy built artifacts: gems, application +COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}" +COPY --from=build /app /app + +# Run and own only the runtime files as a non-root user for security +RUN groupadd --system --gid 1000 dev-training-web && \ + useradd dev-training-web --uid 1000 --gid 1000 --create-home --shell /bin/bash && \ + mkdir -p /app/log /app/tmp && chown -R dev-training-web:dev-training-web /app/log /app/tmp +USER 1000:1000 + +EXPOSE 80 +CMD ["script/server", "--port=80"] diff --git a/Gemfile.lock b/Gemfile.lock index a6894ff..f399c66 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -78,6 +78,9 @@ GEM net-http (~> 0.5) faraday-retry (2.4.0) faraday (~> 2.0) + google-protobuf (4.34.1-aarch64-linux-gnu) + bigdecimal + rake (~> 13.3) google-protobuf (4.34.1-arm64-darwin) bigdecimal rake (~> 13.3) @@ -123,6 +126,8 @@ GEM net-ssh (>= 5.0.0, < 8.0.0) net-ssh (7.3.0) nio4r (2.7.5) + nokogiri (1.19.1-aarch64-linux-gnu) + racc (~> 1.4) nokogiri (1.19.1-arm64-darwin) racc (~> 1.4) oauth2 (2.0.9) @@ -239,6 +244,8 @@ GEM sass-embedded (1.98.0) google-protobuf (~> 4.31) rake (>= 13) + sass-embedded (1.98.0-aarch64-linux-gnu) + google-protobuf (~> 4.31) sass-embedded (1.98.0-arm64-darwin) google-protobuf (~> 4.31) sawyer (0.9.2) @@ -292,6 +299,7 @@ GEM zeitwerk (2.7.5) PLATFORMS + aarch64-linux arm64-darwin arm64-darwin-22 @@ -362,6 +370,7 @@ CHECKSUMS faraday (2.14.1) sha256=a43cceedc1e39d188f4d2cdd360a8aaa6a11da0c407052e426ba8d3fb42ef61c faraday-net_http (3.4.2) sha256=f147758260d3526939bf57ecf911682f94926a3666502e24c69992765875906c faraday-retry (2.4.0) sha256=7b79c48fb7e56526faf247b12d94a680071ff40c9fda7cf1ec1549439ad11ebe + google-protobuf (4.34.1-aarch64-linux-gnu) sha256=f9c07607dc139c895f2792a7740fcd01cd94d4d7b0e0a939045b50d7999f0b1d google-protobuf (4.34.1-arm64-darwin) sha256=2745061f973119e6e7f3c81a0c77025d291a3caa6585a2cd24a25bbc7bedb267 haml (7.2.0) sha256=87fd2b71f7feab1724337b090a7d767f5ab2d42f08c974f3ead673f18cfcd55a haml_lint (0.72.0) sha256=23acb3f5db1602eb99bccec8009372465321702e1229017cca53272957bfc9c8 @@ -383,6 +392,7 @@ CHECKSUMS net-sftp (4.0.0) sha256=65bb91c859c2f93b09826757af11b69af931a3a9155050f50d1b06d384526364 net-ssh (7.3.0) sha256=172076c4b30ce56fb25a03961b0c4da14e1246426401b0f89cba1a3b54bf3ef0 nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1 + nokogiri (1.19.1-aarch64-linux-gnu) sha256=cfdb0eafd9a554a88f12ebcc688d2b9005f9fce42b00b970e3dc199587b27f32 nokogiri (1.19.1-arm64-darwin) sha256=dfe2d337e6700eac47290407c289d56bcf85805d128c1b5a6434ddb79731cb9e oauth2 (2.0.9) sha256=b21f9defcf52dc1610e0dfab4c868342173dcd707fd15c777d9f4f04e153f7fb octokit (10.0.0) sha256=82e99a539b7637b7e905e6d277bb0c1a4bed56735935cc33db6da7eae49a24e8 @@ -425,6 +435,7 @@ CHECKSUMS ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33 ruby2_keywords (0.0.5) sha256=ffd13740c573b7301cf7a2e61fc857b2a8e3d3aff32545d6f8300d8bae10e3ef sass-embedded (1.98.0) sha256=397dcd0071170f079eb97562a035fa113358af10e3ff759a57bc7eef763e9f94 + sass-embedded (1.98.0-aarch64-linux-gnu) sha256=019baa4ee850f9d6f3f53125fd4ee56b13ea5191bc8f8f9436825ec3e171b02d sass-embedded (1.98.0-arm64-darwin) sha256=fed4ee6de2f30b14e7a678ca648730aa78acc86be7a555e16ba3fdbc5e6aca69 sawyer (0.9.2) sha256=fa3a72d62a4525517b18857ddb78926aab3424de0129be6772a8e2ba240e7aca securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1 diff --git a/bin/bundle b/bin/bundle deleted file mode 100755 index 5b593cb..0000000 --- a/bin/bundle +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -# -# This file was generated by Bundler. -# -# The application 'bundle' is installed as part of a gem, and -# this file is here to facilitate running it. -# - -require "rubygems" - -m = Module.new do - module_function - - def invoked_as_script? - File.expand_path($0) == File.expand_path(__FILE__) - end - - def env_var_version - ENV["BUNDLER_VERSION"] - end - - def cli_arg_version - return unless invoked_as_script? # don't want to hijack other binstubs - return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` - bundler_version = nil - update_index = nil - ARGV.each_with_index do |a, i| - if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN - bundler_version = a - end - next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ - bundler_version = $1 - update_index = i - end - bundler_version - end - - def gemfile - gemfile = ENV["BUNDLE_GEMFILE"] - return gemfile if gemfile && !gemfile.empty? - - File.expand_path("../../Gemfile", __FILE__) - end - - def lockfile - lockfile = - case File.basename(gemfile) - when "gems.rb" then gemfile.sub(/\.rb$/, gemfile) - else "#{gemfile}.lock" - end - File.expand_path(lockfile) - end - - def lockfile_version - return unless File.file?(lockfile) - lockfile_contents = File.read(lockfile) - return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ - Regexp.last_match(1) - end - - def bundler_requirement - @bundler_requirement ||= - env_var_version || cli_arg_version || - bundler_requirement_for(lockfile_version) - end - - def bundler_requirement_for(version) - return "#{Gem::Requirement.default}.a" unless version - - bundler_gem_version = Gem::Version.new(version) - - requirement = bundler_gem_version.approximate_recommendation - - return requirement unless Gem.rubygems_version < Gem::Version.new("2.7.0") - - requirement += ".a" if bundler_gem_version.prerelease? - - requirement - end - - def load_bundler! - ENV["BUNDLE_GEMFILE"] ||= gemfile - - activate_bundler - end - - def activate_bundler - gem_error = activation_error_handling do - gem "bundler", bundler_requirement - end - return if gem_error.nil? - require_error = activation_error_handling do - require "bundler/version" - end - return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) - warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" - exit 42 - end - - def activation_error_handling - yield - nil - rescue StandardError, LoadError => e - e - end -end - -m.load_bundler! - -if m.invoked_as_script? - load Gem.bin_path("bundler", "bundle") -end diff --git a/bin/puma b/bin/puma deleted file mode 100755 index 01a92a3..0000000 --- a/bin/puma +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -# -# This file was generated by Bundler. -# -# The application 'puma' is installed as part of a gem, and -# this file is here to facilitate running it. -# - -ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) - -bundle_binstub = File.expand_path("bundle", __dir__) - -if File.file?(bundle_binstub) - if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") - load(bundle_binstub) - else - abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. -Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") - end -end - -require "rubygems" -require "bundler/setup" - -load Gem.bin_path("puma", "puma") diff --git a/bin/rake b/bin/rake index 9275675..9efbee9 100755 --- a/bin/rake +++ b/bin/rake @@ -8,20 +8,7 @@ # this file is here to facilitate running it. # -require "pathname" -ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", - Pathname.new(__FILE__).realpath) - -bundle_binstub = File.expand_path("../bundle", __FILE__) - -if File.file?(bundle_binstub) - if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ - load(bundle_binstub) - else - abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. -Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") - end -end +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) require "rubygems" require "bundler/setup" diff --git a/script/server b/script/server index a285218..ef73eac 100755 --- a/script/server +++ b/script/server @@ -1,12 +1,3 @@ -#!/usr/bin/env ruby +#!/usr/bin/env bash -# frozen_string_literal: true - -require 'fileutils' - -# path to your application root. -APP_ROOT = File.expand_path('..', __dir__) - -FileUtils.chdir APP_ROOT do - system 'bin/puma config.ru' -end +bundle exec puma config.ru "$@"