From 0587c03bcabbcfa6be6ef6298fc35f49cb65e786 Mon Sep 17 00:00:00 2001 From: Raghu Betina Date: Fri, 10 Jul 2026 01:00:54 -0500 Subject: [PATCH] Harden free Render runtime --- Dockerfile | 6 +++++- app/jobs/application_job.rb | 3 +++ config/cache.yml | 3 ++- config/credentials.yml.enc | 1 - config/puma.rb | 24 +++++++++++++----------- config/routes.rb | 3 +++ render.yaml | 3 +-- spec/jobs/application_job_spec.rb | 7 +++++++ spec/requests/health_spec.rb | 9 +++++++++ 9 files changed, 43 insertions(+), 16 deletions(-) delete mode 100644 config/credentials.yml.enc create mode 100644 spec/jobs/application_job_spec.rb create mode 100644 spec/requests/health_spec.rb diff --git a/Dockerfile b/Dockerfile index 56d7553..6e0472d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ # This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand: # docker build -t rails_8_template . -# docker run -d -p 80:80 -e RAILS_MASTER_KEY= --name rails_8_template rails_8_template +# docker run -d -p 80:80 -e SECRET_KEY_BASE= --name rails_8_template rails_8_template # For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html @@ -71,4 +71,8 @@ ENTRYPOINT ["/rails/bin/docker-entrypoint"] # Start server via Thruster by default, this can be overwritten at runtime EXPOSE 80 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ + CMD curl -fsS http://localhost/up || exit 1 + CMD ["./bin/thrust", "./bin/rails", "server"] diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb index d394c3d..10744a6 100644 --- a/app/jobs/application_job.rb +++ b/app/jobs/application_job.rb @@ -1,4 +1,7 @@ class ApplicationJob < ActiveJob::Base + # Keep enqueue semantics portable even though jobs currently share the primary database. + self.enqueue_after_transaction_commit = true + # Automatically retry jobs that encountered a deadlock # retry_on ActiveRecord::Deadlocked diff --git a/config/cache.yml b/config/cache.yml index ae55ceb..a0a6208 100644 --- a/config/cache.yml +++ b/config/cache.yml @@ -2,7 +2,8 @@ default: &default store_options: # Cap age of oldest cache entry to fulfill retention policies # max_age: <%= 60.days.to_i %> - max_size: <%= 256.megabytes %> + # Leave most of Supabase Free's database quota for application data and Solid Queue/Cable. + max_size: <%= Integer(ENV.fetch("SOLID_CACHE_MAX_SIZE_MB", 64)).megabytes %> namespace: <%= Rails.env %> development: diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc deleted file mode 100644 index 0862cad..0000000 --- a/config/credentials.yml.enc +++ /dev/null @@ -1 +0,0 @@ -ALyXtwewMftngG8pOZ9UCpnB2t0XYpbUYq1qXoq60mOr98ImAUnGljPow+p5ghvV+7RDbY/4fuN4ugTTSKEtloAbrP/JilHIWuMg8BhGnelA7UtOjdmNPVCsIXzf6oWPReL8Tm+KlO8OzAuIEtd+3/CCsBHo4VsYMhehB6F02Cd3ZJTWDvvSTXPYhsCgFF6J5NEwiE2YflditWsc6JQmk+47yE5VZTXe4oY2Kcf9rStG6Hjvi88Pt5dkYHWzGLxRgpsxWLAZCHrHkp8/O4aZD+rINjEZe3ntLAeQ1yff9IS7qaEKDqWAPGvqDeX4zAQ8yA2SZxydJXFtXdLpkKhhvK9QwIUdX+yHGlTG+DmP2aHvU5LLBqpGOe4SBUEc18/NXNuMViOsKjEQpGnoNCrneB7kZJw8VDhE3GSgIG8QSGhuH1qEEzX/EfomqH4Baq9X7eb6g5KWeBYZuOIeAN8yhDCfwiFqRq2evwtU+sKxx9Zchv9066JlqF1d--knZ79i6KRvRWhuPQ--yOd82TCMuPt/Aad0SF7kiw== \ No newline at end of file diff --git a/config/puma.rb b/config/puma.rb index 2978d42..fe1eb10 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -33,18 +33,20 @@ # Allow puma to be restarted by `bin/rails restart` command. plugin :tmp_restart -# Run the Solid Queue supervisor inside of Puma for single-server deployments -plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"] +# Run the Solid Queue supervisor inside of Puma for single-server deployments. +if ENV["SOLID_QUEUE_IN_PUMA"] == "true" + plugin :solid_queue -# Run Solid Queue in async mode: worker/dispatcher/scheduler run as threads -# inside the Puma master process instead of being forked into 4 subprocesses. -# Required on Render's 512MB free plan — fork mode adds ~460MB of process -# overhead (supervisor + 3 children) and pushes the container into OOM-loop. -# Async mode collapses that to ~50MB of additional thread overhead in Puma -# master. Trade-off: less isolation between SQ threads and Puma — a hung or -# leaky SQ thread affects request serving. For low-traffic student projects -# the trade-off is right; revisit if upgrading to a paid plan with more RAM. -solid_queue_mode :async if ENV["SOLID_QUEUE_IN_PUMA"] + # Run Solid Queue in async mode: worker/dispatcher/scheduler run as threads + # inside the Puma master process instead of being forked into 4 subprocesses. + # Required on Render's 512MB free plan — fork mode adds ~460MB of process + # overhead (supervisor + 3 children) and pushes the container into OOM-loop. + # Async mode collapses that to ~50MB of additional thread overhead in Puma + # master. Trade-off: less isolation between SQ threads and Puma — a hung or + # leaky SQ thread affects request serving. For low-traffic student projects + # the trade-off is right; revisit if upgrading to a paid plan with more RAM. + solid_queue_mode :async +end # Specify the PID file. Defaults to tmp/pids/server.pid in development. # In other environments, only set the PID file if requested. diff --git a/config/routes.rb b/config/routes.rb index bdb9595..f24717a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,7 @@ Rails.application.routes.draw do + # Liveness only: 200 when Rails boots successfully, 500 otherwise. + get "up" => "rails/health#show", as: :rails_health_check + # This is a blank app! Pick your first screen, build out the RCAV, and go from there. E.g.: # get("/your_first_screen", { :controller => "pages", :action => "first" }) end diff --git a/render.yaml b/render.yaml index 4e7e3d2..c7d3035 100644 --- a/render.yaml +++ b/render.yaml @@ -5,6 +5,7 @@ services: plan: free # make sure to set this to free or you'll get billed $$$ region: ohio # set this to the SAME region as your database, near your users (Supabase "East US (Ohio)" = ohio) to avoid slow cross-region queries dockerfilePath: ./Dockerfile + healthCheckPath: /up envVars: # this section sets some ENV variables needed by Render for deployment - key: SECRET_KEY_BASE generateValue: true @@ -15,8 +16,6 @@ services: value: "0" # Puma single mode (no forked workers); greater value causes OOM on 512 MB free tier - key: RAILS_MAX_THREADS value: "3" # Cap threads to reduce memory - - key: MALLOC_ARENA_MAX - value: "2" # Reduce memory fragmentation - key: SOLID_QUEUE_IN_PUMA value: "true" # Run background jobs in same process - key: BUNDLE_WITHOUT diff --git a/spec/jobs/application_job_spec.rb b/spec/jobs/application_job_spec.rb new file mode 100644 index 0000000..a0e3ad7 --- /dev/null +++ b/spec/jobs/application_job_spec.rb @@ -0,0 +1,7 @@ +require "rails_helper" + +RSpec.describe ApplicationJob do + it "defers enqueues until the surrounding transaction commits" do + expect(described_class.enqueue_after_transaction_commit).to be(true) + end +end diff --git a/spec/requests/health_spec.rb b/spec/requests/health_spec.rb new file mode 100644 index 0000000..40e5fec --- /dev/null +++ b/spec/requests/health_spec.rb @@ -0,0 +1,9 @@ +require "rails_helper" + +RSpec.describe "Health check", type: :request do + it "reports that Rails booted" do + get "/up" + + expect(response).to have_http_status(:ok) + end +end