From 5a7216233beaddf9b903f87b2896d88ee31e7cb3 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 25 Jul 2025 10:48:27 +0100 Subject: [PATCH 01/16] pin ruby/kamal version for kamal deploy --- deploy/.ruby-version | 1 + deploy/Gemfile | 4 +++ deploy/Gemfile.lock | 72 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+) create mode 100644 deploy/.ruby-version create mode 100644 deploy/Gemfile create mode 100644 deploy/Gemfile.lock diff --git a/deploy/.ruby-version b/deploy/.ruby-version new file mode 100644 index 000000000..f9892605c --- /dev/null +++ b/deploy/.ruby-version @@ -0,0 +1 @@ +3.4.4 diff --git a/deploy/Gemfile b/deploy/Gemfile new file mode 100644 index 000000000..aa43c464f --- /dev/null +++ b/deploy/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' +ruby '3.4.4' + +gem "kamal", "~> 2.7" diff --git a/deploy/Gemfile.lock b/deploy/Gemfile.lock new file mode 100644 index 000000000..5baa55c1c --- /dev/null +++ b/deploy/Gemfile.lock @@ -0,0 +1,72 @@ +GEM + remote: https://rubygems.org/ + specs: + activesupport (8.0.2) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + base64 (0.3.0) + bcrypt_pbkdf (1.1.1) + benchmark (0.4.1) + bigdecimal (3.2.2) + concurrent-ruby (1.3.5) + connection_pool (2.5.3) + dotenv (3.1.8) + drb (2.2.3) + ed25519 (1.4.0) + i18n (1.14.7) + concurrent-ruby (~> 1.0) + kamal (2.7.0) + activesupport (>= 7.0) + base64 (~> 0.2) + bcrypt_pbkdf (~> 1.0) + concurrent-ruby (~> 1.2) + dotenv (~> 3.1) + ed25519 (~> 1.4) + net-ssh (~> 7.3) + sshkit (>= 1.23.0, < 2.0) + thor (~> 1.3) + zeitwerk (>= 2.6.18, < 3.0) + logger (1.7.0) + minitest (5.25.5) + net-scp (4.1.0) + net-ssh (>= 2.6.5, < 8.0.0) + net-sftp (4.0.0) + net-ssh (>= 5.0.0, < 8.0.0) + net-ssh (7.3.0) + ostruct (0.6.3) + securerandom (0.4.1) + sshkit (1.24.0) + base64 + logger + net-scp (>= 1.1.2) + net-sftp (>= 2.1.2) + net-ssh (>= 2.8.0) + ostruct + thor (1.3.2) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + uri (1.0.3) + zeitwerk (2.7.3) + +PLATFORMS + arm64-darwin-24 + ruby + +DEPENDENCIES + kamal (~> 2.7) + +RUBY VERSION + ruby 3.4.4p34 + +BUNDLED WITH + 2.6.7 From 8f2eb4353b448fbc426b8fe3017026a623790aad Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 25 Jul 2025 14:01:09 +0100 Subject: [PATCH 02/16] pin nodejs version, and optimise Dockefile, the image size down from 4GB to 2.4GB --- .node-version | 1 + Dockerfile | 27 ++++++++++++++++++++++----- docker-compose.yml | 8 ++------ 3 files changed, 25 insertions(+), 11 deletions(-) create mode 100644 .node-version diff --git a/.node-version b/.node-version new file mode 100644 index 000000000..08b7109d0 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +18.20.8 diff --git a/Dockerfile b/Dockerfile index 5b18e7dcc..08cedd4f1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,21 +4,38 @@ # or SUS-ORS project. # Dockerfile -FROM ruby:3.2.5 +FROM ruby:3.2.5-slim # Rails and SAPI has some additional dependencies, e.g. rake requires a JS # runtime, so attempt to get these from apt, where possible # socat is just for binding ports within docker, not needed for the application -RUN apt-get update && apt-get install -y --force-yes \ +RUN apt-get update && apt-get install --no-install-recommends -y --force-yes \ + # ? libsodium-dev libgmp3-dev libssl-dev \ + # PSQL libpq-dev postgresql-client \ - nodejs \ + # node js + curl xz-utils \ + # For minio, local s3, development only. socat \ + # latex (huge file size) texlive-latex-base texlive-fonts-recommended texlive-fonts-extra texlive-latex-extra \ - ; + # Clean up + && rm -rf /var/lib/apt/lists/* # NB: Postgres client from Debian is 9.4 - not sure if this is acceptable -RUN mkdir /SAPI +# Install Node.js 18.20.8 manually +ARG NODE_VERSION=18.20.8 +ARG TARGETARCH +# Map Docker TARGETARCH to Node.js archive name +RUN case "$TARGETARCH" in \ + amd64) NODE_ARCH=x64 ;; \ + arm64) NODE_ARCH=arm64 ;; \ + *) echo "Unsupported architecture: $TARGETARCH"; exit 1 ;; \ + esac && \ + curl -fsSL https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.xz \ + | tar -xJ -C /usr/local --strip-components=1 + WORKDIR /SAPI # diff --git a/docker-compose.yml b/docker-compose.yml index 329955999..f54462985 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -68,9 +68,7 @@ services: rails: container_name: sapi-rails - build: - context: ./ - dockerfile: Dockerfile + build: . command: bundle exec rails server -p 3000 -b '0.0.0.0' volumes: &rails_volumes # Used for both rails and sidekiq @@ -175,9 +173,7 @@ services: sidekiq: container_name: sapi-sidekiq - build: - context: . - dockerfile: Dockerfile + build: . networks: - sapi depends_on: From 162b1ac07114fbd46710a62d9d69bf89560a0bba Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 25 Jul 2025 14:37:43 +0100 Subject: [PATCH 03/16] config.hosts and ALLOWED_HOSTS only use in local docker development. --- config/environments/production.rb | 1 - config/environments/staging.rb | 2 -- 2 files changed, 3 deletions(-) diff --git a/config/environments/production.rb b/config/environments/production.rb index d93986aa5..c700c15a1 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -116,7 +116,6 @@ # ] # Skip DNS rebinding protection for the default health check endpoint. # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } - config.hosts += ENV['ALLOWED_HOSTS'].split(',') if ENV['ALLOWED_HOSTS'].present? ### # Everything below are WCMC custom settings. diff --git a/config/environments/staging.rb b/config/environments/staging.rb index 6b20dc228..85a35f2c9 100644 --- a/config/environments/staging.rb +++ b/config/environments/staging.rb @@ -108,8 +108,6 @@ # ] # Skip DNS rebinding protection for the default health check endpoint. # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } - config.hosts += ENV['ALLOWED_HOSTS'].split(',') if ENV['ALLOWED_HOSTS'].present? - # Inserts middleware to perform automatic connection switching. # The `database_selector` hash is used to pass options to the DatabaseSelector From 5d8da2218e8649cc01e8d7dabc7ca58b0b42b765 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 25 Jul 2025 14:43:57 +0100 Subject: [PATCH 04/16] mailer config can't use Rails credentials: 1) should migrate to ENV so DevOps can make changes without developers; 2) assets:precompile without RAILS_MASTER_KEY not able to run coz failed to load credentials. --- config/environments/production.rb | 24 ++++++++---------------- config/environments/staging.rb | 24 ++++++++---------------- 2 files changed, 16 insertions(+), 32 deletions(-) diff --git a/config/environments/production.rb b/config/environments/production.rb index c700c15a1..3c38ce0d0 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -124,31 +124,23 @@ config.ember.variant = :production # Custom email settings - mailer_credentials = Rails.application.credentials[:mailer] - config.action_mailer.delivery_method = :smtp config.action_mailer.smtp_settings = { - address: mailer_credentials[:address], - port: mailer_credentials[:port], - domain: mailer_credentials[:domain], - user_name: mailer_credentials[:username], - password: mailer_credentials[:password], + address: "smtp.sendgrid.net", + port: 587, + domain: "unep-wcmc.org", + user_name: ENV['MAIL_USERNAME'], + password: ENV['MAIL_PASSWORD'], authentication: :login, enable_starttls_auto: true } config.action_mailer.default_url_options = { - host: mailer_credentials[:host] + host: "www.speciesplus.net" } - # fix for current version of mail gem: https://github.com/mikel/mail/issues/1538 - # config.action_mailer.delivery_method = :sendmail - # config.action_mailer.sendmail_settings = { - # location: '/usr/sbin/sendmail', arguments: ['-i'] - # } - config.action_mailer.default_options = { - from: mailer_credentials[:from], - reply_to: mailer_credentials[:from] + from: "no-reply@unep-wcmc.org", + reply_to: "no-reply@unep-wcmc.org" } end diff --git a/config/environments/staging.rb b/config/environments/staging.rb index 85a35f2c9..ab9c3621e 100644 --- a/config/environments/staging.rb +++ b/config/environments/staging.rb @@ -134,31 +134,23 @@ config.ember.variant = :production # Custom email settings - mailer_credentials = Rails.application.credentials[:mailer] - config.action_mailer.delivery_method = :smtp config.action_mailer.smtp_settings = { - address: mailer_credentials[:address], - port: mailer_credentials[:port], - domain: mailer_credentials[:domain], - user_name: mailer_credentials[:username], - password: mailer_credentials[:password], + address: "smtp.sendgrid.net", + port: 587, + domain: "unep-wcmc.org", + user_name: ENV['MAIL_USERNAME'], + password: ENV['MAIL_PASSWORD'], authentication: :login, enable_starttls_auto: true } config.action_mailer.default_url_options = { - host: mailer_credentials[:host] + host: "sapi.sapi-staging.linode.unep-wcmc.org" } - # fix for current version of mail gem: https://github.com/mikel/mail/issues/1538 - # config.action_mailer.delivery_method = :sendmail - # config.action_mailer.sendmail_settings = { - # location: '/usr/sbin/sendmail', arguments: ['-i'] - # } - config.action_mailer.default_options = { - from: mailer_credentials[:from], - reply_to: mailer_credentials[:from] + from: "no-reply@unep-wcmc.org", + reply_to: "no-reply@unep-wcmc.org" } end From a0fd60c975e01bbd2065e7a90c86171d5fb51458 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 25 Jul 2025 15:46:47 +0100 Subject: [PATCH 05/16] Things that can't raise error, when running assets:precompile without RAILS_MASTER_KEY. --- config/initializers/devise.rb | 2 +- config/initializers/schema_dump_options.rb | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 66d3f7fe7..afd5a0394 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -4,7 +4,7 @@ # The secret key used by Devise. Devise uses this key to generate # random tokens. Changing this key will render invalid all existing # confirmation, reset password and unlock tokens in the database. - config.secret_key = Rails.application.credentials.secret_key_base! + config.secret_key = Rails.application.credentials.secret_key_base # ==> Mailer Configuration # Configure the e-mail address which will be shown in Devise::Mailer, diff --git a/config/initializers/schema_dump_options.rb b/config/initializers/schema_dump_options.rb index 37c23a120..4a5323ef6 100644 --- a/config/initializers/schema_dump_options.rb +++ b/config/initializers/schema_dump_options.rb @@ -1,2 +1,4 @@ -ActiveRecord::SchemaDumper.ignore_tables << - ActiveRecord::Base.connection.data_sources.grep(/^trade_sandbox_\d+/) +if ENV['SECRET_KEY_BASE_DUMMY'].blank? + ActiveRecord::SchemaDumper.ignore_tables << + ActiveRecord::Base.connection.data_sources.grep(/^trade_sandbox_\d+/) +end From c3d3be07acfc989bed42d1d4926fb47be019c590 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 25 Jul 2025 15:57:41 +0100 Subject: [PATCH 06/16] Production Dockerfile and .dockerignore --- .dockerignore | 47 +++++++++++++++++++++++++ Dockerfile.deploy | 90 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 Dockerfile.deploy diff --git a/.dockerignore b/.dockerignore index e3cd043e9..541931e5d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -45,3 +45,50 @@ # Ignore Docker-related files /.dockerignore /Dockerfile* + +# SAPI +/coverage +/rdoc +/private +/public/system +/public/downloads/*.pdf +#LaTeX +/public/latex/*.aux +/public/latex/*.out +/public/latex/*.log +/public/latex/*.gz +/public/latex/index.pdf +/public/latex/history.pdf +/public/sitemap* + +#checklist downloads +/public/downloads/checklist/*.* + +#exports csvs +/public/downloads/documents/*.csv +/public/downloads/quotas/*.csv +/public/downloads/cites_listings/*.csv +/public/downloads/cites_suspensions/*.csv +/public/downloads/eu_listings/*.csv +/public/downloads/eu_decisions/*.csv +/public/downloads/cms_listings/*.csv +/public/downloads/checklist/*.pdf +/public/downloads/checklist/*.csv +/public/downloads/checklist/*.json +/public/downloads/taxon_concepts_names/*.csv +/public/downloads/synonyms_and_trade_names/*.csv +/public/downloads/taxon_concepts_distributions/*.csv +/public/downloads/shipments/*.csv +/public/downloads/comptab/*.csv +/public/downloads/gross_exports/*.csv +/public/downloads/gross_imports/*.csv +/public/downloads/net_exports/*.csv +/public/downloads/net_imports/*.csv +/public/downloads/trade_download_stats/*.csv +/public/downloads/species_reference_output/*.csv +/public/downloads/standard_reference_output/*.csv +/public/downloads/common_names/*.csv +/public/downloads/iucn_mappings/*.csv +/public/downloads/cms_mappings/*.csv +/public/downloads/orphaned_taxon_concepts/*.csv +/public/uploads/* diff --git a/Dockerfile.deploy b/Dockerfile.deploy new file mode 100644 index 000000000..369f38c5a --- /dev/null +++ b/Dockerfile.deploy @@ -0,0 +1,90 @@ +# syntax=docker/dockerfile:1 +# check=error=true + +# This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand. + +# For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html + +# Make sure RUBY_VERSION matches the Ruby version in .ruby-version +ARG RUBY_VERSION=3.2.5 +FROM ruby:$RUBY_VERSION-slim AS base + +# Rails app lives here +WORKDIR /rails + +# Install base packages +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y curl libjemalloc2 libpq-dev \ + # ? + libsodium-dev libgmp3-dev libssl-dev \ + # latex (huge file size) + texlive-latex-base texlive-fonts-recommended texlive-fonts-extra texlive-latex-extra \ + && rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Set production environment +ENV NODE_ENV="production" \ + RAILS_ENV="production" \ + BUNDLE_DEPLOYMENT="1" \ + BUNDLE_PATH="/usr/local/bundle" \ + BUNDLE_WITHOUT="development" + +# 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 git libyaml-dev pkg-config \ + # install node js + curl xz-utils \ + && rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Install Node.js 18.20.8 manually +ARG NODE_VERSION=18.20.8 +ARG TARGETARCH +# Map Docker TARGETARCH to Node.js archive name +RUN case "$TARGETARCH" in \ + amd64) NODE_ARCH=x64 ;; \ + arm64) NODE_ARCH=arm64 ;; \ + *) echo "Unsupported architecture: $TARGETARCH"; exit 1 ;; \ + esac && \ + curl -fsSL https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.xz \ + | tar -xJ -C /usr/local --strip-components=1 + +# 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 + +# Copy built artifacts: gems, application +COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}" +COPY --from=build /rails /rails + +# Run and own only the runtime files as a non-root user for security +RUN groupadd --system --gid 1000 rails && \ + useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \ + chown -R rails:rails db log tmp +USER 1000:1000 + +# Entrypoint prepares the database. +ENTRYPOINT ["/rails/bin/docker-entrypoint"] + +# Start server +EXPOSE 80 +# CMD ["./bin/rails", "server"] +CMD ["tail", "-f", "/dev/null"] From d353f2b61a0669a9d88f6bf2b17c5705c4e5d334 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 25 Jul 2025 17:08:28 +0100 Subject: [PATCH 07/16] Align ENV name with DevOps --- config/database.yml | 4 ++-- config/storage.yml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/config/database.yml b/config/database.yml index 3b51f642b..26026bfbb 100644 --- a/config/database.yml +++ b/config/database.yml @@ -6,7 +6,7 @@ default: &default host: <%= ENV.fetch("SAPI_DATABASE_HOST", Rails.application.credentials.dig(:db, :host)) %> username: <%= ENV.fetch("SAPI_DATABASE_USERNAME", Rails.application.credentials.dig(:db, :username)) %> password: <%= ENV.fetch("SAPI_DATABASE_PASSWORD", Rails.application.credentials.dig(:db, :password)) %> - port: <%= ENV.fetch("SAPI_DATABASE_PORT", Rails.application.credentials.dig(:db, :port)) %> + port: <%= ENV.fetch("SAPI_DATABASE_PORT", Rails.application.credentials.dig(:db, :port) || '5432') %> database: <%= ENV.fetch("SAPI_DATABASE_NAME", "sapi_#{Rails.env}") %> variables: # It is important that ordinary queries do not hang while waiting for a @@ -24,7 +24,7 @@ default: &default username: <%= ENV.fetch("CAPTIVE_BREEDING_DATABASE_USERNAME", Rails.application.credentials.dig(:captive_breeding_db, :username) || 'postgres') %> password: <%= ENV.fetch("CAPTIVE_BREEDING_DATABASE_PASSWORD", Rails.application.credentials.dig(:captive_breeding_db, :password)) %> port: <%= ENV.fetch("CAPTIVE_BREEDING_DATABASE_PORT", Rails.application.credentials.dig(:captive_breeding_db, :port) || '5432') %> - database: <%= ENV.fetch("CAPTIVE_BREEDING_DATABASE", Rails.application.credentials.dig(:captive_breeding_db, :database) || "captive_breeding_database_#{Rails.env}") %> + database: <%= ENV.fetch("CAPTIVE_BREEDING_DATABASE_NAME", Rails.application.credentials.dig(:captive_breeding_db, :database) || "captive_breeding_database_#{Rails.env}") %> database_tasks: false development: diff --git a/config/storage.yml b/config/storage.yml index 90da9d2bd..34b151869 100644 --- a/config/storage.yml +++ b/config/storage.yml @@ -14,9 +14,9 @@ local: # secret_access_key: xXXxxXXXx0Xx/XXxx000xxXX0XxX/xxx00X0xx0X amazon: &amazon service: S3 - access_key_id: <%= ENV.fetch("AWS_S3_ACCESS_KEY_ID") { Rails.application.credentials.dig(:storage, :aws, :access_key_id) } %> - secret_access_key: <%= ENV.fetch("AWS_S3_SECRET_ACCESS_KEY") { Rails.application.credentials.dig(:storage, :aws, :secret_access_key) } %> - region: <%= ENV.fetch("AWS_S3_REGION") { Rails.application.credentials.dig(:storage, :aws, :region) || 'eu-west-2' } %> + access_key_id: <%= ENV.fetch("AWS_ACCESS_KEY_ID") { Rails.application.credentials.dig(:storage, :aws, :access_key_id) } %> + secret_access_key: <%= ENV.fetch("AWS_SECRET_ACCESS_KEY") { Rails.application.credentials.dig(:storage, :aws, :secret_access_key) } %> + region: <%= ENV.fetch("AWS_REGION") { Rails.application.credentials.dig(:storage, :aws, :region) || 'eu-west-2' } %> bucket: <%= ENV.fetch("AWS_S3_BUCKET") { Rails.application.credentials.dig(:storage, :aws, :bucket) || "species-plus-#{Rails.env}" } %> local_s3: From c950332a160cbc58e6ece81f51732b8252f6c49b Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 25 Jul 2025 20:25:39 +0100 Subject: [PATCH 08/16] sidekiq admin username/password move to credentials. Was nginx .htpasswd. --- config/credentials/production.yml.enc | 2 +- config/credentials/staging.yml.enc | 2 +- config/environments/development.rb | 2 +- config/routes.rb | 16 ++++++++++++++++ 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/config/credentials/production.yml.enc b/config/credentials/production.yml.enc index bd1ef26c3..ed1380f72 100644 --- a/config/credentials/production.yml.enc +++ b/config/credentials/production.yml.enc @@ -1 +1 @@ -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--4qJJGNVpgQ3/1fEB--8mjgJXG+SInWCclUQkgTqA== \ No newline at end of file +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--3BxCon5UxLls1Gsi--aRhvTbWT82GnXRKdWvv4Cw== \ No newline at end of file diff --git a/config/credentials/staging.yml.enc b/config/credentials/staging.yml.enc index 76ab6ed9a..a1a494783 100644 --- a/config/credentials/staging.yml.enc +++ b/config/credentials/staging.yml.enc @@ -1 +1 @@ -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--m9FT/eiK2duMeoPd--TPeu8jM4DG29Rd1MfvgePw== \ No newline at end of file +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--Y3cbBcS9LVL6HEYv--ydc+IIWgrsJdp3h/Two1PA== \ No newline at end of file diff --git a/config/environments/development.rb b/config/environments/development.rb index f994d2d70..2e18abc54 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -24,7 +24,7 @@ config.action_controller.enable_fragment_cache_logging = true # Use a redis instance as a cache store in local development. - config.cache_store = :redis_cache_store, { url: ENV.fetch('SAPI_SIDEKIQ_REDIS_CACHE_URL') } + config.cache_store = :redis_cache_store, { url: ENV.fetch('SAPI_SIDEKIQ_REDIS_CACHE_URL', '') } config.public_file_server.headers = { 'Cache-Control' => "public, max-age=#{2.days.to_i}" } diff --git a/config/routes.rb b/config/routes.rb index ce44a452b..481f14a42 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,22 @@ require 'sidekiq/web' require 'sidekiq/cron/web' +if %w[test development].exclude?(Rails.env) + Sidekiq::Web.use Rack::Auth::Basic do |username, password| + # Protect against timing attacks: + # - See https://codahale.com/a-lesson-in-timing-attacks/ + # - See https://thisdata.com/blog/timing-attacks-against-string-comparison/ + # - Use & (do not use &&) so that it doesn't short circuit. + # - Use digests to stop length information leaking (see also ActiveSupport::SecurityUtils.variable_size_secure_compare) + sidekiq_username = Rails.application.credentials.sidekiq.username! + sidekiq_password = Rails.application.credentials.sidekiq.password! + ActiveSupport::SecurityUtils.secure_compare(Digest::SHA256.hexdigest(username), + Digest::SHA256.hexdigest(sidekiq_username)) & + ActiveSupport::SecurityUtils.secure_compare(Digest::SHA256.hexdigest(password), + Digest::SHA256.hexdigest(sidekiq_password)) + end +end + Rails.application.routes.draw do # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. # Can be used by load balancers and uptime monitors to verify that the app is live. From 92d816a635d48ed0b6b021ed1e6418669e00e540 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 25 Jul 2025 20:48:22 +0100 Subject: [PATCH 09/16] =?UTF-8?q?Remove=20the=20bundle=20install=20command?= =?UTF-8?q?=20from=20the=20entrypoint.=20In=20staging=20and=20production,?= =?UTF-8?q?=20it=E2=80=99s=20already=20run=20when=20the=20Docker=20image?= =?UTF-8?q?=20is=20built;=20Move=20it=20into=20the=20Docker=20Compose=20co?= =?UTF-8?q?nfiguration=20for=20development=20only.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/docker-entrypoint | 2 -- docker-compose.yml | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint index cd970544f..5c404a181 100755 --- a/bin/docker-entrypoint +++ b/bin/docker-entrypoint @@ -4,8 +4,6 @@ if [[ "${@}" =~ "rails server" ]]; then rm -f ./tmp/pids/server.pid; fi -bundle install - mkdir -p {./,spec/}public/downloads/checklist mkdir -p {./,spec/}public/downloads/cites_listings mkdir -p {./,spec/}public/downloads/cites_suspensions diff --git a/docker-compose.yml b/docker-compose.yml index f54462985..ca05afb0d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -69,7 +69,7 @@ services: rails: container_name: sapi-rails build: . - command: bundle exec rails server -p 3000 -b '0.0.0.0' + command: bundle install && bundle exec rails server -p 3000 -b '0.0.0.0' volumes: &rails_volumes # Used for both rails and sidekiq From acb3d4d40c8d30fd7e5ea82fb9c6a6ff642db8e5 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 25 Jul 2025 20:49:42 +0100 Subject: [PATCH 10/16] kamal 2 config --- .kamal/hooks/docker-setup.sample | 3 + .kamal/hooks/post-app-boot.sample | 3 + .kamal/hooks/post-deploy.sample | 14 +++ .kamal/hooks/post-proxy-reboot.sample | 3 + .kamal/hooks/pre-app-boot.sample | 3 + .kamal/hooks/pre-build | 51 +++++++++++ .kamal/hooks/pre-connect.sample | 47 ++++++++++ .kamal/hooks/pre-deploy.sample | 122 ++++++++++++++++++++++++++ .kamal/hooks/pre-proxy-reboot.sample | 3 + .kamal/secrets-common | 39 ++++++++ Dockerfile | 2 + config/database.yml.sample | 29 ------ config/deploy.production.yml | 36 ++++++++ config/deploy.staging.yml | 35 ++++++++ config/deploy.yml | 48 ++++++++++ 15 files changed, 409 insertions(+), 29 deletions(-) create mode 100755 .kamal/hooks/docker-setup.sample create mode 100755 .kamal/hooks/post-app-boot.sample create mode 100755 .kamal/hooks/post-deploy.sample create mode 100755 .kamal/hooks/post-proxy-reboot.sample create mode 100755 .kamal/hooks/pre-app-boot.sample create mode 100755 .kamal/hooks/pre-build create mode 100755 .kamal/hooks/pre-connect.sample create mode 100755 .kamal/hooks/pre-deploy.sample create mode 100755 .kamal/hooks/pre-proxy-reboot.sample create mode 100644 .kamal/secrets-common delete mode 100644 config/database.yml.sample create mode 100644 config/deploy.production.yml create mode 100644 config/deploy.staging.yml create mode 100644 config/deploy.yml diff --git a/.kamal/hooks/docker-setup.sample b/.kamal/hooks/docker-setup.sample new file mode 100755 index 000000000..2fb07d7d7 --- /dev/null +++ b/.kamal/hooks/docker-setup.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Docker set up on $KAMAL_HOSTS..." diff --git a/.kamal/hooks/post-app-boot.sample b/.kamal/hooks/post-app-boot.sample new file mode 100755 index 000000000..70f9c4bc9 --- /dev/null +++ b/.kamal/hooks/post-app-boot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Booted app version $KAMAL_VERSION on $KAMAL_HOSTS..." diff --git a/.kamal/hooks/post-deploy.sample b/.kamal/hooks/post-deploy.sample new file mode 100755 index 000000000..fd364c2a7 --- /dev/null +++ b/.kamal/hooks/post-deploy.sample @@ -0,0 +1,14 @@ +#!/bin/sh + +# A sample post-deploy hook +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLES (if set) +# KAMAL_DESTINATION (if set) +# KAMAL_RUNTIME + +echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds" diff --git a/.kamal/hooks/post-proxy-reboot.sample b/.kamal/hooks/post-proxy-reboot.sample new file mode 100755 index 000000000..1435a677f --- /dev/null +++ b/.kamal/hooks/post-proxy-reboot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Rebooted kamal-proxy on $KAMAL_HOSTS" diff --git a/.kamal/hooks/pre-app-boot.sample b/.kamal/hooks/pre-app-boot.sample new file mode 100755 index 000000000..45f735504 --- /dev/null +++ b/.kamal/hooks/pre-app-boot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Booting app version $KAMAL_VERSION on $KAMAL_HOSTS..." diff --git a/.kamal/hooks/pre-build b/.kamal/hooks/pre-build new file mode 100755 index 000000000..c5a55678b --- /dev/null +++ b/.kamal/hooks/pre-build @@ -0,0 +1,51 @@ +#!/bin/sh + +# A sample pre-build hook +# +# Checks: +# 1. We have a clean checkout +# 2. A remote is configured +# 3. The branch has been pushed to the remote +# 4. The version we are deploying matches the remote +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLES (if set) +# KAMAL_DESTINATION (if set) + +if [ -n "$(git status --porcelain)" ]; then + echo "Git checkout is not clean, aborting..." >&2 + git status --porcelain >&2 + exit 1 +fi + +first_remote=$(git remote) + +if [ -z "$first_remote" ]; then + echo "No git remote set, aborting..." >&2 + exit 1 +fi + +current_branch=$(git branch --show-current) + +if [ -z "$current_branch" ]; then + echo "Not on a git branch, aborting..." >&2 + exit 1 +fi + +remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1) + +if [ -z "$remote_head" ]; then + echo "Branch not pushed to remote, aborting..." >&2 + exit 1 +fi + +if [ "$KAMAL_VERSION" != "$remote_head" ]; then + echo "Version ($KAMAL_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2 + exit 1 +fi + +exit 0 diff --git a/.kamal/hooks/pre-connect.sample b/.kamal/hooks/pre-connect.sample new file mode 100755 index 000000000..77744bdca --- /dev/null +++ b/.kamal/hooks/pre-connect.sample @@ -0,0 +1,47 @@ +#!/usr/bin/env ruby + +# A sample pre-connect check +# +# Warms DNS before connecting to hosts in parallel +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLES (if set) +# KAMAL_DESTINATION (if set) +# KAMAL_RUNTIME + +hosts = ENV["KAMAL_HOSTS"].split(",") +results = nil +max = 3 + +elapsed = Benchmark.realtime do + results = hosts.map do |host| + Thread.new do + tries = 1 + + begin + Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME) + rescue SocketError + if tries < max + puts "Retrying DNS warmup: #{host}" + tries += 1 + sleep rand + retry + else + puts "DNS warmup failed: #{host}" + host + end + end + + tries + end + end.map(&:value) +end + +retries = results.sum - hosts.size +nopes = results.count { |r| r == max } + +puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ] diff --git a/.kamal/hooks/pre-deploy.sample b/.kamal/hooks/pre-deploy.sample new file mode 100755 index 000000000..05b3055b7 --- /dev/null +++ b/.kamal/hooks/pre-deploy.sample @@ -0,0 +1,122 @@ +#!/usr/bin/env ruby + +# A sample pre-deploy hook +# +# Checks the Github status of the build, waiting for a pending build to complete for up to 720 seconds. +# +# Fails unless the combined status is "success" +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_COMMAND +# KAMAL_SUBCOMMAND +# KAMAL_ROLES (if set) +# KAMAL_DESTINATION (if set) + +# Only check the build status for production deployments +if ENV["KAMAL_COMMAND"] == "rollback" || ENV["KAMAL_DESTINATION"] != "production" + exit 0 +end + +require "bundler/inline" + +# true = install gems so this is fast on repeat invocations +gemfile(true, quiet: true) do + source "https://rubygems.org" + + gem "octokit" + gem "faraday-retry" +end + +MAX_ATTEMPTS = 72 +ATTEMPTS_GAP = 10 + +def exit_with_error(message) + $stderr.puts message + exit 1 +end + +class GithubStatusChecks + attr_reader :remote_url, :git_sha, :github_client, :combined_status + + def initialize + @remote_url = github_repo_from_remote_url + @git_sha = `git rev-parse HEAD`.strip + @github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"]) + refresh! + end + + def refresh! + @combined_status = github_client.combined_status(remote_url, git_sha) + end + + def state + combined_status[:state] + end + + def first_status_url + first_status = combined_status[:statuses].find { |status| status[:state] == state } + first_status && first_status[:target_url] + end + + def complete_count + combined_status[:statuses].count { |status| status[:state] != "pending"} + end + + def total_count + combined_status[:statuses].count + end + + def current_status + if total_count > 0 + "Completed #{complete_count}/#{total_count} checks, see #{first_status_url} ..." + else + "Build not started..." + end + end + + private + def github_repo_from_remote_url + url = `git config --get remote.origin.url`.strip.delete_suffix(".git") + if url.start_with?("https://github.com/") + url.delete_prefix("https://github.com/") + elsif url.start_with?("git@github.com:") + url.delete_prefix("git@github.com:") + else + url + end + end +end + + +$stdout.sync = true + +begin + puts "Checking build status..." + + attempts = 0 + checks = GithubStatusChecks.new + + loop do + case checks.state + when "success" + puts "Checks passed, see #{checks.first_status_url}" + exit 0 + when "failure" + exit_with_error "Checks failed, see #{checks.first_status_url}" + when "pending" + attempts += 1 + end + + exit_with_error "Checks are still pending, gave up after #{MAX_ATTEMPTS * ATTEMPTS_GAP} seconds" if attempts == MAX_ATTEMPTS + + puts checks.current_status + sleep(ATTEMPTS_GAP) + checks.refresh! + end +rescue Octokit::NotFound + exit_with_error "Build status could not be found" +end diff --git a/.kamal/hooks/pre-proxy-reboot.sample b/.kamal/hooks/pre-proxy-reboot.sample new file mode 100755 index 000000000..061f8059e --- /dev/null +++ b/.kamal/hooks/pre-proxy-reboot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Rebooting kamal-proxy on $KAMAL_HOSTS..." diff --git a/.kamal/secrets-common b/.kamal/secrets-common new file mode 100644 index 000000000..d32adb401 --- /dev/null +++ b/.kamal/secrets-common @@ -0,0 +1,39 @@ +# Minimal Secrets Template - Backend Rails API Kamal Deployment +# Secrets defined here are available for reference under registry/password, env/secret, builder/secrets, +# and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either +# password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git. + +# Registry Configuration (ALWAYS REQUIRED) +KAMAL_REGISTRY_USERNAME=$KAMAL_REGISTRY_USERNAME +KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD + +# Rails Configuration (REQUIRED) +RAILS_MASTER_KEY=$RAILS_MASTER_KEY + +# Database Configuration (special, 2 database) +SAPI_DATABASE_HOST=$SAPI_DATABASE_HOST +SAPI_DATABASE_NAME=$SAPI_DATABASE_NAME +SAPI_DATABASE_USERNAME=$SAPI_DATABASE_USERNAME +SAPI_DATABASE_PASSWORD=$SAPI_DATABASE_PASSWORD +SAPI_DATABASE_PORT=$SAPI_DATABASE_PORT + +CAPTIVE_BREEDING_DATABASE_HOST=$CAPTIVE_BREEDING_DATABASE_HOST +CAPTIVE_BREEDING_DATABASE_NAME=$CAPTIVE_BREEDING_DATABASE_NAME +CAPTIVE_BREEDING_DATABASE_USERNAME=$CAPTIVE_BREEDING_DATABASE_USERNAME +CAPTIVE_BREEDING_DATABASE_PASSWORD=$CAPTIVE_BREEDING_DATABASE_PASSWORD +CAPTIVE_BREEDING_DATABASE_PORT=$CAPTIVE_BREEDING_DATABASE_PORT + +# Mail Configuration +MAIL_USERNAME=$MAIL_USERNAME +MAIL_PASSWORD=$MAIL_PASSWORD + +# AWS Configuration +AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID +AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY +AWS_REGION=$AWS_REGION + +# Redis for Sidekiq +SAPI_SIDEKIQ_REDIS_URL=$SAPI_SIDEKIQ_REDIS_URL + +# Redis for cache +SAPI_SIDEKIQ_REDIS_CACHE_URL=$SAPI_SIDEKIQ_REDIS_CACHE_URL diff --git a/Dockerfile b/Dockerfile index 08cedd4f1..61ba877a1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,8 @@ FROM ruby:3.2.5-slim RUN apt-get update && apt-get install --no-install-recommends -y --force-yes \ # ? libsodium-dev libgmp3-dev libssl-dev \ + # Editor + vim nano \ # PSQL libpq-dev postgresql-client \ # node js diff --git a/config/database.yml.sample b/config/database.yml.sample deleted file mode 100644 index 64521c841..000000000 --- a/config/database.yml.sample +++ /dev/null @@ -1,29 +0,0 @@ - -default: &default - host: <%= ENV.fetch("SAPI_DATABASE_HOST", 'localhost') %> - adapter: postgresql - encoding: unicode - # For details on connection pooling, see Rails configuration guide - # https://guides.rubyonrails.org/configuring.html#database-pooling - pool: <%= ENV.fetch("SAPI_RAILS_MAX_THREADS") { 5 } %> - username: <%= ENV.fetch("SAPI_DATABASE_USERNAME", 'postgres') %> - port: <%= ENV.fetch("SAPI_DATABASE_PORT", 5432) %> - -development: - <<: *default - database: sapi_development - timeout: 5000 - -test: - <<: *default - database: sapi_test - timeout: 5000 - -staging: - <<: *default - database: sapi_development - port: 5432 - -production: - <<: *default - database: sapi_development diff --git a/config/deploy.production.yml b/config/deploy.production.yml new file mode 100644 index 000000000..58bd56641 --- /dev/null +++ b/config/deploy.production.yml @@ -0,0 +1,36 @@ +# Name of the container image +image: ghcr.io/unepwcmc/sapi/rails-production + +# Define services (servers for Kamal 2.5.2 compatibility) +servers: + web: + hosts: + - example.com # TODO: Ruan + proxy: + hosts: + - www.speciesplus.net # Public-facing domain + # TODO: Ruan, do we need to add `speciesplus.net` (without www)? + ssl: true # Proxy terminates SSL + forward_headers: true # Forward headers like X-Forwarded-Proto + healthcheck: + path: /up + logging: + request_headers: + - Cache-Control + - User-Agent + - X-Forwarded-Proto # Critical for Rails to detect HTTPS + response_headers: + - X-Request-ID + job: + hosts: + - example.com # TODO: Ruan + options: + add-host: host.docker.internal:host-gateway + cmd: bundle exec sidekiq -C config/sidekiq.yml + healthcheck: + cmd: /rails/bin/docker-sidekiq-healthcheck + +# Environment variables +env: + clear: + RAILS_ENV: production diff --git a/config/deploy.staging.yml b/config/deploy.staging.yml new file mode 100644 index 000000000..3b7cf46ef --- /dev/null +++ b/config/deploy.staging.yml @@ -0,0 +1,35 @@ +# Name of the container image +image: ghcr.io/unepwcmc/sapi/rails-staging + +# Define services (servers for Kamal 2.5.2 compatibility) +servers: + web: + hosts: + - example.com # TODO: Ruan + proxy: + hosts: + - sapi.sapi-staging.linode.unep-wcmc.org # Public-facing domain + ssl: true # Proxy terminates SSL + forward_headers: true # Forward headers like X-Forwarded-Proto + healthcheck: + path: /up + logging: + request_headers: + - Cache-Control + - User-Agent + - X-Forwarded-Proto # Critical for Rails to detect HTTPS + response_headers: + - X-Request-ID + job: + hosts: + - example.com # TODO: Ruan + options: + add-host: host.docker.internal:host-gateway + cmd: bundle exec sidekiq -C config/sidekiq.yml + healthcheck: + cmd: /rails/bin/docker-sidekiq-healthcheck + +# Environment variables +env: + clear: + RAILS_ENV: staging diff --git a/config/deploy.yml b/config/deploy.yml new file mode 100644 index 000000000..f67661cba --- /dev/null +++ b/config/deploy.yml @@ -0,0 +1,48 @@ +# Name of your application. Used to uniquely configure containers. +service: sapi + +# Credentials for your image host. +registry: + server: ghcr.io + username: + - KAMAL_REGISTRY_USERNAME + password: + - KAMAL_REGISTRY_PASSWORD + +# Inject ENV variables into containers (secrets come from .kamal/secrets.{environment}) +env: + clear: + RAILS_LOG_LEVEL: warn # @see https://github.com/heartcombo/devise#password-reset-tokens-and-rails-logs + PORT: 80 + RAILS_SERVE_STATIC_FILES: 1 + RAILS_LOG_TO_STDOUT: 1 # Need this before upgrade to Rails 7.1, which then default is STDOUT. + secret: + - RAILS_MASTER_KEY + - SAPI_DATABASE_HOST + - SAPI_DATABASE_NAME + - SAPI_DATABASE_USERNAME + - SAPI_DATABASE_PASSWORD + - SAPI_DATABASE_PORT + - CAPTIVE_BREEDING_DATABASE_HOST + - CAPTIVE_BREEDING_DATABASE_NAME + - CAPTIVE_BREEDING_DATABASE_USERNAME + - CAPTIVE_BREEDING_DATABASE_PASSWORD + - CAPTIVE_BREEDING_DATABASE_PORT + - MAIL_USERNAME + - MAIL_PASSWORD + - AWS_ACCESS_KEY_ID + - AWS_SECRET_ACCESS_KEY + - AWS_REGION + - SAPI_SIDEKIQ_REDIS_URL + - SAPI_SIDEKIQ_REDIS_CACHE_URL + +# Use a different ssh user than root +ssh: + user: wcmc + +# Configure builder setup +builder: + arch: amd64 + dockerfile: Dockerfile.deploy + args: + RUBY_VERSION: 3.2.5 From 19ba0c95a906cd770190639388fb20940c1c7ac0 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 25 Jul 2025 22:06:11 +0100 Subject: [PATCH 11/16] add sidekiq healthcheck script --- bin/docker-sidekiq-healthcheck | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100755 bin/docker-sidekiq-healthcheck diff --git a/bin/docker-sidekiq-healthcheck b/bin/docker-sidekiq-healthcheck new file mode 100755 index 000000000..975e7c159 --- /dev/null +++ b/bin/docker-sidekiq-healthcheck @@ -0,0 +1,11 @@ +#!/bin/bash + +# Cannot use `sidekiqmon`, because it rely on REDIS_URL env. +# We do not store redis_url in REDIS_URL: +# - it has password, is serect, should be in rails credentials; +# - there maybe more than one redis (e.g. One for sidekiq [maxmemory-policy=noeviction]; one for cache [maxmemory-policy=lfu]) +if [ $(ps aux | grep 'sidekiq ' | grep -v grep | wc -l) -gt 0 ]; then + exit 0 +else + exit 1 +fi From ed76285662bc478328c0c4fed1a34e07b6833be7 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 25 Jul 2025 22:07:27 +0100 Subject: [PATCH 12/16] nodejs only use for assets:precompile, but somehow it broken if not available in runtime, maybe due to terser? --- Dockerfile.deploy | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/Dockerfile.deploy b/Dockerfile.deploy index 369f38c5a..c678f6183 100644 --- a/Dockerfile.deploy +++ b/Dockerfile.deploy @@ -17,6 +17,8 @@ RUN apt-get update -qq && \ apt-get install --no-install-recommends -y curl libjemalloc2 libpq-dev \ # ? libsodium-dev libgmp3-dev libssl-dev \ + # install node js + curl xz-utils \ # latex (huge file size) texlive-latex-base texlive-fonts-recommended texlive-fonts-extra texlive-latex-extra \ && rm -rf /var/lib/apt/lists /var/cache/apt/archives @@ -28,16 +30,6 @@ ENV NODE_ENV="production" \ BUNDLE_PATH="/usr/local/bundle" \ BUNDLE_WITHOUT="development" -# 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 git libyaml-dev pkg-config \ - # install node js - curl xz-utils \ - && rm -rf /var/lib/apt/lists /var/cache/apt/archives - # Install Node.js 18.20.8 manually ARG NODE_VERSION=18.20.8 ARG TARGETARCH @@ -50,6 +42,15 @@ RUN case "$TARGETARCH" in \ curl -fsSL https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.xz \ | tar -xJ -C /usr/local --strip-components=1 + +# 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 git libyaml-dev pkg-config \ + && rm -rf /var/lib/apt/lists /var/cache/apt/archives + # Install application gems COPY Gemfile Gemfile.lock ./ RUN bundle install && \ From 60e88d392520b580f5d12fcd439cea370bb5caee Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 25 Jul 2025 22:08:41 +0100 Subject: [PATCH 13/16] No longer need to remove the pid file in production mode, only need for development. Move to docker compose --- bin/docker-entrypoint | 6 +++-- config/puma.rb | 60 ++++++++++++++++++++++++------------------- 2 files changed, 37 insertions(+), 29 deletions(-) diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint index 5c404a181..3054757cc 100755 --- a/bin/docker-entrypoint +++ b/bin/docker-entrypoint @@ -1,7 +1,9 @@ #!/bin/bash -e -if [[ "${@}" =~ "rails server" ]]; then - rm -f ./tmp/pids/server.pid; +# 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 mkdir -p {./,spec/}public/downloads/checklist diff --git a/config/puma.rb b/config/puma.rb index 58e1c205b..a248513b2 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -1,35 +1,41 @@ # This configuration file will be evaluated by Puma. The top-level methods that # are invoked here are part of Puma's configuration DSL. For more information # about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. - -# Puma can serve each request in a thread from an internal thread pool. -# The `threads` method setting takes two numbers: a minimum and maximum. -# Any libraries that use thread pools should be configured to match -# the maximum value specified for Puma. Default is set to 5 threads for minimum -# and maximum; this matches the default thread size of Active Record. -max_threads_count = ENV.fetch('RAILS_MAX_THREADS') { 5 } -min_threads_count = ENV.fetch('RAILS_MIN_THREADS') { max_threads_count } -threads min_threads_count, max_threads_count - -# Specifies that the worker count should equal the number of processors in production. -if ENV['RAILS_ENV'] == 'production' - require 'concurrent-ruby' - worker_count = Integer(ENV.fetch('WEB_CONCURRENCY') { Concurrent.physical_processor_count }) - workers worker_count if worker_count > 1 -end - -# Specifies the `worker_timeout` threshold that Puma will use to wait before -# terminating a worker in development environments. -worker_timeout 3600 if ENV.fetch('RAILS_ENV', 'development') == 'development' +# +# Puma starts a configurable number of processes (workers) and each process +# serves each request in a thread from an internal thread pool. +# +# You can control the number of workers using ENV["WEB_CONCURRENCY"]. You +# should only set this value when you want to run 2 or more workers. The +# default is already 1. +# +# The ideal number of threads per worker depends both on how much time the +# application spends waiting for IO operations and on how much you wish to +# prioritize throughput over latency. +# +# As a rule of thumb, increasing the number of threads will increase how much +# traffic a given process can handle (throughput), but due to CRuby's +# Global VM Lock (GVL) it has diminishing returns and will degrade the +# response time (latency) of the application. +# +# The default is set to 3 threads as it's deemed a decent compromise between +# throughput and latency for the average Rails application. +# +# Any libraries that use a connection pool or another resource pool should +# be configured to provide at least as many connections as the number of +# threads. This includes Active Record's `pool` parameter in `database.yml`. +threads_count = ENV.fetch("RAILS_MAX_THREADS", 3) +threads threads_count, threads_count # Specifies the `port` that Puma will listen on to receive requests; default is 3000. -port ENV.fetch('PORT') { 3000 } +port ENV.fetch("PORT", 3000) -# Specifies the `environment` that Puma will run in. -environment ENV.fetch('RAILS_ENV') { 'development' } +# Allow puma to be restarted by `bin/rails restart` command. +plugin :tmp_restart -# Specifies the `pidfile` that Puma will use. -pidfile ENV.fetch('PIDFILE') { 'tmp/pids/server.pid' } +# Run the Solid Queue supervisor inside of Puma for single-server deployments +plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"] -# Allow puma to be restarted by `rails restart` command. -plugin :tmp_restart +# Specify the PID file. Defaults to tmp/pids/server.pid in development. +# In other environments, only set the PID file if requested. +pidfile ENV["PIDFILE"] if ENV["PIDFILE"] From 36d46f865c7b246d72cb99e4aac14ca7e9041616 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 25 Jul 2025 22:09:37 +0100 Subject: [PATCH 14/16] fix production config, to serve assets from puma --- config/environments/production.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/environments/production.rb b/config/environments/production.rb index 3c38ce0d0..df9557e54 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -21,7 +21,7 @@ # config.require_master_key = true # Disable serving static files from `public/`, relying on NGINX/Apache to do so instead. - config.public_file_server.enabled = false + config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? # Compress JavaScripts and CSS. config.assets.js_compressor = :terser From 95ad590502545683ffcb8d4d553322ff643a234c Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 1 Aug 2025 17:02:04 +0100 Subject: [PATCH 15/16] add remove puma pid file in docker compose for development only --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index ca05afb0d..26167db01 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -69,7 +69,7 @@ services: rails: container_name: sapi-rails build: . - command: bundle install && bundle exec rails server -p 3000 -b '0.0.0.0' + command: /bin/bash -l -c "bundle install && rm -rf /SAPI/tmp/pids/server.pid && bundle exec rails server -p 3000 -b '0.0.0.0'" volumes: &rails_volumes # Used for both rails and sidekiq From c6d05d28c6c52bb8c3d8035b7b48c817e371c44b Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 1 Aug 2025 17:08:02 +0100 Subject: [PATCH 16/16] add redis cache port in compose --- docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 26167db01..9dcc5f458 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -164,6 +164,8 @@ services: redis_cache: container_name: sapi-redis-cache image: redis:7.2.0 + ports: + - "${SAPI_CONTAINER_REDIS_PORT:-6380}:6380" networks: - sapi command: redis-server /usr/local/etc/redis/redis.conf