Skip to content

Instantly share code, notes, and snippets.

@zealot128
Created April 22, 2025 08:20
Show Gist options
  • Save zealot128/3d4cf52406964aec86834ef096c70356 to your computer and use it in GitHub Desktop.
Save zealot128/3d4cf52406964aec86834ef096c70356 to your computer and use it in GitHub Desktop.
Kamal on Rails app - Diff
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..325bfc0
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,51 @@
+# See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files.
+
+# Ignore git directory.
+/.git/
+/.gitignore
+
+# Ignore bundler config.
+/.bundle
+
+# Ignore all environment files.
+/.env*
+
+# Ignore all default key files.
+/config/master.key
+/config/credentials/*.key
+
+# Ignore all logfiles and tempfiles.
+/log/*
+/tmp/*
+!/log/.keep
+!/tmp/.keep
+
+# Ignore pidfiles, but keep the directory.
+/tmp/pids/*
+!/tmp/pids/.keep
+
+# Ignore storage (uploaded files in development and any SQLite databases).
+/storage/*
+!/storage/.keep
+/tmp/storage/*
+!/tmp/storage/.keep
+
+# Ignore assets.
+/node_modules/
+/app/assets/builds/*
+!/app/assets/builds/.keep
+/public/assets
+
+# Ignore CI service files.
+/.github
+
+# Ignore Kamal files.
+/config/deploy*.yml
+/.kamal
+
+# Ignore development files
+/.devcontainer
+
+# Ignore Docker-related files
+/.dockerignore
+/Dockerfile*
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..f2f62d8
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,72 @@
+# 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 -t testapp .
+# docker run -d -p 80:80 -e RAILS_MASTER_KEY=<value from config/master.key> --name testapp testapp
+
+# 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.3.0
+FROM docker.io/library/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 libvips postgresql-client && \
+ rm -rf /var/lib/apt/lists /var/cache/apt/archives
+
+# Set production environment
+ENV 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 libpq-dev libyaml-dev pkg-config && \
+ rm -rf /var/lib/apt/lists /var/cache/apt/archives
+
+# 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 storage tmp
+USER 1000:1000
+
+# Entrypoint prepares the database.
+ENTRYPOINT ["/rails/bin/docker-entrypoint"]
+
+# Start server via Thruster by default, this can be overwritten at runtime
+EXPOSE 80
+CMD ["./bin/thrust", "./bin/rails", "server"]
diff --git a/Gemfile b/Gemfile
index 0f4d642..7c975ac 100644
--- a/Gemfile
+++ b/Gemfile
@@ -28,6 +28,12 @@ gem "solid_queue"
# Reduces boot times through caching; required in config/boot.rb
gem "bootsnap", require: false
+# Deploy this application anywhere as a Docker container [https://kamal-deploy.org]
+gem "kamal", require: false
+
+# Add HTTP asset caching/compression and X-Sendfile acceleration to Puma [https://github.com/basecamp/thruster/]
+gem "thruster", require: false
+
# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
# gem "image_processing", "~> 1.2"
diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint
new file mode 100755
index 0000000..57567d6
--- /dev/null
+++ b/bin/docker-entrypoint
@@ -0,0 +1,14 @@
+#!/bin/bash -e
+
+# 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
+
+# If running the rails server then create or migrate existing database
+if [ "${@: -2:1}" == "./bin/rails" ] && [ "${@: -1:1}" == "server" ]; then
+ ./bin/rails db:prepare
+fi
+
+exec "${@}"
diff --git a/bin/thrust b/bin/thrust
new file mode 100755
index 0000000..36bde2d
--- /dev/null
+++ b/bin/thrust
@@ -0,0 +1,5 @@
+#!/usr/bin/env ruby
+require "rubygems"
+require "bundler/setup"
+
+load Gem.bin_path("thruster", "thrust")
diff --git a/config/deploy.yml b/config/deploy.yml
new file mode 100644
index 0000000..e278caa
--- /dev/null
+++ b/config/deploy.yml
@@ -0,0 +1,97 @@
+# 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 one server).
+# If using something like Cloudflare, it is recommended to set encryption mode
+# in Cloudflare's SSL/TLS setting to "Full" to enable end-to-end encryption.
+proxy:
+ ssl: true
+ host: app.example.com
+ # kamal-proxy connects to your container over port 80, use `app_port` to specify a different port.
+ # app_port: 3000
+
+# Credentials for your image host.
+registry:
+ # 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
+
+# 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 <alias>". You can overwrite arguments on invocation:
+# "bin/kamal 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: redis:7.0
+# host: 192.168.0.2
+# port: 6379
+# directories:
+# - data:/data
diff --git a/config/environments/production.rb b/config/environments/production.rb
index 559671c..1749607 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -24,6 +24,9 @@ Rails.application.configure do
# Store uploaded files on the local file system (see config/storage.yml for options).
config.active_storage.service = :local
+ # Assume all access to the app is happening through a SSL-terminating reverse proxy.
+ config.assume_ssl = true
+
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
config.force_ssl = true
@@ -37,6 +40,9 @@ Rails.application.configure do
# Change to "debug" to log everything (including potentially personally-identifiable information!)
config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info")
+ # Prevent health checks from clogging up the logs.
+ config.silence_healthcheck_path = "/up"
+
# Don't log any deprecations.
config.active_support.report_deprecations = false
diff --git a/config/routes.rb b/config/routes.rb
index ebedc25..48254e8 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -3,6 +3,7 @@ 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.
+ get "up" => "rails/health#show", as: :rails_health_check
# Render dynamic PWA files from app/views/pwa/* (remember to link manifest in application.html.erb)
# get "manifest" => "rails/pwa#manifest", as: :pwa_manifest
--
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment