From 4cdf648630780f17c811777a1d63558af6ce3a6c Mon Sep 17 00:00:00 2001 From: Justin <16829344+jmilljr24@users.noreply.github.com> Date: Sat, 9 Nov 2024 18:07:34 -0500 Subject: [PATCH] Kamal deploy azure (#1117) * Remove some extra routes leading to 500 * Kamal init * Add production credentials -- still need real master key though * Shift to use the Dockerfile suggested by Rails * Allow secrets to be included * Silence storage lookup and fix up DB creds * Enable public_file_server to handle fetching assets * WIP * Bust * Stop trying to use azure storage for now * Skip over the need for Azure to be defined when doing precomppile * Add basic comment on whyu * Add missing docker-entrypoint * Update secrets * Remove extra key * Fix invalid encryption for credentials * Fix indentation * Does not like this * Add healthcheck place Up WIP WIP Try to make healthcheck work WIP Does this help Use scss to ensure compiling happens Up Fix registry WIP Fx Prolly cheating here Build Up Silence WIP W WI WIP P Use propshaft because it is easier WIP WHat Now it is working WIP eXclude one placements Up Clean up * Remove code we do not need * Fix up to use the new credentials space * Update * Adding notes * Starting documentation * Ok * ok * Ok' * WIP * test * test deploy * change domain * change name * fix * ssh * clean up delete * update dockerhub * dockerhub space * standard --------- Co-authored-by: Edwin Mak --- .gitignore | 2 + .kamal/hooks/docker-setup.sample | 3 + .kamal/hooks/post-deploy.sample | 14 +++ .kamal/hooks/post-proxy-reboot.sample | 3 + .kamal/hooks/pre-build.sample | 51 ++++++++ .kamal/hooks/pre-connect.sample | 47 ++++++++ .kamal/hooks/pre-deploy.sample | 109 ++++++++++++++++++ .kamal/hooks/pre-proxy-reboot.sample | 3 + .kamal/secrets | 21 ++++ DEPLOYMENT.md | 55 +++++++++ Dockerfile | 74 ++++++++++++ Gemfile | 8 +- Gemfile.lock | 53 ++++++--- app/assets/config/manifest.js | 4 - app/assets/stylesheets/application.scss | 3 +- app/components/avatar_component.rb | 2 +- app/controllers/root_controller.rb | 6 +- app/models/concerns/avatarable.rb | 4 +- app/views/layouts/shared/_head.html.erb | 3 + bin/docker-entrypoint | 13 +++ config/application.rb | 9 +- config/credentials/production.yml.enc | 1 + config/database.yml | 9 +- config/deploy.yml | 103 +++++++++++++++++ config/environments/production.rb | 4 +- config/initializers/assets.rb | 15 --- config/initializers/dartsass.rb | 5 + config/routes.rb | 11 +- config/storage.yml | 2 +- docker-compose.yml | 4 +- dockerfile => dockerfile.dev | 0 lib/tasks/credentials.rake | 10 ++ .../{theme.min.css => theme.min.scss} | 0 33 files changed, 592 insertions(+), 59 deletions(-) create mode 100755 .kamal/hooks/docker-setup.sample create mode 100755 .kamal/hooks/post-deploy.sample create mode 100755 .kamal/hooks/post-proxy-reboot.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 DEPLOYMENT.md create mode 100644 Dockerfile delete mode 100644 app/assets/config/manifest.js create mode 100755 bin/docker-entrypoint create mode 100644 config/credentials/production.yml.enc create mode 100644 config/deploy.yml delete mode 100644 config/initializers/assets.rb create mode 100644 config/initializers/dartsass.rb rename dockerfile => dockerfile.dev (100%) create mode 100644 lib/tasks/credentials.rake rename vendor/assets/stylesheets/{theme.min.css => theme.min.scss} (100%) diff --git a/.gitignore b/.gitignore index 62d13e170..773f7ecee 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,5 @@ # OS .DS_Store + +/config/credentials/production.key diff --git a/.kamal/hooks/docker-setup.sample b/.kamal/hooks/docker-setup.sample new file mode 100755 index 000000000..2fb07d7d7 --- /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-deploy.sample b/.kamal/hooks/post-deploy.sample new file mode 100755 index 000000000..75efafc10 --- /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-proxy-reboot.sample b/.kamal/hooks/post-proxy-reboot.sample new file mode 100755 index 000000000..1435a677f --- /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-build.sample b/.kamal/hooks/pre-build.sample new file mode 100755 index 000000000..f87d81130 --- /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 000000000..18e61d7e5 --- /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 000000000..1b280c719 --- /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-proxy-reboot.sample b/.kamal/hooks/pre-proxy-reboot.sample new file mode 100755 index 000000000..061f8059e --- /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 000000000..cba127199 --- /dev/null +++ b/.kamal/secrets @@ -0,0 +1,21 @@ +# 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=$(RAILS_ENV=production KEY=deploy,kamal_registry_password, bin/rails credentials:read) +DB_HOST=$(RAILS_ENV=production KEY=deploy,db_host, bin/rails credentials:read) +DB_USER=$(RAILS_ENV=production KEY=deploy,db_user, bin/rails credentials:read) +DB_PASSWORD=$(RAILS_ENV=production KEY=deploy,db_password, bin/rails credentials:read) + +# Option 2: Read secrets via a command +RAILS_MASTER_KEY=$(cat config/credentials/production.key) + +# 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/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 000000000..881e79ade --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,55 @@ +# Deploying To Azure + +## Introduction + +This document aims to provide a step-by-step guide to deploying this +application to Azure using Kamal 2.0. Since this application is using Rails 7.2 since +the writing of this document, the codebase lacks some required configuration +changes included in Rails 8 by default. Furthermore, Azure is a bit different +and requires some considerations not explicitly mentioned in the documentation. + +This guide focuses just deploying the application to a single VM instance on Azure. +It doesn't cover setting up the database on the same VM instance or anything +else. For this guide we are going to setup a domain to point to a Azure +VM running the application with SSL + a external database also hosted +on Azure. + +## Prerequisites + +Here are some key things you need before starting this guide and why: + +- Dockerhub account: You need this to store the Docker image of the application between +deployments +- Azure Account: You need this to create the VM instance and the database. +- `production.key`: Needed to decrypt the credentials in the production +environment which contain the kamal secrets. Ask leads for this file +if you don't have it. + +## Step 1: Initial Kamal Setup + +First, you need to setup Kamal on your local machine. You can follow the guide +[here](https://kamal-deploy.org/docs/installation/) here. You should have the +`kamal` command and all extra files needed for deployment. Notably, you'll be +modifying just two files `config/deploy.yml` and `.kamal/secrets` in the project. + +### Step 1.1: Setup `config/deploy.yml` + +You will need to do a few modifications here to make sure the deployment works + + +- Change the `service`, `app_name`, and `servers` to match the application's name +``` +# Name of your application. Used to uniquely configure containers. +service: homeward-tails # Name of the application. + +# Name of the container image. +image: edwinthinks/homeward-tails + +# Deploy to these servers. +servers: + web: + - tails.edwinmak.com +``` + + +## Step 2: Setup Azure diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..2fb1b77e1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,74 @@ +# 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 + +# 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 + +RUN echo "Bust" + +# 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 + +# Install Node.js +# [edwin] - this appears to be necessary to deploy our application even though we are using Dartsass +RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - && \ + apt-get install -y nodejs && \ + 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 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 PRECOMPILING=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 the server by default, this can be overwritten at runtime +EXPOSE 3000 +CMD ["./bin/rails", "server"] diff --git a/Gemfile b/Gemfile index a764fa212..7a02b5a63 100644 --- a/Gemfile +++ b/Gemfile @@ -6,8 +6,11 @@ ruby "3.3.0" # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" gem "rails", "~> 7.2.1" -# The original asset pipeline for Rails [https://github.com/rails/sprockets-rails] -gem "sprockets-rails" +# Used to deploy to production +gem "kamal" + +# Used for handling asset delivery +gem "propshaft" # figaro to handle ENV variables for postgresql gem "figaro" @@ -57,7 +60,6 @@ gem "devise" gem "devise_invitable", "~> 2.0.9" # Use Sass to process CSS -gem "dartsass-sprockets" gem "dartsass-rails" # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] diff --git a/Gemfile.lock b/Gemfile.lock index 4d7865804..34b7d36a5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -101,6 +101,9 @@ GEM nokogiri (~> 1, >= 1.10.8) base64 (0.2.0) bcrypt (3.1.20) + bcrypt_pbkdf (1.1.1) + bcrypt_pbkdf (1.1.1-arm64-darwin) + bcrypt_pbkdf (1.1.1-x86_64-darwin) better_errors (2.10.1) erubi (>= 1.0.0) rack (>= 0.9.0) @@ -140,12 +143,6 @@ GEM dartsass-rails (0.5.1) railties (>= 6.0.0) sass-embedded (~> 1.63) - dartsass-sprockets (3.1.0) - railties (>= 4.0.0) - sassc-embedded (~> 1.69) - sprockets (> 3.0) - sprockets-rails - tilt date (3.3.4) debug (1.9.2) irb (~> 1.10) @@ -160,6 +157,7 @@ GEM actionmailer (>= 5.0) devise (>= 4.6) docile (1.4.0) + dotenv (3.1.4) drb (2.2.1) dry-core (1.0.1) concurrent-ruby (~> 1.0) @@ -177,6 +175,7 @@ GEM dry-inflector (~> 1.0) dry-logic (~> 1.4) zeitwerk (~> 2.6) + ed25519 (1.3.0) em-websocket (0.5.3) eventmachine (>= 0.12.9) http_parser.rb (~> 0) @@ -274,6 +273,17 @@ GEM actionview (>= 5.0.0) activesupport (>= 5.0.0) json (2.7.2) + kamal (2.2.2) + activesupport (>= 7.0) + base64 (~> 0.2) + bcrypt_pbkdf (~> 1.0) + concurrent-ruby (~> 1.2) + dotenv (~> 3.1) + ed25519 (~> 1.2) + net-ssh (~> 7.0) + sshkit (>= 1.23.0, < 2.0) + thor (~> 1.3) + zeitwerk (~> 2.5) language_server-protocol (3.17.0.3) launchy (3.0.0) addressable (~> 2.8) @@ -315,8 +325,13 @@ GEM 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.3.0) nio4r (2.7.3) nokogiri (1.16.7) mini_portile2 (~> 2.8.2) @@ -331,6 +346,7 @@ GEM nenv (~> 0.1) shellany (~> 0.0) orm_adapter (0.5.0) + ostruct (0.6.0) pagy (9.1.0) parallel (1.26.3) parser (3.3.5.0) @@ -339,6 +355,11 @@ GEM pg (1.5.8) phonelib (0.9.2) popper_js (2.11.8) + propshaft (1.1.0) + actionpack (>= 7.0.0) + activesupport (>= 7.0.0) + rack + railties (>= 7.0.0) pry (0.14.2) coderay (~> 1.1) method_source (~> 1.0) @@ -441,8 +462,6 @@ GEM google-protobuf (~> 4.26) sass-embedded (1.77.8-x86_64-linux-gnu) google-protobuf (~> 4.26) - sassc-embedded (1.70.1) - sass-embedded (~> 1.70) securerandom (0.3.1) shellany (0.0.1) shoulda (4.0.0) @@ -457,13 +476,12 @@ GEM simplecov_json_formatter (~> 0.1) simplecov-html (0.12.3) simplecov_json_formatter (0.1.4) - sprockets (4.2.1) - concurrent-ruby (~> 1.0) - rack (>= 2.2.4, < 4) - sprockets-rails (3.5.2) - actionpack (>= 6.1) - activesupport (>= 6.1) - sprockets (>= 3.0.0) + sshkit (1.23.2) + base64 + net-scp (>= 1.1.2) + net-sftp (>= 2.1.2) + net-ssh (>= 2.8.0) + ostruct standard (1.41.1) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.0) @@ -482,7 +500,6 @@ GEM strong_migrations (2.0.2) activerecord (>= 6.1) thor (1.3.2) - tilt (2.3.0) timeout (0.4.1) traceroute (0.8.1) rails (>= 3.0.0) @@ -536,7 +553,6 @@ DEPENDENCIES city-state (~> 1.1.0) cuprite dartsass-rails - dartsass-sprockets debug devise devise_invitable (~> 2.0.9) @@ -551,11 +567,13 @@ DEPENDENCIES guard-livereload (~> 2.5, >= 2.5.2) importmap-rails jbuilder + kamal letter_opener mocha pagy pg (~> 1.5) phonelib + propshaft pry (~> 0.14.2) puma (~> 6.4.3) rails (~> 7.2.1) @@ -567,7 +585,6 @@ DEPENDENCIES shoulda (~> 4.0) shoulda-matchers simplecov - sprockets-rails standard stimulus-rails strong_migrations (~> 2.0) diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js deleted file mode 100644 index 0e3c18927..000000000 --- a/app/assets/config/manifest.js +++ /dev/null @@ -1,4 +0,0 @@ -//= link_tree ../images -//= link_tree ../fonts -//= link_tree ../../javascript .js -//= link_tree ../builds diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 65c076b25..3328cadf8 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -4,8 +4,7 @@ @import "rails_bootstrap_forms"; // Geek theme (https://geeksui.codescandy.com/) -@import 'theme.min.css'; -@import 'feather.css'; +// @import 'theme.min.scss'; // Custom Styling @import 'custom'; diff --git a/app/components/avatar_component.rb b/app/components/avatar_component.rb index c24dc1163..0e7a5b68f 100644 --- a/app/components/avatar_component.rb +++ b/app/components/avatar_component.rb @@ -9,7 +9,7 @@ class AvatarComponent < ApplicationComponent def avatar if image_url - image_tag(url_for(image_url), alt: alt, class: image_classes) + image_tag(url(image_url), alt: alt, class: image_classes) else content_tag(:span, initials, class: initials_classes) end diff --git a/app/controllers/root_controller.rb b/app/controllers/root_controller.rb index 8f0765827..84416ca13 100644 --- a/app/controllers/root_controller.rb +++ b/app/controllers/root_controller.rb @@ -1,10 +1,14 @@ class RootController < ApplicationController skip_before_action :authenticate_user! - skip_verify_authorized only: %i[index] + skip_verify_authorized only: %i[index up] def index if Current.organization redirect_to home_index_path end end + + def up + render plain: "ok" + end end diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb index a1dd9563b..68492899e 100644 --- a/app/models/concerns/avatarable.rb +++ b/app/models/concerns/avatarable.rb @@ -2,7 +2,9 @@ module Avatarable extend ActiveSupport::Concern included do - has_one_attached :avatar + # Added to avoid running into a error when precompiling which + # forces Azure to be defined + has_one_attached :avatar unless ENV["PRECOMPILING"] # TODO: move these validation strings to a locale file validates :avatar, content_type: {in: ["image/png", "image/jpeg"], diff --git a/app/views/layouts/shared/_head.html.erb b/app/views/layouts/shared/_head.html.erb index 7eba75eaf..75ddd3e3a 100644 --- a/app/views/layouts/shared/_head.html.erb +++ b/app/views/layouts/shared/_head.html.erb @@ -12,7 +12,10 @@ <%= csrf_meta_tags %> <%= csp_meta_tag %> + <%= stylesheet_link_tag "feather.css", "data-turbo-track": "reload" %> <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> + <%= stylesheet_link_tag "theme.min.css", "data-turbo-track": "reload" %> + <%= javascript_importmap_tags %> diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint new file mode 100755 index 000000000..840d093a9 --- /dev/null +++ b/bin/docker-entrypoint @@ -0,0 +1,13 @@ +#!/bin/bash -e + +# Enable jemalloc for reduced memory usage and latency. +if [ -z "${LD_PRELOAD+x}" ] && [ -f /usr/lib/*/libjemalloc.so.2 ]; then + export LD_PRELOAD="$(echo /usr/lib/*/libjemalloc.so.2)" +fi + +# If running the rails server then create or migrate existing database +if [ "${1}" == "./bin/rails" ] && [ "${2}" == "server" ]; then + ./bin/rails db:prepare +fi + +exec "${@}" diff --git a/config/application.rb b/config/application.rb index 12bd5c532..7fed97530 100644 --- a/config/application.rb +++ b/config/application.rb @@ -28,11 +28,10 @@ class Application < Rails::Application # # config.time_zone = "Central Time (US & Canada)" # config.eager_load_paths << Rails.root.join("extras") + config.assets.paths << Rails.root.join("app", "assets", "builds") - # - # Deactivate the CSS compressor since it conflicts with the - # theme CSS - # - config.assets.css_compressor = nil + # Exclude this because we do not need to load this directly to servers, this is used to build + # application.css + config.assets.excluded_paths << Rails.root.join("vendor", "assets", "stylesheets") end end diff --git a/config/credentials/production.yml.enc b/config/credentials/production.yml.enc new file mode 100644 index 000000000..b76650775 --- /dev/null +++ b/config/credentials/production.yml.enc @@ -0,0 +1 @@ +AQ3WDT1KUPGQ7jW/GtHSucCBqfZ/akEWufvam26yYk/c4aK/WaX5W00luhcutjErSMzU+RV7SIH7qJ3TjO+r9wMHCkOOobvl3QfOdgHueFIzi8+I/c2rHtucgiShix2VsRJaZHWcV+qCNezsScNn7qjFJbH82Z5GFr7+U/AJaiRtbaZ9eV8Pu2lnVX3EzxGxkLmZy6tUO09bJcXexJXXMKEDmL7P4B10EZXht0NmYkl2lJKrbwGLq60faoPE8gVaNIJAJ0EOqik3JXh1tYOqju95rOg9f64J1u3sKmayG2ntVCjcSqSQ/+4KXHjZDJ33ox7sGAXwR8nOSifGPMUyxrPtYQ3eXLhlL3aIErZ2z0i3LC/ECev61D82B+rVTw6b2bh2w/wNFjCTTYv9Agjz9xvfnWlomxHMsaegdVDF+yUMnQuTkJMm+h5bSpqvRvKLLdFwPJWQLe7LObLlZLW55GvlL9w8Sg9RPHsXOqUbFwKGRRdw0aoA56WhypXcmnVRreSg4ikRSdp7gZuCScGo4l0uNjy71eINOaUheM/7yzrjD1GZ4hW24Ly1qZekyXrDwdFmTEaBEp5aioV2qn3gZFUaZIQUmuIYYE4yuHGPqB8eoJwpmn2YwDZvIe/xxdehG0egPRaNsqXOjiB9zY3Yc2CrAG3BiP0eYYVqptMg/3XlBtXZSPQL1rlTIgXr5VghHYm23RIFYwTuFnQZDyHk7ol2Z0zwMAmO0Y2zLNRv32thd9hnFZBEKOUw6dLdPH+dYdSQsXAvmNYgIkPP9pk0GAyY47K1tpNChLk79/PDU3rrJk9o4DMv9y/WwIEMI2z1HYt4rg6kAGuQijVkVjrP4Larb4pQgmWVFv7K3J9Ha2+Qu3lH6g==--of45FhC9vrAs5aJU--BbAam4aVpHw3OZ5OnbYj7A== \ No newline at end of file diff --git a/config/database.yml b/config/database.yml index a030ea518..fe7011f3b 100644 --- a/config/database.yml +++ b/config/database.yml @@ -98,6 +98,9 @@ test: # production: <<: *default - database: pet_rescue_production - username: <%= ENV['database_username'] %> - password: <%= ENV['database_password'] %> + host: <%= ENV["DB_HOST"] %> + pool: 5 + username: <%= ENV["DB_USER"] %> + password: <%= ENV["DB_PASSWORD"] %> + database: postgres + timeout: 5000 diff --git a/config/deploy.yml b/config/deploy.yml new file mode 100644 index 000000000..6f414be2d --- /dev/null +++ b/config/deploy.yml @@ -0,0 +1,103 @@ +# Name of your application. Used to uniquely configure containers. +service: homewardtails + +# Name of the container image. +image: homewardtails/homewardtails + +# Deploy to these servers. +servers: + web: + - homewardtails.org + # 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: homewardtails.org + 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: homewardtails + + # Always use an access token rather than real password (pulled from .kamal/secrets). + password: + - KAMAL_REGISTRY_PASSWORD + +# Configure builder setup. +builder: + arch: amd64 + # [edwin] - Using a remote builder can be faster especially if you are on a different architecture. + # For example, I use a Macbook (ARM64) but we want to deploy to Ubuntu (AMD64). This speeds it up + # but assumes you already setup SSH access to the server + # remote: ssh://deploy@homeward-tails.eastus.cloudapp.azure.com + +# Inject ENV variables into containers (secrets come from .kamal/secrets). +# +env: + secret: + - RAILS_MASTER_KEY + - DB_HOST + - DB_USER + - DB_PASSWORD + +# Aliases are triggered with "bin/kamal ". 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: deploy + keys: [ "~/.ssh/homewardtails.pem" ] + +# 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 047f6fbb9..7bdb3f998 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -22,7 +22,7 @@ # Disable serving static files from the `/public` folder by default since # Apache or NGINX already handles this. - config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present? + config.public_file_server.enabled = true # Compress CSS using a preprocessor. # config.assets.css_compressor = :sass @@ -39,6 +39,7 @@ # Store uploaded files on Azure. config.active_storage.service = :azure + # config.active_storage.service = :local # Mount Action Cable outside main process or domain. # config.action_cable.mount_path = nil @@ -46,6 +47,7 @@ # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ] # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + config.assume_ssl = true config.force_ssl = true # Include generic and useful information about system operation, but avoid logging too much diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb deleted file mode 100644 index 92e6eaaed..000000000 --- a/config/initializers/assets.rb +++ /dev/null @@ -1,15 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Version of your assets, change this if you want to expire all your assets. -Rails.application.config.assets.version = "1.0" - -# Precompile bootstrap.min.js -Rails.application.config.assets.precompile += %w[bootstrap.min.js popper.js] - -# Add additional assets to the asset load path. -Rails.application.config.assets.paths << Rails.root.join("app", "assets", "fonts") - -# Precompile additional assets. -# application.js, application.css, and all non-JS/CSS in the app/assets -# folder are already added. -# Rails.application.config.assets.precompile += %w( admin.js admin.css ) diff --git a/config/initializers/dartsass.rb b/config/initializers/dartsass.rb new file mode 100644 index 000000000..c72f4435d --- /dev/null +++ b/config/initializers/dartsass.rb @@ -0,0 +1,5 @@ +Rails.application.config.dartsass.builds = { + "application.scss" => "application.css", + "feather.css" => "feather.css", + "../../../vendor/assets/stylesheets/theme.min.scss" => "theme.min.css" +} diff --git a/config/routes.rb b/config/routes.rb index 942c05791..3305fff88 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -77,11 +77,16 @@ resources :states, only: [:index] end - match "/404", to: "errors#not_found", via: :all - match "/422", to: "errors#unprocessable_content", via: :all - match "/500", to: "errors#internal_server_error", via: :all + # + # [edwin] - routes cause failures if you cannot find an asset because it matches + # for all 404s, 422s, and 500s + # + # match "/404", to: "errors#not_found", via: :all + # match "/422", to: "errors#unprocessable_content", via: :all + # match "/500", to: "errors#internal_server_error", via: :all root "root#index" + get "/up", to: "root#up" # Health check endpoint to let Kamal know the app is up get "/about_us", to: "static_pages#about_us" get "/partners", to: "static_pages#partners" get "/donate", to: "static_pages#donate" diff --git a/config/storage.yml b/config/storage.yml index 326309bbf..854f7e100 100644 --- a/config/storage.yml +++ b/config/storage.yml @@ -19,7 +19,7 @@ azure: service: AzureStorage storage_account_name: <%= Rails.application.credentials.dig(:azure, :storage_account_name) %> storage_access_key: <%= Rails.application.credentials.dig(:azure, :storage_access_key) %> - container: <%= ENV['AZURE_STORAGE_CONTAINER'] %> + container: <%= Rails.application.credentials.dig(:azure, :container) %> # Remember not to checkin your GCS keyfile to a repository # google: diff --git a/docker-compose.yml b/docker-compose.yml index 351e03302..b84ff6b14 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,9 @@ services: interval: 10s app: - build: . + build: + context: . + dockerfile: Dockerfile.dev command: bash -c "rm -f tmp/pids/server.pid && bundle install && bundle exec rails s -b '0.0.0.0'" volumes: - .:/petrescue diff --git a/dockerfile b/dockerfile.dev similarity index 100% rename from dockerfile rename to dockerfile.dev diff --git a/lib/tasks/credentials.rake b/lib/tasks/credentials.rake new file mode 100644 index 000000000..d8a4909ff --- /dev/null +++ b/lib/tasks/credentials.rake @@ -0,0 +1,10 @@ +# Added to get the value of a specific credential from the credentials.yml.enc file +# for deployment purposes. +namespace :credentials do + desc "Read a specific credential" + task read: :environment do + key_path = ENV["KEY"].to_s.split(",").map(&:to_sym) + value = Rails.application.credentials.dig(*key_path) + puts value + end +end diff --git a/vendor/assets/stylesheets/theme.min.css b/vendor/assets/stylesheets/theme.min.scss similarity index 100% rename from vendor/assets/stylesheets/theme.min.css rename to vendor/assets/stylesheets/theme.min.scss