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