diff --git a/.kamal/hooks/docker-setup.sample b/.kamal/hooks/docker-setup.sample new file mode 100755 index 0000000..d914913 --- /dev/null +++ b/.kamal/hooks/docker-setup.sample @@ -0,0 +1,13 @@ +#!/usr/bin/env ruby + +# A sample docker-setup hook +# +# Sets up a Docker network on defined hosts which can then be used by the application’s containers + +hosts = ENV["KAMAL_HOSTS"].split(",") + +hosts.each do |ip| + destination = "root@#{ip}" + puts "Creating a Docker network \"kamal\" on #{destination}" + `ssh #{destination} docker network create kamal` +end diff --git a/.kamal/hooks/post-deploy.sample b/.kamal/hooks/post-deploy.sample new file mode 100755 index 0000000..75efafc --- /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_ROLE (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-traefik-reboot.sample b/.kamal/hooks/post-traefik-reboot.sample new file mode 100755 index 0000000..e3d9e3c --- /dev/null +++ b/.kamal/hooks/post-traefik-reboot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Rebooted Traefik on $KAMAL_HOSTS" diff --git a/.kamal/hooks/pre-build.sample b/.kamal/hooks/pre-build.sample new file mode 100755 index 0000000..f87d811 --- /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_ROLE (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..18e61d7 --- /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_ROLE (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..1b280c7 --- /dev/null +++ b/.kamal/hooks/pre-deploy.sample @@ -0,0 +1,109 @@ +#!/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_ROLE (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 = `git config --get remote.origin.url`.strip.delete_prefix("https://github.com/") + @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 +end + + +$stdout.sync = true + +puts "Checking build status..." +attempts = 0 +checks = GithubStatusChecks.new + +begin + 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-traefik-reboot.sample b/.kamal/hooks/pre-traefik-reboot.sample new file mode 100755 index 0000000..8cfda6d --- /dev/null +++ b/.kamal/hooks/pre-traefik-reboot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Rebooting Traefik on $KAMAL_HOSTS..." diff --git a/.ruby-version b/.ruby-version index fefb2b7..f13c6f4 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -ruby-3.3.3 +ruby-3.3.5 diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..1dd1998 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +ruby 3.3.5 diff --git a/Dockerfile b/Dockerfile index 37f08b0..9947667 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,51 +1,47 @@ # syntax = docker/dockerfile:1 -# This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand: -# docker build -t my-app . -# docker run -d -p 80:80 -p 443:443 --name my-app -e RAILS_MASTER_KEY= my-app - -# 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.3 -FROM docker.io/library/ruby:$RUBY_VERSION-slim as base +ARG RUBY_VERSION=3.3.5 +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 libsqlite3-0 && \ + apt-get install --no-install-recommends -y curl libjemalloc2 libsqlite3-0 \ + build-essential libssl-dev git pkg-config python-is-python3 libgmp-dev ca-certificates gnupg xz-utils && \ rm -rf /var/lib/apt/lists /var/cache/apt/archives # Set production environment ENV RAILS_ENV="production" \ + BUNDLE_WITHOUT="development:test" \ BUNDLE_DEPLOYMENT="1" \ - BUNDLE_PATH="/usr/local/bundle" \ - BUNDLE_WITHOUT="development" + BUNDLE_PATH="/usr/local/bundle" -# Throw-away build stage to reduce size of final image -FROM base as build +# Install Node.js and Yarn +ARG NODE_VERSION=22.3.0 +ARG YARN_VERSION=1.22.19 +ENV PATH=/usr/local/node/bin:/usr/local/bin:$PATH -# Install packages needed to build gems and node modules -RUN apt-get update -qq && \ - apt-get install --no-install-recommends -y build-essential git node-gyp pkg-config python-is-python3 && \ - rm -rf /var/lib/apt/lists /var/cache/apt/archives +RUN case "$(dpkg --print-architecture)" in \ + amd64) ARCH='x64' ;; \ + arm64) ARCH='arm64' ;; \ + *) echo "Unsupported architecture"; exit 1 ;; \ + esac \ + && curl -fsSL https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${ARCH}.tar.xz | tar -xJ -C /usr/local --strip-components=1 \ + && npm install -g yarn@$YARN_VERSION -# Install JavaScript dependencies -ARG NODE_VERSION=18.19.0 -ARG YARN_VERSION=1.22.19 -ENV PATH=/usr/local/node/bin:$PATH -RUN curl -sL https://github.com/nodenv/node-build/archive/master.tar.gz | tar xz -C /tmp/ && \ - /tmp/node-build-master/bin/node-build "${NODE_VERSION}" /usr/local/node && \ - npm install -g yarn@$YARN_VERSION && \ - rm -rf /tmp/node-build-master +# Verify Node.js and Yarn installation +RUN node --version && yarn --version + +# Throw-away build stage to reduce size of final image +FROM base AS build # 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 +RUN bundle config set --local build.nokogiri --use-system-libraries && \ + bundle install --jobs 4 --retry 3 && \ + rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git # Install node modules COPY package.json yarn.lock ./ @@ -58,28 +54,33 @@ COPY . . 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 - - -RUN rm -rf node_modules +RUN RAILS_ENV=production SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile +# RUN yarn vite build # Final stage for app image FROM base -# Copy built artifacts: gems, application +# Copy built artifacts: gems, application, and node_modules COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}" COPY --from=build /rails /rails +COPY --from=build /rails/node_modules /rails/node_modules # 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 /rails && \ + chmod -R 755 /rails && \ chown -R rails:rails db log storage tmp -USER 1000:1000 + +USER rails # Entrypoint prepares the database. ENTRYPOINT ["/rails/bin/docker-entrypoint"] +ENV HTTP_PORT="3000" \ + TARGET_PORT="3001" + # Start the server by default, this can be overwritten at runtime EXPOSE 3000 -CMD ["./bin/rails", "server"] +CMD ["bundle", "exec", "thrust", "./bin/rails", "server"] diff --git a/Gemfile b/Gemfile index 29640de..934b610 100644 --- a/Gemfile +++ b/Gemfile @@ -3,13 +3,18 @@ source "https://rubygems.org" gem "rails", github: "rails/rails", branch: "main" gem "bootsnap", require: false +gem "kamal", "~> 1.8.3", require: false +gem "mission_control-jobs", "~> 0.3.1" +gem "litestream", "~> 0.11.2" gem "propshaft", "~> 0.9.1" +gem "solid_cache", "~> 1.0.5" +gem "solid_queue", "~> 0.5.0" gem "sqlite3", "~> 2.0.4" gem "stimulus-rails" gem "turbo-rails" gem "puma", ">= 6.4.2" - gem "phlex-rails", "~> 1.2.1" +gem "thruster", "~> 0.1.8" gem "vite_rails", "~> 3.0.17" group :development do @@ -28,20 +33,10 @@ group :test do end group :development, :test do - gem "brakeman", "~> 6.2", ">= 6.2.1", require: false + gem "brakeman", "~> 6.2.1", require: false gem "debug", platforms: %i[ mri windows ], require: "debug/prelude" gem "rubocop-rails-omakase", require: false gem "amazing_print", "~> 1.6" gem "minio", "~> 0.4.0" gem "dotenv-rails" end -# Add Solid Queue for background jobs -gem "solid_queue", "~> 0.5.0" -# Add a web UI for Solid Queue -gem "mission_control-jobs", "~> 0.3.1" - - -# Add Solid Cache as an Active Support cache store -gem "solid_cache", "~> 1.0.1" -# Ensure all SQLite databases are backed up -gem "litestream", "~> 0.11.0" diff --git a/Gemfile.lock b/Gemfile.lock index f78dcba..819723b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/rails/rails.git - revision: 16882b886e245f363cd61f5ea553fe2bc525d070 + revision: dd3f743c97fef03815023d6dc9e4711f4401fa08 branch: main specs: actioncable (8.0.0.alpha) @@ -72,6 +72,7 @@ GIT minitest (>= 5.1) securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) rails (8.0.0.alpha) actioncable (= 8.0.0.alpha) actionmailbox (= 8.0.0.alpha) @@ -103,6 +104,9 @@ GEM amazing_print (1.6.0) ast (2.4.2) base64 (0.2.0) + bcrypt_pbkdf (1.1.1) + bcrypt_pbkdf (1.1.1-arm64-darwin) + bcrypt_pbkdf (1.1.1-x86_64-darwin) better_html (2.1.1) actionview (>= 6.0) activesupport (>= 6.0) @@ -136,12 +140,13 @@ GEM irb (~> 1.10) reline (>= 0.3.8) docile (1.4.1) - dotenv (3.1.2) - dotenv-rails (3.1.2) - dotenv (= 3.1.2) - railties (>= 6.1) + dotenv (2.8.1) + dotenv-rails (2.8.1) + dotenv (= 2.8.1) + railties (>= 3.2) drb (2.2.1) dry-cli (1.1.0) + ed25519 (1.3.0) erb_lint (0.6.0) activesupport better_html (>= 2.0.1) @@ -169,25 +174,52 @@ GEM rdoc (>= 4.0.0) reline (>= 0.4.2) json (2.7.2) + kamal (1.8.3) + activesupport (>= 7.0) + base64 (~> 0.2) + bcrypt_pbkdf (~> 1.0) + concurrent-ruby (~> 1.2) + dotenv (~> 2.8) + ed25519 (~> 1.2) + net-ssh (~> 7.0) + sshkit (>= 1.23.0, < 2.0) + thor (~> 1.2) + zeitwerk (~> 2.5) language_server-protocol (3.17.0.3) - litestream (0.11.0) - activejob + litestream (0.11.2) + actionpack (>= 7.0) + actionview (>= 7.0) + activejob (>= 7.0) + activesupport (>= 7.0) logfmt (>= 0.0.10) + railties (>= 7.0) sqlite3 - litestream (0.11.0-arm64-darwin) - activejob + litestream (0.11.2-arm64-darwin) + actionpack (>= 7.0) + actionview (>= 7.0) + activejob (>= 7.0) + activesupport (>= 7.0) logfmt (>= 0.0.10) + railties (>= 7.0) sqlite3 - litestream (0.11.0-x86_64-darwin) - activejob + litestream (0.11.2-x86_64-darwin) + actionpack (>= 7.0) + actionview (>= 7.0) + activejob (>= 7.0) + activesupport (>= 7.0) logfmt (>= 0.0.10) + railties (>= 7.0) sqlite3 - litestream (0.11.0-x86_64-linux) - activejob + litestream (0.11.2-x86_64-linux) + actionpack (>= 7.0) + actionview (>= 7.0) + activejob (>= 7.0) + activesupport (>= 7.0) logfmt (>= 0.0.10) + railties (>= 7.0) sqlite3 logfmt (0.0.10) - logger (1.6.0) + logger (1.6.1) loofah (2.22.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) @@ -215,15 +247,20 @@ GEM mocha (2.4.5) ruby2_keywords (>= 0.0.5) msgpack (1.7.2) - net-imap (0.4.14) + net-imap (0.4.15) date net-protocol net-pop (0.1.2) net-protocol net-protocol (0.2.2) timeout + net-scp (4.0.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.0) net-protocol + net-ssh (7.2.3) nio4r (2.7.3) nokogiri (1.16.7-aarch64-linux) racc (~> 1.4) @@ -237,6 +274,7 @@ GEM racc (~> 1.4) nokogiri (1.16.7-x86_64-linux) racc (~> 1.4) + ostruct (0.6.0) overcommit (0.64.0) childprocess (>= 0.6.3, < 6) iniparse (~> 1.4) @@ -335,7 +373,7 @@ GEM simplecov (~> 0.16) simplecov_json_formatter (0.1.4) smart_properties (1.17.0) - solid_cache (1.0.1) + solid_cache (1.0.5) activejob (>= 7.2) activerecord (>= 7.2) railties (>= 7.2) @@ -355,11 +393,22 @@ GEM sqlite3 (2.0.4-x86_64-darwin) sqlite3 (2.0.4-x86_64-linux-gnu) sqlite3 (2.0.4-x86_64-linux-musl) + sshkit (1.23.1) + base64 + net-scp (>= 1.1.2) + net-sftp (>= 2.1.2) + net-ssh (>= 2.8.0) + ostruct stimulus-rails (1.3.3) railties (>= 6.0.0) stringio (3.1.1) strscan (3.1.0) - thor (1.3.1) + thor (1.3.2) + thruster (0.1.8) + thruster (0.1.8-aarch64-linux) + thruster (0.1.8-arm64-darwin) + thruster (0.1.8-x86_64-darwin) + thruster (0.1.8-x86_64-linux) timeout (0.4.1) turbo-rails (2.0.6) actionpack (>= 6.0.0) @@ -368,6 +417,7 @@ GEM tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (2.5.0) + uri (0.13.1) useragent (0.16.10) vite_rails (3.0.17) railties (>= 5.1, < 8) @@ -388,7 +438,7 @@ GEM websocket-extensions (0.1.5) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.17) + zeitwerk (2.6.18) PLATFORMS aarch64-linux @@ -410,12 +460,13 @@ DEPENDENCIES amazing_print (~> 1.6) better_html bootsnap - brakeman (~> 6.2, >= 6.2.1) + brakeman (~> 6.2.1) capybara debug dotenv-rails erb_lint - litestream (~> 0.11.0) + kamal (~> 1.8.3) + litestream (~> 0.11.2) minio (~> 0.4.0) mission_control-jobs (~> 0.3.1) mocha (~> 2.4.5) @@ -428,10 +479,11 @@ DEPENDENCIES selenium-webdriver (~> 4.24) simplecov simplecov-tailwindcss - solid_cache (~> 1.0.1) + solid_cache (~> 1.0.5) solid_queue (~> 0.5.0) sqlite3 (~> 2.0.4) stimulus-rails + thruster (~> 0.1.8) turbo-rails vite_rails (~> 3.0.17) web-console diff --git a/README.md b/README.md index 7db80e4..8d5ad47 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,5 @@ # README -This README would normally document whatever steps are necessary to get the -application up and running. +* For local SQLite replication, start minio with: -Things you may want to cover: -* Ruby version - -* System dependencies - -* Configuration - -* Database creation - -* Database initialization - -* How to run the test suite - -* Services (job queues, cache servers, search engines, etc.) - -* Deployment instructions - -* ... diff --git a/app/views/static/about.html.erb b/app/views/static/about.html.erb index fc71b04..e2d29b7 100644 --- a/app/views/static/about.html.erb +++ b/app/views/static/about.html.erb @@ -1,6 +1,5 @@ <% content_for :title, "About" %> - -
+

