diff --git a/.gitignore b/.gitignore index e0c3069..166cccc 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,5 @@ coverage /app/assets/builds/* !/app/assets/builds/.keep + +/config/credentials/production.key diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7851b56 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,73 @@ +# syntax=docker/dockerfile:1 +# check=error=true + +# Match Ruby version from .tool-versions +ARG RUBY_VERSION=3.3.8 +FROM ruby:$RUBY_VERSION-slim AS base + +WORKDIR /rails + +# Install base packages +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y \ + curl \ + libjemalloc2 \ + sqlite3 && \ + 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:test" + + +# Build stage +FROM base AS build + +# Install packages needed to build gems +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y \ + build-essential \ + libffi-dev \ + libyaml-dev \ + libsqlite3-dev \ + git && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Install 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/ + +# Precompile assets for production +RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile + + +# Final production image +FROM base + +# Copy built artifacts: gems, application +COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}" +COPY --from=build /rails /rails + +# Run as non-root user +RUN groupadd --system --gid 1000 rails && \ + useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \ + mkdir -p data && \ + chown -R 1000:1000 db log storage tmp data +USER 1000:1000 + +# Entrypoint prepares the database +ENTRYPOINT ["/rails/bin/docker-entrypoint"] + +# Start server on port 3000 +EXPOSE 3000 +CMD ["./bin/rails", "server", "-b", "0.0.0.0"] diff --git a/Gemfile b/Gemfile index 6225e78..7537a58 100644 --- a/Gemfile +++ b/Gemfile @@ -23,6 +23,7 @@ gem 'redis', '~> 5.0' gem 'solid_queue', '~> 1.1' gem 'sprockets-rails' gem 'stimulus-rails', '~> 1.3' +gem 'sqlite3' gem 'turbo-rails', '~> 1.5' # Use Active Storage variant @@ -35,8 +36,6 @@ gem 'net-pop', require: false gem 'net-smtp', require: false group :development, :test do - # Use sqlite3 as the database for Active Record - gem 'sqlite3' # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platforms: %i[mri mingw x64_mingw] gem 'htmlbeautifier' @@ -66,9 +65,5 @@ group :test do gem 'simplecov', require: false end -group :production do - # gem 'pg', '~> 1.5' -end - # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] diff --git a/README.md b/README.md index 9c999f7..c2d54f6 100644 --- a/README.md +++ b/README.md @@ -26,5 +26,35 @@ You can easily set up and run this project in a [VS Code Devcontainer](https://c - The app will be available at [http://localhost:8080](http://localhost:8080) on your host machine. ## Up and Running -* the devcontainer.json configuration will also copy your ssh settings for github to the docker container so you can -use git without needing to reauthenticate \ No newline at end of file +* the devcontainer.json configuration will also copy your ssh settings for github to the docker container so you can +use git without needing to reauthenticate + +## Running with Docker + +### Build the image + +```sh +bin/docker-build +``` + +Or pass a custom tag: + +```sh +bin/docker-build v1.0.0 +``` + +This tags the image as both `draft:` and `draft:`. + +### Run the container + +```sh +bin/docker-run +``` + +The app will be available at [http://draft.localhost](http://draft.localhost). To use a different port: + +```sh +PORT=3001 bin/docker-run +``` + +Named volumes (`draft-data` and `draft-storage`) persist the database and uploaded files across container restarts. \ No newline at end of file diff --git a/app/models/concerns/mailing_list.rb b/app/models/concerns/mailing_list.rb index 5521a4e..57a5479 100644 --- a/app/models/concerns/mailing_list.rb +++ b/app/models/concerns/mailing_list.rb @@ -2,7 +2,11 @@ module MailingList MAILER_LITE_GROUP = 111712173989824242 def subscribe_to_mailing - MailerLite.create_group_subscriber(MAILER_LITE_GROUP, { email: email }) if Rails.env.production? + return unless Rails.env.production? + + MailerLite.create_group_subscriber(MAILER_LITE_GROUP, { email: email }) + rescue StandardError => e + Rails.logger.warn("MailerLite subscription failed for #{email}: #{e.message}") end end \ No newline at end of file diff --git a/bin/docker-build b/bin/docker-build new file mode 100755 index 0000000..e08e81f --- /dev/null +++ b/bin/docker-build @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail + +IMAGE_NAME="draft" +TAG="${1:-latest}" + +# Use git SHA as additional tag when available +GIT_SHA=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown") + +echo "Building ${IMAGE_NAME}:${TAG} (git: ${GIT_SHA})..." + +docker build \ + -t "${IMAGE_NAME}:${TAG}" \ + -t "${IMAGE_NAME}:${GIT_SHA}" \ + . + +echo "" +echo "Built images:" +echo " ${IMAGE_NAME}:${TAG}" +echo " ${IMAGE_NAME}:${GIT_SHA}" +echo "" +echo "Run with:" +echo " docker run -p 3000:3000 ${IMAGE_NAME}:${TAG}" diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint index 57567d6..3fa5c58 100755 --- a/bin/docker-entrypoint +++ b/bin/docker-entrypoint @@ -7,7 +7,7 @@ if [ -z "${LD_PRELOAD+x}" ]; then fi # If running the rails server then create or migrate existing database -if [ "${@: -2:1}" == "./bin/rails" ] && [ "${@: -1:1}" == "server" ]; then +if [ "$1" == "./bin/rails" ] && [ "$2" == "server" ]; then ./bin/rails db:prepare fi diff --git a/bin/docker-run b/bin/docker-run new file mode 100755 index 0000000..d8c57fa --- /dev/null +++ b/bin/docker-run @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -euo pipefail + +IMAGE_NAME="draft" +TAG="${1:-latest}" +CONTAINER_NAME="draft" +PORT="${PORT:-80}" + +# Remove existing container if present +if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + echo "Removing existing '${CONTAINER_NAME}' container..." + docker rm -f "$CONTAINER_NAME" +fi + +echo "Starting ${IMAGE_NAME}:${TAG} on port ${PORT}..." + +docker run -d \ + --name "$CONTAINER_NAME" \ + -p "${PORT}:3000" \ + -e RAILS_ASSUME_SSL=false \ + -e RAILS_FORCE_SSL=false \ + -e RAILS_MASTER_KEY="${RAILS_MASTER_KEY:-$(cat config/credentials/production.key)}" \ + -e DATABASE_URL="sqlite3:/rails/data/production.sqlite3" \ + -v draft-data:/rails/data \ + -v draft-storage:/rails/storage \ + "${IMAGE_NAME}:${TAG}" + +if [ "$PORT" = "80" ]; then + echo "Running at http://draft.localhost" +else + echo "Running at http://draft.localhost:${PORT}" +fi diff --git a/config/credentials/production.yml.enc b/config/credentials/production.yml.enc new file mode 100644 index 0000000..2dd44b2 --- /dev/null +++ b/config/credentials/production.yml.enc @@ -0,0 +1 @@ +GIPj3kbP/xHIbHWy5G6bSAR0PMzVe30hJsRgdxOvjcTIQEuVPO8uEhyvTVANlO/1RzDVVnKJtxnZRO0ty/Z1N/KYLB6VhmKQcKr0+g8bY/EneEcfyRj3iarEg1MHf860IOA4aF5snBIQw4Qs229jFWwVZ7h+CDkIhoVeBXz75buXu23VXZmn26q4iv8sxlehAqDfUyb8po2CWf0sXFLg2N5T/Mk6hrDDMb9rJYu3Ut3VlGom5/LFAhCjTuxIudUf6LMjoM5pIKJvi66vU9vwBBQzbASxgMxYMaaTaJWgbkyvgvs9hWY3NvXk0t6dIbdsRKqQNT+Ou2c6cbscRFhJLL/AfRlvceiwwHItwakrhgn79z2i4X8/I7mTu8OyYQ6pgOgfXQRNmcnV4Ef+V+lcOFHLl/9C3/lV7+md/CplK1y9nG/k9JID9e2h7f8EaZl74Th3gL5YEC41XnPtpVcU32Y4t09/Ya6HiOpfG2OiEpWawaX2N1gMdxlt--drWRJWyzzwUZdrhF--b/iTIrkCtZAy0iE1tCYB3A== \ No newline at end of file diff --git a/config/environments/production.rb b/config/environments/production.rb index 36daef5..6073c39 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -25,10 +25,10 @@ config.active_storage.service = :local # Assume all access to the app is happening through a SSL-terminating reverse proxy. - config.assume_ssl = true + config.assume_ssl = ENV.fetch('RAILS_ASSUME_SSL', 'true') == 'true' # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. - config.force_ssl = true + config.force_ssl = ENV.fetch('RAILS_FORCE_SSL', 'true') == 'true' # Skip http-to-https redirect for the default health check endpoint. # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } } @@ -62,12 +62,12 @@ # Specify outgoing SMTP server. Remember to add smtp/* credentials via rails credentials:edit. config.action_mailer.smtp_settings = { - user_name: Rails.application.credentials.mailer[:username], - password: Rails.application.credentials.mailer[:password], - address: Rails.application.credentials.mailer[:address], - domain: Rails.application.credentials.mailer[:domain], - port: Rails.application.credentials.mailer[:port], - authentication: Rails.application.credentials.mailer[:authentication], + user_name: Rails.application.credentials.dig(:mailer,:username), + password: Rails.application.credentials.dig(:mailer,:password), + address: Rails.application.credentials.dig(:mailer,:address), + domain: Rails.application.credentials.dig(:mailer,:domain), + port: Rails.application.credentials.dig(:mailer,:port), + authentication: Rails.application.credentials.dig(:mailer,:authentication), enable_starttls_auto: true } diff --git a/config/initializers/mailerlite.rb b/config/initializers/mailerlite.rb index 8a9d123..cd293f2 100644 --- a/config/initializers/mailerlite.rb +++ b/config/initializers/mailerlite.rb @@ -1,4 +1,4 @@ MailerLite.configure do |config| - config.api_key = Rails.application.credentials.mailerlite[:api_key] if Rails.env.production? + config.api_key = Rails.application.credentials.dig(:mailerlite,:api_key) if Rails.env.production? # config.timeout = 10 end