rails-project-setup — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited rails-project-setup (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
The Solid Stack (solid_queue, solid_cache, solid_cable) plus propshaft, importmap-rails, kamal, and thruster — a complete Rails 8 setup with no Redis dependency.
# cooldown: gems must be public 4 days before resolving (supply-chain defense, Bundler 4.0.6+)
source "https://rubygems.org", cooldown: 4
gem "rails", "~> 8.1"
# Assets & front end
gem "importmap-rails" # JavaScript without Node bundling
gem "propshaft" # modern asset pipeline (replaces Sprockets)
gem "stimulus-rails"
gem "turbo-rails"
# Deployment and drivers
gem "bootsnap", require: false
gem "kamal", require: false # zero-downtime deployment
gem "puma", ">= 5.0"
gem "solid_cable", ">= 3.0" # database-backed ActionCable
gem "solid_cache", "~> 1.0" # database-backed cache store
gem "solid_queue", "~> 1.2" # database-backed job queue
gem "sqlite3", ">= 2.0"
gem "thruster", require: false # HTTP/2 proxy (replaces nginx for simple setups)
gem "trilogy", "~> 2.9" # MySQL adapter, faster than mysql2
# Operations
gem "mission_control-jobs" # web UI for Solid QueueBundler cooldown (supply-chain defense):
cooldown: 4 on the source line means gems must have been public for 4 days before Bundler will resolve them, so freshly-hijacked releases can be vetted/yanked first (announcement, Bundler 4.0.6+).bundle config set --global cooldown 4; the explicit per-Gemfile line documents the policy and survives global-config changes..ruby-lsp/Gemfile, or appraisal gemfiles.Vite + pnpm apps (alternative front-end stack):
app/assets. Don't migrate such an app to importmaps.config/queue.yml defines dispatchers and workers, with process counts derived from CPU count:
default: &default
dispatchers:
- polling_interval: 1
batch_size: 500
workers:
- queues: [ "default", "solid_queue_recurring" ]
threads: 3
processes: <%= Integer(ENV.fetch("JOB_CONCURRENCY") { Concurrent.physical_processor_count }) %>
polling_interval: 0.1
development: *default
test: *default
production: *defaultFor queue naming (SLO tiers, worker-per-queue), see [[rails-jobs]] Pattern 5.
# config/puma.rb — run Solid Queue inside Puma, no separate worker process
unless ENV["SOLID_QUEUE_IN_PUMA"] == "false"
plugin :solid_queue
end# config/cache.yml
default_options: &default_options
store_options:
max_age: <%= 60.days.to_i %>
namespace: <%= Rails.env %> # isolate cache per environment
default_connection: &default_connection
database: cache
default: &default
<<: *default_connection
<<: *default_options
development: *default
test: *default_options
production: *default# config/cable.yml
cable: &cable
adapter: solid_cable
connects_to:
database:
writing: cable
reading: cable
polling_interval: 0.1.seconds
message_retention: 1.day
development: *cable
test:
adapter: test
production: *cableSeparate databases for primary data, cache, queue, and cable prevent contention and allow independent scaling. Each gets its own migrations_paths.
# SQLite
default: &default
adapter: sqlite3
pool: 5
timeout: 5000
production:
primary:
<<: *default
database: storage/production.sqlite3
schema_dump: schema_sqlite.rb
cable:
<<: *default
database: storage/production_cable.sqlite3
migrations_paths: db/cable_migrate
cache:
<<: *default
database: storage/production_cache.sqlite3
migrations_paths: db/cache_migrate
queue:
<<: *default
database: storage/production_queue.sqlite3
migrations_paths: db/queue_migrate# MySQL via trilogy — note the much larger pool in production
default: &default
adapter: trilogy
host: <%= ENV.fetch("MYSQL_HOST", "127.0.0.1") %>
port: <%= ENV.fetch("MYSQL_PORT", "3306") %>
username: <%= ENV.fetch("MYSQL_USER", "root") %>
password: <%= ENV["MYSQL_PASSWORD"] %>
pool: 50
timeout: 5000
production:
primary:
<<: *default
database: app_production
cable:
<<: *default
database: app_production_cable
migrations_paths: db/cable_migrate
queue:
<<: *default
database: app_production_queue
migrations_paths: db/queue_migrate
cache:
<<: *default
database: app_production_cache
migrations_paths: db/cache_migrateconfig/deploy.yml: servers, automatic SSL via the proxy, secrets from .kamal/secrets, and persistent volumes for SQLite/Active Storage.
service: app
image: app
servers:
web:
- app.example.com
ssh:
user: root
# Automatic SSL
proxy:
ssl: true
host: app.example.com
# Secrets come from .kamal/secrets; clear vars are plain
env:
secret:
- SECRET_KEY_BASE
- SMTP_USERNAME
- SMTP_PASSWORD
clear:
BASE_URL: https://app.example.com
SOLID_QUEUE_IN_PUMA: true # jobs run in the web container
registry:
server: localhost:5555
aliases:
console: app exec --interactive --reuse "bin/rails console"
shell: app exec --interactive --reuse "bash"
logs: app logs -f
dbc: app exec --interactive --reuse "bin/rails dbconsole --include-password"
# Persistent storage for sqlite and Active Storage
volumes:
- "app_storage:/rails/storage"
# Bridge assets between versions during zero-downtime deploys
asset_path: /rails/public/assets
builder:
arch: amd64Multi-stage build with jemalloc, bootsnap precompilation, and a non-root user:
# syntax=docker/dockerfile:1
# check=error=true
ARG RUBY_VERSION=3.4.7
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base
WORKDIR /rails
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 libssl-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
ENV RAILS_ENV="production" \
BUNDLE_DEPLOYMENT="1" \
BUNDLE_PATH="/usr/local/bundle" \
BUNDLE_WITHOUT="development:test" \
LD_PRELOAD="/usr/local/lib/libjemalloc.so"
# Build stage
FROM base AS build
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
COPY Gemfile Gemfile.lock vendor ./
RUN bundle install && \
rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \
bundle exec bootsnap precompile -j 1 --gemfile
COPY . .
RUN bundle exec bootsnap precompile -j 1 app/ lib/
# SECRET_KEY_BASE_DUMMY=1 allows asset precompilation without real secrets
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
# Final stage
FROM base
RUN groupadd --system --gid 1000 rails && \
useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash
USER 1000:1000
COPY --chown=rails:rails --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"
COPY --chown=rails:rails --from=build /rails /rails
ENTRYPOINT ["/rails/bin/docker-entrypoint"]
EXPOSE 80
CMD ["./bin/thrust", "./bin/rails", "server"]#!/bin/bash -e
# bin/docker-entrypoint — auto-migrate on deploy when starting the server
if [ "${1}" == "./bin/thrust" ] && [ "${2}" == "./bin/rails" ] && [ "${3}" == "server" ]; then
MIGRATE=1 ./bin/rails db:prepare
fi
exec "${@}"Scheduled tasks live in config/recurring.yml:
production: &production
cleanup_magic_links:
command: "MagicLink.cleanup"
schedule: every 4 hoursFull pattern (command vs class, Solid Queue maintenance entries, schedule syntax): [[rails-jobs]] Pattern 4.
Workers per CPU, single thread per worker (optimal for SQLite's low I/O wait), copy-on-write warmup, and GC deferred to between requests:
# config/puma.rb
port ENV.fetch("PORT", 3000)
plugin :tmp_restart
unless ENV["SOLID_QUEUE_IN_PUMA"] == "false"
plugin :solid_queue
end
if !Rails.env.local?
# 1 worker per CPU, 1 thread per worker (optimal for SQLite)
workers Integer(ENV.fetch("WEB_CONCURRENCY") { Concurrent.physical_processor_count })
threads 1, 1
# Optimize for copy-on-write
before_fork do
Process.warmup
end
# Defer major GC until after request handling
before_worker_boot do
GC.config(rgengc_allow_full_mark: false)
end
out_of_band do
GC.start if GC.latest_gc_info(:need_major_by)
end
endcooldown: 4 on the source line)config/queue.yml, config/cache.yml, config/cable.ymlconfig/database.ymlplugin :solid_queue to config/puma.rbconfig/deploy.yml for Kamalconfig/recurring.yml for scheduled tasksbin/docker-entrypoint for auto-migrations# Required secrets (in .kamal/secrets)
SECRET_KEY_BASE=...
RAILS_MASTER_KEY=...
# Database (for MySQL)
MYSQL_HOST=127.0.0.1
MYSQL_PASSWORD=...
# Concurrency tuning
WEB_CONCURRENCY=4 # Puma workers
JOB_CONCURRENCY=4 # Solid Queue processes
SOLID_QUEUE_IN_PUMA=true # Run jobs in web process~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.