Behind the Scenes

@@ -9,7 +8,7 @@ 👋! We are an indie maker couple currently living in sunny Valencia, Spain 🇪🇸. If you are in the area, drop by for a ☕️ (it's on us)!

-

<%= image_tag src="mia-trinity.jpg", class: "mb-5 mt-5" %>

+

<%= image_tag "mia-trinity.jpg", class: "mb-5 mt-5" %>

After working on everyone else’s dreams for almost two decades, we are ready to pursue our own. railsnew.io is our first baby step, but there’s @@ -25,7 +24,9 @@

diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint index 840d093..95b6622 100755 --- a/bin/docker-entrypoint +++ b/bin/docker-entrypoint @@ -6,7 +6,7 @@ if [ -z "${LD_PRELOAD+x}" ] && [ -f /usr/lib/*/libjemalloc.so.2 ]; then fi # If running the rails server then create or migrate existing database -if [ "${1}" == "./bin/rails" ] && [ "${2}" == "server" ]; then +if [ "${4}" == "./bin/rails" ] && [ "${5}" == "server" ]; then ./bin/rails db:prepare fi diff --git a/bin/jobs b/bin/jobs new file mode 100755 index 0000000..dcf59f3 --- /dev/null +++ b/bin/jobs @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby + +require_relative "../config/environment" +require "solid_queue/cli" + +SolidQueue::Cli.start(ARGV) diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc index 906cfdc..d8ca631 100644 --- a/config/credentials.yml.enc +++ b/config/credentials.yml.enc @@ -1 +1 @@ -SPW5xrLZ1+84kFkFr1qT3DwjRpiNnQ3gIUBQNoN02OZuHAmc9737RZ1DYBeBxBOf/jYQ2wojDIYuZOX/Uv5aS+cOBRj44ClrR1Pj/OuW6A58yJOGUOULPU7KPTUN/K6cQDtFFGZw7FYXLDd93fdQWY3MtnJGmG92yZSHenp38odgYjg4Ckbbhm+4WvjqWtMyT3TjNBcoCRTgIv9A2tvOc5Tk3qkSzZc0994qA738YXpRTNoPflgRSG04yvshsxpBaCcd/0neueqjEkSsFPlp3voFk7JGuknNcG7oIEtSj4a7h2jSp/ivlT6HEVibq9JmJp67P93/yrG1D8t1L6zmSR/YiEpuVFsiGtKDAzp/gYOoNO9IL2ZlslbIePpaVjB4FrnRuRx4X0PICr2J9A==--LL79p8iQRobarvOr--z5janW7FasH1MeAnN1A1gA== \ No newline at end of file +z76/MswTatHGoZwEWcfgBZqlXXm/b2R5VUrLNRXCfRuM7BAdbawzRFdCGQbOn4G+KH7VY7uuuUBu/QRcBdYyRokgswXW0caF0KBHNGIX/nHtaO5IQcdvN1BCGNDhYmGp1zlLseVpE5aVOz5+xEzEfhq8NLimuTsKY2pC/Bs6GSHTE3YCgn6MQAwEvMOawuSjhVIWnCueEP8NO5Ieeldik9YREj167EpVdkZKm+qtVpajZkvURcpbXahWZE4I+z0gsgtGWewSeRhZJeIWUAtDs4b/eiqUSCn63w5e+4DUJ48cPErWEzsvvi1r0GQQJ+Az1gxL1tHg+sPxW2I0aR0BFYr2yCmdYPiz+Aw0pGzEO0RK9SWjFH6EBwbaFFMZ2NS+7XRjM/msXzRktB8TaxVy--g6l/qrpWSssPflj1--9Qrr9rFok+KXFEmInjdMMA== \ No newline at end of file diff --git a/config/database.yml b/config/database.yml index e0b6ce0..895fc27 100644 --- a/config/database.yml +++ b/config/database.yml @@ -46,6 +46,6 @@ test: production: primary: <<: *default - # database: path/to/persistent/storage/production.sqlite3 + database: /rails/storage/production.sqlite3 queue: *queue cache: *cache diff --git a/config/deploy.yml b/config/deploy.yml new file mode 100644 index 0000000..9f39146 --- /dev/null +++ b/config/deploy.yml @@ -0,0 +1,65 @@ +service: rails-new-io +image: canny-code/rails-new-io + +volumes: + - "/storage:/rails/storage" + +ssh: + user: deploy + +servers: + web: + hosts: + - 5.161.247.143 + labels: + traefik.http.routers.rails_new_io.rule: Host(`alpha.railsnew.io`) + traefik.http.routers.rails_new_io_secure.entrypoints: websecure + traefik.http.routers.rails_new_io_secure.rule: Host(`alpha.railsnew.io`) + traefik.http.routers.rails_new_io_secure.tls: true + traefik.http.routers.rails_new_io_secure.tls.certresolver: letsencrypt + options: + network: "private" + job: + hosts: + - 5.161.247.143 + cmd: bundle exec rails solid_queue:start + options: + network: "private" + +traefik: + options: + publish: + - "443:443" + volume: + - "/letsencrypt/acme.json:/letsencrypt/acme.json" + network: "private" + args: + entryPoints.web.address: ":80" + entryPoints.websecure.address: ":443" + certificatesResolvers.letsencrypt.acme.email: "trinity@railsnew.io" + certificatesResolvers.letsencrypt.acme.storage: "/letsencrypt/acme.json" + certificatesResolvers.letsencrypt.acme.httpchallenge: true + certificatesResolvers.letsencrypt.acme.httpchallenge.entrypoint: web + +registry: + server: ghcr.io + username: trinitytakei + password: + - KAMAL_REGISTRY_PASSWORD + +builder: + multiarch: <%= ENV["CI"].present? ? false : true %> + cache: + type: registry + image: trinitytakei/rails-new-io-build-cache + options: mode=max,oci-mediatypes=true + +env: + clear: + HOST: alpha.railsnew.io + PORT: 3001 + RAILS_SERVE_STATIC_FILES: true + RAILS_LOG_TO_STDOUT: true + SKIP_COVERAGE: "1" + secret: + - RAILS_MASTER_KEY diff --git a/config/environments/production.rb b/config/environments/production.rb index f531155..cabe235 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -12,6 +12,8 @@ # Rake tasks automatically ignore this option for performance. config.eager_load = true + config.silence_healthcheck_path = "/up" + # Full error reports are disabled and caching is turned on. config.consider_all_requests_local = false config.action_controller.perform_caching = true @@ -21,7 +23,8 @@ # 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? # Enable serving of images, stylesheets, and JavaScripts from an asset server. # config.asset_host = "http://assets.example.com" @@ -37,7 +40,7 @@ # Assume all access to the app is happening through a SSL-terminating reverse proxy. # Can be used together with config.force_ssl for Strict-Transport-Security and secure cookies. - # config.assume_ssl = true + config.assume_ssl = true # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. config.force_ssl = true diff --git a/config/puma.rb b/config/puma.rb index cd851e9..bc5c07a 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -1,38 +1,17 @@ -# 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 starts a configurable number of processes (workers) and each process -# serves each request in a thread from an internal thread pool. -# -# 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 -# 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 `environment` that Puma will run in. rails_env = ENV.fetch("RAILS_ENV", "development") environment rails_env case rails_env when "production" - # If you are running more than 1 thread per process, the workers count - # should be equal to the number of processors (CPU cores) in production. - # - # Automatically detect the number of available processors in production. + app_path = ENV.fetch("APP_PATH") { Dir.pwd } + bind "unix://#{app_path}/tmp/puma.sock" + port ENV.fetch("PORT", 3000) + pidfile "#{app_path}/tmp/puma.pid" + state_path "#{app_path}/tmp/puma.state" + require "concurrent-ruby" workers_count = Integer(ENV.fetch("WEB_CONCURRENCY") { Concurrent.available_processor_count }) workers workers_count if workers_count > 1 @@ -41,18 +20,12 @@ when "development" # Specifies a very generous `worker_timeout` so that the worker # isn't killed by Puma when suspended by a debugger. + port ENV.fetch("PORT", 3000) worker_timeout 3600 end -# Specifies the `port` that Puma will listen on to receive requests; default is 3000. -port ENV.fetch("PORT", 3000) - -# Allow puma to be restarted by `bin/rails restart` command. plugin :tmp_restart -# Allow puma to manage Litestream replication process plugin :litestream -# Allow puma to manage Solid Queue's supervisor process plugin :solid_queue -# Only use a pidfile when requested pidfile ENV["PIDFILE"] if ENV["PIDFILE"] diff --git a/config/vite.json b/config/vite.json index 11d6efc..04f2741 100644 --- a/config/vite.json +++ b/config/vite.json @@ -15,5 +15,9 @@ "autoBuild": true, "publicOutputDir": "vite-test", "port": 3037 + }, + "production": { + "autoBuild": true, + "publicOutputDir": "vite" } } diff --git a/db/cache_schema.rb b/db/cache_schema.rb index 250103a..a3815ff 100644 --- a/db/cache_schema.rb +++ b/db/cache_schema.rb @@ -11,6 +11,14 @@ # It's strongly recommended that you check this file into your version control system. ActiveRecord::Schema[8.0].define(version: 2024_08_18_142932) do + create_table "_litestream_lock", id: false, force: :cascade do |t| + t.integer "id" + end + + create_table "_litestream_seq", force: :cascade do |t| + t.integer "seq" + end + create_table "solid_cache_entries", force: :cascade do |t| t.binary "key", limit: 1024, null: false t.binary "value", limit: 536870912, null: false diff --git a/db/queue_schema.rb b/db/queue_schema.rb index 5fbee0a..303e61c 100644 --- a/db/queue_schema.rb +++ b/db/queue_schema.rb @@ -11,6 +11,14 @@ # It's strongly recommended that you check this file into your version control system. ActiveRecord::Schema[8.0].define(version: 2024_08_18_142916) do + create_table "_litestream_lock", id: false, force: :cascade do |t| + t.integer "id" + end + + create_table "_litestream_seq", force: :cascade do |t| + t.integer "seq" + end + create_table "solid_queue_blocked_executions", force: :cascade do |t| t.integer "job_id", null: false t.string "queue_name", null: false diff --git a/db/schema.rb b/db/schema.rb index a02f94d..f30c6ef 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,6 +11,14 @@ # It's strongly recommended that you check this file into your version control system. ActiveRecord::Schema[8.0].define(version: 2024_09_01_091744) do + create_table "_litestream_lock", id: false, force: :cascade do |t| + t.integer "id" + end + + create_table "_litestream_seq", force: :cascade do |t| + t.integer "seq" + end + create_table "solid_queue_blocked_executions", force: :cascade do |t| t.integer "job_id", null: false t.string "queue_name", null: false diff --git a/lib/tasks/test.rake b/lib/tasks/test.rake index c43ab63..74d0e4d 100644 --- a/lib/tasks/test.rake +++ b/lib/tasks/test.rake @@ -1,5 +1,10 @@ -require "simplecov" -require "simplecov-tailwindcss" +require_relative "../../test/support/coverage_helper" +include CoverageHelper + +unless skip_coverage? + require "simplecov" + require "simplecov-tailwindcss" +end namespace :test do desc "Run all tests and collate coverage" diff --git a/package.json b/package.json index e550d16..d935300 100644 --- a/package.json +++ b/package.json @@ -11,12 +11,12 @@ "autoprefixer": "^10.4.19", "esbuild": "^0.23.0", "postcss": "^8.4.38", - "tailwindcss": "^3.4.4" + "tailwindcss": "^3.4.4", + "vite": "^5.0.0", + "vite-plugin-ruby": "^5.0.0" }, "devDependencies": { - "vite": "^5.0.0", "vite-plugin-full-reload": "^1.2.0", - "vite-plugin-ruby": "^5.0.0", "vite-plugin-stimulus-hmr": "^3.0.0" } } diff --git a/test/support/coverage_helper.rb b/test/support/coverage_helper.rb index bdc3a01..ad3f012 100644 --- a/test/support/coverage_helper.rb +++ b/test/support/coverage_helper.rb @@ -1,5 +1,5 @@ module CoverageHelper def skip_coverage? - ENV["SKIP_COVERAGE"] == "1" + Rails.env.production? || ENV["SKIP_COVERAGE"] == "1" end end