From b34d85df1660c6666c2c1d04a0fe7ad3a0734f10 Mon Sep 17 00:00:00 2001 From: benmelz Date: Mon, 16 Mar 2026 10:00:50 -0400 Subject: [PATCH 1/6] copy dockerfile changes from gtfs_cache --- Dockerfile | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 589a360..a549883 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,8 @@ +# syntax=docker/dockerfile:1 +# check=error=true +# +# docker build --tag fleetfocus-api --build-arg RUBY_VERSION="$(cat .ruby-version)" . + ARG RUBY_VERSION=OVERRIDE_ME FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim AS base @@ -41,4 +46,4 @@ USER fleetfocus-api:fleetfocus-api EXPOSE 80 -CMD ["script/server", "--port", "80"] +CMD ["script/server", "--port=80"] From e27bc0343267d359a1b781278bd8f206fa6051a0 Mon Sep 17 00:00:00 2001 From: benmelz Date: Mon, 16 Mar 2026 10:02:25 -0400 Subject: [PATCH 2/6] drop capistrano --- Capfile | 10 -------- Gemfile | 4 ---- Gemfile.lock | 44 ++---------------------------------- config/deploy.rb | 20 ---------------- config/deploy/production.rb | 5 ---- lib/capistrano/tasks/db.rake | 14 ------------ 6 files changed, 2 insertions(+), 95 deletions(-) delete mode 100644 Capfile delete mode 100644 config/deploy.rb delete mode 100644 config/deploy/production.rb delete mode 100644 lib/capistrano/tasks/db.rake diff --git a/Capfile b/Capfile deleted file mode 100644 index f8f6eac..0000000 --- a/Capfile +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -%w[setup deploy scm/git pending bundler passenger].each do |lib| - require "capistrano/#{lib}" -end - -install_plugin Capistrano::SCM::Git - -# Load custom tasks from `lib/capistrano/tasks` if you have any defined -Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r } diff --git a/Gemfile b/Gemfile index 0a8481a..40b4fe9 100644 --- a/Gemfile +++ b/Gemfile @@ -31,10 +31,6 @@ end group :development do gem 'bcrypt_pbkdf', '>= 1.0', '< 2.0', require: false - gem 'capistrano', '~> 3.20', require: false - gem 'capistrano-bundler', require: false - gem 'capistrano-passenger', require: false - gem 'capistrano-pending', require: false gem 'ed25519', '>= 1.2', '< 2.0', require: false gem 'railties', require: false gem 'rubocop', require: false diff --git a/Gemfile.lock b/Gemfile.lock index dd5f822..7ca8005 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -80,24 +80,11 @@ GEM uri (>= 0.13.1) addressable (2.8.9) public_suffix (>= 2.0.2, < 8.0) - airbrussh (1.6.0) - sshkit (>= 1.6.1, != 1.7.0) ast (2.4.3) base64 (0.3.0) bcrypt_pbkdf (1.1.2) bigdecimal (4.0.1) builder (3.3.0) - capistrano (3.20.0) - airbrussh (>= 1.0.0) - i18n - rake (>= 10.0.0) - sshkit (>= 1.9.0) - capistrano-bundler (2.2.0) - capistrano (~> 3.1) - capistrano-passenger (0.2.1) - capistrano (~> 3.0) - capistrano-pending (0.2.0) - capistrano (>= 3.2.0) concurrent-ruby (1.3.6) connection_pool (3.0.2) crass (1.0.6) @@ -165,20 +152,14 @@ GEM net-protocol net-protocol (0.2.2) timeout - 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-smtp (0.5.1) net-protocol - net-ssh (7.3.0) nio4r (2.7.5) nokogiri (1.19.1) mini_portile2 (~> 2.8.2) racc (~> 1.4) nokogiri (1.19.1-x86_64-linux-gnu) racc (~> 1.4) - ostruct (0.6.3) parallel (1.27.0) parser (3.3.10.2) ast (~> 2.4.1) @@ -310,13 +291,6 @@ GEM sqlite3 (2.6.0) mini_portile2 (~> 2.8.0) sqlite3 (2.6.0-x86_64-linux-gnu) - sshkit (1.25.0) - base64 - logger - net-scp (>= 1.1.2) - net-sftp (>= 2.1.2) - net-ssh (>= 2.8.0) - ostruct stringio (3.2.0) thor (1.5.0) tilt (2.6.1) @@ -348,10 +322,6 @@ DEPENDENCIES activerecord-sqlserver-adapter activesupport bcrypt_pbkdf (>= 1.0, < 2.0) - capistrano (~> 3.20) - capistrano-bundler - capistrano-passenger - capistrano-pending database_cleaner ed25519 (>= 1.2, < 2.0) exception_notification @@ -392,16 +362,11 @@ CHECKSUMS activestorage (8.1.2) sha256=8a63a48c3999caeee26a59441f813f94681fc35cc41aba7ce1f836add04fba76 activesupport (8.1.2) sha256=88842578ccd0d40f658289b0e8c842acfe9af751afee2e0744a7873f50b6fdae addressable (2.8.9) sha256=cc154fcbe689711808a43601dee7b980238ce54368d23e127421753e46895485 - airbrussh (1.6.0) sha256=7e2cf581f2319d2c2b2b672c9fc486efb4dfcfed4bd2dadbef5f10b8b2a000d0 ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383 base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b bcrypt_pbkdf (1.1.2) sha256=c2414c23ce66869b3eb9f643d6a3374d8322dfb5078125c82792304c10b94cf6 bigdecimal (4.0.1) sha256=8b07d3d065a9f921c80ceaea7c9d4ae596697295b584c296fe599dd0ad01c4a7 builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f - capistrano (3.20.0) sha256=0113e58dda99add0342e56a244f664734c59f442c5ed734f5303b0b559b479c9 - capistrano-bundler (2.2.0) sha256=47b4cf2ea17ea132bb0a5cabc5663443f5190a54f4da5b322d04e1558ff1468c - capistrano-passenger (0.2.1) sha256=07a1d25edd5c1d909c19d4fe45fe2ea5f11200569f6967f6bff1d605ade98e13 - capistrano-pending (0.2.0) sha256=f9e8c1e6b6a2ce760ed49ccb470c474f802c1d453704fb62d67ca4c1ee547066 concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a crass (1.0.6) sha256=dc516022a56e7b3b156099abc81b6d2b08ea1ed12676ac7a5657617f012bd45d @@ -438,14 +403,10 @@ CHECKSUMS net-imap (0.5.12) sha256=cb8cd05bd353fcc19b6cbc530a9cb06b577a969ea10b7ddb0f37787f74be4444 net-pop (0.1.2) sha256=848b4e982013c15b2f0382792268763b748cce91c9e91e36b0f27ed26420dff3 net-protocol (0.2.2) sha256=aa73e0cba6a125369de9837b8d8ef82a61849360eba0521900e2c3713aa162a8 - net-scp (4.1.0) sha256=a99b0b92a1e5d360b0de4ffbf2dc0c91531502d3d4f56c28b0139a7c093d1a5d - net-sftp (4.0.0) sha256=65bb91c859c2f93b09826757af11b69af931a3a9155050f50d1b06d384526364 net-smtp (0.5.1) sha256=ed96a0af63c524fceb4b29b0d352195c30d82dd916a42f03c62a3a70e5b70736 - net-ssh (7.3.0) sha256=172076c4b30ce56fb25a03961b0c4da14e1246426401b0f89cba1a3b54bf3ef0 nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1 nokogiri (1.19.1) sha256=598b327f36df0b172abd57b68b18979a6e14219353bca87180c31a51a00d5ad3 nokogiri (1.19.1-x86_64-linux-gnu) sha256=1a4902842a186b4f901078e692d12257678e6133858d0566152fe29cdb98456a - ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912 parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130 parser (3.3.10.2) sha256=6f60c84aa4bdcedb6d1a2434b738fe8a8136807b6adc8f7f53b97da9bc4e9357 pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6 @@ -490,7 +451,6 @@ CHECKSUMS sinatra-activerecord (2.0.28) sha256=99f352c2dfa244d02b4f877efbe00135360b758390b8bb7bc2d4d91171c93811 sqlite3 (2.6.0) sha256=a1c625f11948e6726eb082700283a8a3f4cf20b0548c0051c6104c56fedbe314 sqlite3 (2.6.0-x86_64-linux-gnu) sha256=415a950be612b865152dd4529b16fdce2c8962ff9fa3f6b95adbfa7b8f54f2d0 - sshkit (1.25.0) sha256=c8c6543cdb60f91f1d277306d585dd11b6a064cb44eab0972827e4311ff96744 stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1 thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73 tilt (2.6.1) sha256=35a99bba2adf7c1e362f5b48f9b581cce4edfba98117e34696dde6d308d84770 @@ -508,7 +468,7 @@ CHECKSUMS zeitwerk (2.7.5) sha256=d8da92128c09ea6ec62c949011b00ed4a20242b255293dd66bf41545398f73dd RUBY VERSION - ruby 3.4.8p72 + ruby 3.4.8p72 BUNDLED WITH - 4.0.6 + 4.0.6 diff --git a/config/deploy.rb b/config/deploy.rb deleted file mode 100644 index b3136dd..0000000 --- a/config/deploy.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true -# config valid only for current version of Capistrano -lock '~> 3.20' - -set :application, 'fleetfocus-api' -set :repo_url, 'https://github.com/umts/fleetfocus-api.git' -set :branch, 'main' -set :deploy_to, "/srv/#{fetch :application}" - -set :log_level, :info - -set :capistrano_pending_role, :app - -append :linked_files, 'config/fleetfocus-api.key' -append :linked_dirs, '.bundle', 'log', 'tmp/pids' - -set :passenger_restart_with_sudo, true -set :bundle_version, 4 - -before 'deploy:publishing', 'db:check' diff --git a/config/deploy/production.rb b/config/deploy/production.rb deleted file mode 100644 index 9a3cad2..0000000 --- a/config/deploy/production.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -server 'af-transit-app3.admin.umass.edu', - roles: %w(app web), - ssh_options: { forward_agent: false } diff --git a/lib/capistrano/tasks/db.rake b/lib/capistrano/tasks/db.rake deleted file mode 100644 index 136b00d..0000000 --- a/lib/capistrano/tasks/db.rake +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -namespace :db do - desc 'Check that we can connect to the database' - task :check do - on roles(:app) do - within release_path do - with rack_env: fetch(:stage) do - execute :rake, 'check' - end - end - end - end -end From 5b1c621cc4cd66f3391a22c3bb3ab0524d1dc8f7 Mon Sep 17 00:00:00 2001 From: benmelz Date: Mon, 16 Mar 2026 10:04:02 -0400 Subject: [PATCH 3/6] bundle exec kamal init --- .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.sample | 51 +++++++++++ .kamal/hooks/pre-connect.sample | 47 ++++++++++ .kamal/hooks/pre-deploy.sample | 122 ++++++++++++++++++++++++++ .kamal/hooks/pre-proxy-reboot.sample | 3 + .kamal/secrets | 18 ++++ Gemfile | 19 ++-- Gemfile.lock | 33 +++++++ config/deploy.yml | 102 +++++++++++++++++++++ 13 files changed, 412 insertions(+), 9 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.sample 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 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 0000000..2fb07d7 --- /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 0000000..70f9c4b --- /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 0000000..fd364c2 --- /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 0000000..1435a67 --- /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 0000000..45f7355 --- /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.sample b/.kamal/hooks/pre-build.sample new file mode 100755 index 0000000..c5a5567 --- /dev/null +++ b/.kamal/hooks/pre-build.sample @@ -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 0000000..77744bd --- /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 0000000..05b3055 --- /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 0000000..061f805 --- /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 b/.kamal/secrets new file mode 100644 index 0000000..60fbcaf --- /dev/null +++ b/.kamal/secrets @@ -0,0 +1,18 @@ +# 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. + +# Option 1: Read secrets from the environment +# KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD + +# Option 2: Read secrets via a command +# RAILS_MASTER_KEY=$(cat config/master.key) +# KAMAL_REGISTRY_PASSWORD=$(rails credentials:fetch kamal.registry_password) + +# Option 3: Read secrets via kamal secrets helpers +# These will handle logging in and fetching the secrets in as few calls as possible +# There are adapters for 1Password, LastPass + Bitwarden +# +# SECRETS=$(kamal secrets fetch --adapter 1password --account my-account --from MyVault/MyItem KAMAL_REGISTRY_PASSWORD RAILS_MASTER_KEY) +# KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD $SECRETS) +# RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY $SECRETS) diff --git a/Gemfile b/Gemfile index 40b4fe9..2f46387 100644 --- a/Gemfile +++ b/Gemfile @@ -20,18 +20,10 @@ group :production, :development do gem 'tiny_tds', force_ruby_platform: true # TODO: Remove dependency completely when have newer GLIBC. end -group :test do - gem 'database_cleaner' - gem 'factory_bot' - gem 'rack-test', require: 'rack/test' - gem 'rspec' - gem 'simplecov', require: false - gem 'sqlite3', '>= 2.1' -end - group :development do gem 'bcrypt_pbkdf', '>= 1.0', '< 2.0', require: false gem 'ed25519', '>= 1.2', '< 2.0', require: false + gem 'kamal', require: false gem 'railties', require: false gem 'rubocop', require: false gem 'rubocop-factory_bot', require: false @@ -39,3 +31,12 @@ group :development do gem 'rubocop-rake', require: false gem 'rubocop-rspec', require: false end + +group :test do + gem 'database_cleaner' + gem 'factory_bot' + gem 'rack-test', require: 'rack/test' + gem 'rspec' + gem 'simplecov', require: false + gem 'sqlite3', '>= 2.1' +end diff --git a/Gemfile.lock b/Gemfile.lock index 7ca8005..2965209 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -97,6 +97,7 @@ GEM date (3.5.1) diff-lcs (1.6.2) docile (1.4.0) + dotenv (3.2.0) drb (2.2.3) ed25519 (1.4.0) erb (6.0.2) @@ -123,6 +124,17 @@ GEM json-schema (6.1.0) addressable (~> 2.8) bigdecimal (>= 3.1, < 5) + kamal (2.10.1) + 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) language_server-protocol (3.17.0.5) lint_roller (1.1.0) logger (1.7.0) @@ -152,14 +164,20 @@ GEM net-protocol net-protocol (0.2.2) timeout + 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-smtp (0.5.1) net-protocol + net-ssh (7.3.1) nio4r (2.7.5) nokogiri (1.19.1) mini_portile2 (~> 2.8.2) racc (~> 1.4) nokogiri (1.19.1-x86_64-linux-gnu) racc (~> 1.4) + ostruct (0.6.3) parallel (1.27.0) parser (3.3.10.2) ast (~> 2.4.1) @@ -291,6 +309,13 @@ GEM sqlite3 (2.6.0) mini_portile2 (~> 2.8.0) sqlite3 (2.6.0-x86_64-linux-gnu) + sshkit (1.25.0) + base64 + logger + net-scp (>= 1.1.2) + net-sftp (>= 2.1.2) + net-ssh (>= 2.8.0) + ostruct stringio (3.2.0) thor (1.5.0) tilt (2.6.1) @@ -328,6 +353,7 @@ DEPENDENCIES factory_bot irb json + kamal psych puma rack-test @@ -376,6 +402,7 @@ CHECKSUMS date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0 diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962 docile (1.4.0) sha256=5f1734bde23721245c20c3d723e76c104208e1aa01277a69901ce770f0ebb8d3 + dotenv (3.2.0) sha256=e375b83121ea7ca4ce20f214740076129ab8514cd81378161f11c03853fe619d drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373 ed25519 (1.4.0) sha256=16e97f5198689a154247169f3453ef4cfd3f7a47481fde0ae33206cdfdcac506 erb (6.0.2) sha256=9fe6264d44f79422c87490a1558479bd0e7dad4dd0e317656e67ea3077b5242b @@ -389,6 +416,7 @@ CHECKSUMS jbuilder (2.13.0) sha256=7200a38a1c0081aa81b7a9757e7a299db75bc58cf1fd45ca7919a91627d227d6 json (2.19.1) sha256=dd94fdc59e48bff85913829a32350b3148156bc4fd2a95a2568a78b11344082d json-schema (6.1.0) sha256=6bf70a2cfb6dfd5a06da28093fa8190f324c88eabd36a7f47097f227321dc702 + kamal (2.10.1) sha256=53b7ecb4c33dd83b1aedfc7aacd1c059f835993258a552d70d584c6ce32b6340 language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87 logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 @@ -403,10 +431,14 @@ CHECKSUMS net-imap (0.5.12) sha256=cb8cd05bd353fcc19b6cbc530a9cb06b577a969ea10b7ddb0f37787f74be4444 net-pop (0.1.2) sha256=848b4e982013c15b2f0382792268763b748cce91c9e91e36b0f27ed26420dff3 net-protocol (0.2.2) sha256=aa73e0cba6a125369de9837b8d8ef82a61849360eba0521900e2c3713aa162a8 + net-scp (4.1.0) sha256=a99b0b92a1e5d360b0de4ffbf2dc0c91531502d3d4f56c28b0139a7c093d1a5d + net-sftp (4.0.0) sha256=65bb91c859c2f93b09826757af11b69af931a3a9155050f50d1b06d384526364 net-smtp (0.5.1) sha256=ed96a0af63c524fceb4b29b0d352195c30d82dd916a42f03c62a3a70e5b70736 + net-ssh (7.3.1) sha256=229d518b429211bebd89151e2a12febff0631138513ac259953aa7b7cd42b53b nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1 nokogiri (1.19.1) sha256=598b327f36df0b172abd57b68b18979a6e14219353bca87180c31a51a00d5ad3 nokogiri (1.19.1-x86_64-linux-gnu) sha256=1a4902842a186b4f901078e692d12257678e6133858d0566152fe29cdb98456a + ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912 parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130 parser (3.3.10.2) sha256=6f60c84aa4bdcedb6d1a2434b738fe8a8136807b6adc8f7f53b97da9bc4e9357 pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6 @@ -451,6 +483,7 @@ CHECKSUMS sinatra-activerecord (2.0.28) sha256=99f352c2dfa244d02b4f877efbe00135360b758390b8bb7bc2d4d91171c93811 sqlite3 (2.6.0) sha256=a1c625f11948e6726eb082700283a8a3f4cf20b0548c0051c6104c56fedbe314 sqlite3 (2.6.0-x86_64-linux-gnu) sha256=415a950be612b865152dd4529b16fdce2c8962ff9fa3f6b95adbfa7b8f54f2d0 + sshkit (1.25.0) sha256=c8c6543cdb60f91f1d277306d585dd11b6a064cb44eab0972827e4311ff96744 stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1 thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73 tilt (2.6.1) sha256=35a99bba2adf7c1e362f5b48f9b581cce4edfba98117e34696dde6d308d84770 diff --git a/config/deploy.yml b/config/deploy.yml new file mode 100644 index 0000000..cea8269 --- /dev/null +++ b/config/deploy.yml @@ -0,0 +1,102 @@ +# Name of your application. Used to uniquely configure containers. +service: my-app + +# Name of the container image. +image: my-user/my-app + +# Deploy to these servers. +servers: + web: + - 192.168.0.1 + # job: + # hosts: + # - 192.168.0.1 + # cmd: bin/jobs + +# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server. +# Remove this section when using multiple web servers and ensure you terminate SSL at your load balancer. +# +# Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption. +proxy: + ssl: true + host: app.example.com + # Proxy connects to your container on port 80 by default. + # app_port: 3000 + +# Credentials for your image host. +registry: + server: localhost:5555 + # Specify the registry server, if you're not using Docker Hub + # server: registry.digitalocean.com / ghcr.io / ... + # username: my-user + + # Always use an access token rather than real password (pulled from .kamal/secrets). + # password: + # - KAMAL_REGISTRY_PASSWORD + +# Configure builder setup. +builder: + arch: amd64 + # Pass in additional build args needed for your Dockerfile. + # args: + # RUBY_VERSION: <%= ENV["RBENV_VERSION"] || ENV["rvm_ruby_string"] || "#{RUBY_ENGINE}-#{RUBY_ENGINE_VERSION}" %> + +# Inject ENV variables into containers (secrets come from .kamal/secrets). +# +# env: +# clear: +# DB_HOST: 192.168.0.2 +# secret: +# - RAILS_MASTER_KEY + +# Aliases are triggered with "bin/kamal ". You can overwrite arguments on invocation: +# "bin/kamal app logs -r job" will tail logs from the first server in the job section. +# +# aliases: +# shell: app exec --interactive --reuse "bash" + +# Use a different ssh user than root +# +# ssh: +# user: app + +# Use a persistent storage volume. +# +# volumes: +# - "app_storage:/app/storage" + +# Bridge fingerprinted assets, like JS and CSS, between versions to avoid +# hitting 404 on in-flight requests. Combines all files from new and old +# version inside the asset_path. +# +# asset_path: /app/public/assets + +# Configure rolling deploys by setting a wait time between batches of restarts. +# +# boot: +# limit: 10 # Can also specify as a percentage of total hosts, such as "25%" +# wait: 2 + +# Use accessory services (secrets come from .kamal/secrets). +# +# accessories: +# db: +# image: mysql:8.0 +# host: 192.168.0.2 +# port: 3306 +# env: +# clear: +# MYSQL_ROOT_HOST: '%' +# secret: +# - MYSQL_ROOT_PASSWORD +# files: +# - config/mysql/production.cnf:/etc/mysql/my.cnf +# - db/production.sql:/docker-entrypoint-initdb.d/setup.sql +# directories: +# - data:/var/lib/mysql +# redis: +# image: valkey/valkey:8 +# host: 192.168.0.2 +# port: 6379 +# directories: +# - data:/data From 378dd87e5da8c5fc7793cc88d9a1d3e91601544d Mon Sep 17 00:00:00 2001 From: benmelz Date: Mon, 16 Mar 2026 10:09:10 -0400 Subject: [PATCH 4/6] copy config from gtfs_cache --- .kamal/secrets | 4 +- config/deploy.yml | 117 +++++++++------------------------------------- 2 files changed, 25 insertions(+), 96 deletions(-) diff --git a/.kamal/secrets b/.kamal/secrets index 60fbcaf..24b316b 100644 --- a/.kamal/secrets +++ b/.kamal/secrets @@ -3,11 +3,11 @@ # password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git. # Option 1: Read secrets from the environment -# KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD +#KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD # Option 2: Read secrets via a command # RAILS_MASTER_KEY=$(cat config/master.key) -# KAMAL_REGISTRY_PASSWORD=$(rails credentials:fetch kamal.registry_password) +MASTER_KEY=$(cat config/fleetfocus-api.key) # Option 3: Read secrets via kamal secrets helpers # These will handle logging in and fetching the secrets in as few calls as possible diff --git a/config/deploy.yml b/config/deploy.yml index cea8269..9ffc580 100644 --- a/config/deploy.yml +++ b/config/deploy.yml @@ -1,102 +1,31 @@ -# Name of your application. Used to uniquely configure containers. -service: my-app - -# Name of the container image. -image: my-user/my-app - -# Deploy to these servers. +<% + def project_root + KAMAL.configured?[:config_file].join('../..') + end + + def user_for(host) + Net::SSH::Config.for(host).fetch(:user, ENV.fetch('USER', nil)) + end +%> + +service: fleetfocus-api +image: umts/fleetfocus-api servers: web: - - 192.168.0.1 - # job: - # hosts: - # - 192.168.0.1 - # cmd: bin/jobs - -# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server. -# Remove this section when using multiple web servers and ensure you terminate SSL at your load balancer. -# -# Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption. + - afs-umts-web4.admin.umass.edu proxy: ssl: true - host: app.example.com - # Proxy connects to your container on port 80 by default. - # app_port: 3000 - -# Credentials for your image host. + host: https://transit-fuel-api.admin.umass.edu registry: server: localhost:5555 - # Specify the registry server, if you're not using Docker Hub - # server: registry.digitalocean.com / ghcr.io / ... - # username: my-user - - # Always use an access token rather than real password (pulled from .kamal/secrets). - # password: - # - KAMAL_REGISTRY_PASSWORD - -# Configure builder setup. builder: arch: amd64 - # Pass in additional build args needed for your Dockerfile. - # args: - # RUBY_VERSION: <%= ENV["RBENV_VERSION"] || ENV["rvm_ruby_string"] || "#{RUBY_ENGINE}-#{RUBY_ENGINE_VERSION}" %> - -# Inject ENV variables into containers (secrets come from .kamal/secrets). -# -# env: -# clear: -# DB_HOST: 192.168.0.2 -# secret: -# - RAILS_MASTER_KEY - -# Aliases are triggered with "bin/kamal ". You can overwrite arguments on invocation: -# "bin/kamal app logs -r job" will tail logs from the first server in the job section. -# -# aliases: -# shell: app exec --interactive --reuse "bash" - -# Use a different ssh user than root -# -# ssh: -# user: app - -# Use a persistent storage volume. -# -# volumes: -# - "app_storage:/app/storage" - -# Bridge fingerprinted assets, like JS and CSS, between versions to avoid -# hitting 404 on in-flight requests. Combines all files from new and old -# version inside the asset_path. -# -# asset_path: /app/public/assets - -# Configure rolling deploys by setting a wait time between batches of restarts. -# -# boot: -# limit: 10 # Can also specify as a percentage of total hosts, such as "25%" -# wait: 2 - -# Use accessory services (secrets come from .kamal/secrets). -# -# accessories: -# db: -# image: mysql:8.0 -# host: 192.168.0.2 -# port: 3306 -# env: -# clear: -# MYSQL_ROOT_HOST: '%' -# secret: -# - MYSQL_ROOT_PASSWORD -# files: -# - config/mysql/production.cnf:/etc/mysql/my.cnf -# - db/production.sql:/docker-entrypoint-initdb.d/setup.sql -# directories: -# - data:/var/lib/mysql -# redis: -# image: valkey/valkey:8 -# host: 192.168.0.2 -# port: 6379 -# directories: -# - data:/data + args: + RUBY_VERSION: <%= project_root.join('.ruby-version').read.strip %> +env: + secret: + - MASTER_KEY +ssh: + user: <%= user_for 'afs-umts-web4.admin.umass.edu' %> +volumes: + - "fleetfocus-api_log:/app/log" From be64b6d52075bfdc95a79fab83e9386b59cefdd9 Mon Sep 17 00:00:00 2001 From: benmelz Date: Mon, 16 Mar 2026 10:12:46 -0400 Subject: [PATCH 5/6] add missing platforms, new glibc --- Gemfile | 2 +- Gemfile.lock | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 2f46387..61ed8db 100644 --- a/Gemfile +++ b/Gemfile @@ -17,7 +17,7 @@ gem 'tilt-jbuilder', require: 'sinatra/jbuilder' group :production, :development do gem 'activerecord-sqlserver-adapter' - gem 'tiny_tds', force_ruby_platform: true # TODO: Remove dependency completely when have newer GLIBC. + gem 'tiny_tds' end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index 2965209..e9b3c24 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -175,6 +175,10 @@ GEM nokogiri (1.19.1) mini_portile2 (~> 2.8.2) racc (~> 1.4) + nokogiri (1.19.1-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.1-arm64-darwin) + racc (~> 1.4) nokogiri (1.19.1-x86_64-linux-gnu) racc (~> 1.4) ostruct (0.6.3) @@ -308,6 +312,8 @@ GEM sinatra (>= 1.0) sqlite3 (2.6.0) mini_portile2 (~> 2.8.0) + sqlite3 (2.6.0-aarch64-linux-gnu) + sqlite3 (2.6.0-arm64-darwin) sqlite3 (2.6.0-x86_64-linux-gnu) sshkit (1.25.0) base64 @@ -325,6 +331,8 @@ GEM timeout (0.4.4) tiny_tds (3.4.0) bigdecimal (>= 2.0.0) + tiny_tds (3.4.0-aarch64-linux-gnu) + bigdecimal (>= 2.0.0) tsort (0.2.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) @@ -340,7 +348,10 @@ GEM zeitwerk (2.7.5) PLATFORMS + aarch64-linux + arm64-darwin ruby + x86-darwin x86_64-linux DEPENDENCIES @@ -437,6 +448,8 @@ CHECKSUMS net-ssh (7.3.1) sha256=229d518b429211bebd89151e2a12febff0631138513ac259953aa7b7cd42b53b nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1 nokogiri (1.19.1) sha256=598b327f36df0b172abd57b68b18979a6e14219353bca87180c31a51a00d5ad3 + nokogiri (1.19.1-aarch64-linux-gnu) sha256=cfdb0eafd9a554a88f12ebcc688d2b9005f9fce42b00b970e3dc199587b27f32 + nokogiri (1.19.1-arm64-darwin) sha256=dfe2d337e6700eac47290407c289d56bcf85805d128c1b5a6434ddb79731cb9e nokogiri (1.19.1-x86_64-linux-gnu) sha256=1a4902842a186b4f901078e692d12257678e6133858d0566152fe29cdb98456a ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912 parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130 @@ -482,6 +495,8 @@ CHECKSUMS sinatra (4.2.1) sha256=b7aeb9b11d046b552972ade834f1f9be98b185fa8444480688e3627625377080 sinatra-activerecord (2.0.28) sha256=99f352c2dfa244d02b4f877efbe00135360b758390b8bb7bc2d4d91171c93811 sqlite3 (2.6.0) sha256=a1c625f11948e6726eb082700283a8a3f4cf20b0548c0051c6104c56fedbe314 + sqlite3 (2.6.0-aarch64-linux-gnu) sha256=febc29bd7037695779d6b482fac7f7add9af7b420a1c5120ccff79213415975e + sqlite3 (2.6.0-arm64-darwin) sha256=88a793bf0010339e85657e460ab3ad2c7e9ee5d0e468b9dea383ff1039380056 sqlite3 (2.6.0-x86_64-linux-gnu) sha256=415a950be612b865152dd4529b16fdce2c8962ff9fa3f6b95adbfa7b8f54f2d0 sshkit (1.25.0) sha256=c8c6543cdb60f91f1d277306d585dd11b6a064cb44eab0972827e4311ff96744 stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1 @@ -490,6 +505,7 @@ CHECKSUMS tilt-jbuilder (0.7.1) sha256=f5841fd18217f6400a8894c3160190edc5dd3041063f854a388095ff6c732862 timeout (0.4.4) sha256=f0f6f970104b82427cd990680f539b6bbb8b1e55efa913a55c6492935e4e0edb tiny_tds (3.4.0) sha256=b43e1475cc4b2fa6674847a6a5b72aed99d3332840e1a9f195ca8b1f68653688 + tiny_tds (3.4.0-aarch64-linux-gnu) sha256=2bfad37bfd894b520e7998dbdc6811fbe7a9f40b4803d30b1307aca3e777fc39 tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42 From 9edfc7c2cbeca47b6832a50eb9b454080df12f78 Mon Sep 17 00:00:00 2001 From: benmelz Date: Mon, 16 Mar 2026 12:22:06 -0400 Subject: [PATCH 6/6] rails-y dockerfile --- Dockerfile | 52 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/Dockerfile b/Dockerfile index a549883..dfd4fe7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,49 +1,61 @@ # 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 fleetfocus-api --build-arg RUBY_VERSION="$(cat .ruby-version)" . +# docker run --interactive --tty --publish 80:80 --env MASTER_KEY="$(cat config/fleetfocus-api.key)" fleetfocus-api +# 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 -FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim AS base +# App lives here +WORKDIR /app -LABEL org.opencontainers.image.source=https://github.com/umts/fleetfocus-api -LABEL org.opencontainers.image.description="fleetfocus-api" -LABEL org.opencontainers.image.licenses=MIT +# Install base packages +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y curl libjemalloc2 freetds-dev && \ + 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" \ - BUNDLE_PATH="/usr/local/bundle" + LD_PRELOAD="/usr/local/lib/libjemalloc.so" -WORKDIR /app +# 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 pkg-config libyaml-dev freetds-dev + apt-get install --no-install-recommends -y build-essential libyaml-dev && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives +# 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 -FROM base - -RUN apt-get update -qq && \ - apt-get install --no-install-recommends -y freetds-dev && \ - rm -rf /var/lib/apt/lists /var/cache/apt/archives +# Copy application code +COPY . . -COPY --from=build /usr/local/bundle /usr/local/bundle -COPY . . +# Final stage for app image +FROM base -RUN useradd fleetfocus-api --create-home --shell /bin/bash && \ - mkdir -p log && \ - chown -R fleetfocus-api log +# Copy built artifacts: gems, application +COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}" +COPY --from=build /app /app -USER fleetfocus-api:fleetfocus-api +# Run and own only the runtime files as a non-root user for security +RUN groupadd --system --gid 1000 fleetfocus-api && \ + useradd fleetfocus-api --uid 1000 --gid 1000 --create-home --shell /bin/bash && \ + mkdir -p /app/log /app/tmp && chown -R fleetfocus-api:fleetfocus-api /app/log /app/tmp +USER 1000:1000 EXPOSE 80 - CMD ["script/server", "--port=80"]