diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index bdc0550..49cfd40 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up flyctl uses: superfly/flyctl-actions/setup-flyctl@master - name: Deploy to Fly.io diff --git a/Dockerfile b/Dockerfile index 0faa233..42338ac 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,51 +1,53 @@ # 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 +# Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile ARG RUBY_VERSION=3.3.5 -FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base +FROM ruby:$RUBY_VERSION-slim as base LABEL fly_launch_runtime="rails" # Rails app lives here WORKDIR /rails -# Install base packages -RUN apt-get update -qq && \ - apt-get install --no-install-recommends -y curl libjemalloc2 libvips postgresql-client && \ - rm -rf /var/lib/apt/lists /var/cache/apt/archives - # Set production environment -ENV RAILS_ENV="production" \ - BUNDLE_DEPLOYMENT="1" \ +ENV BUNDLE_DEPLOYMENT="1" \ BUNDLE_PATH="/usr/local/bundle" \ - BUNDLE_WITHOUT="development:test" + BUNDLE_WITHOUT="development:test" \ + RAILS_ENV="production" + +# Update gems and bundler +RUN gem update --system --no-document && \ + gem install -N bundler + # Throw-away build stage to reduce size of final image -FROM base AS build +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 libyaml-dev && \ - rm -rf /var/lib/apt/lists /var/cache/apt/archives + apt-get install --no-install-recommends -y build-essential libpq-dev libvips # Install application gems -COPY Gemfile Gemfile.lock ./ +COPY --link Gemfile Gemfile.lock ./ RUN bundle install && \ + bundle exec bootsnap precompile --gemfile && \ rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git # Copy application code -COPY . . - +COPY --link . . +# Precompile bootsnap code for faster boot times +RUN bundle exec bootsnap precompile app/ lib/ # Final stage for app image FROM base +# Install packages needed for deployment +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y curl imagemagick libvips postgresql-client && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + # Copy built artifacts: gems, application COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}" COPY --from=build /rails /rails @@ -53,10 +55,10 @@ 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 + chown -R 1000:1000 db log storage tmp USER 1000:1000 -# Entrypoint prepares the database. +# Entrypoint sets up the container. ENTRYPOINT ["/rails/bin/docker-entrypoint"] # Start the server by default, this can be overwritten at runtime diff --git a/Gemfile b/Gemfile index a1ec00f..5783c89 100644 --- a/Gemfile +++ b/Gemfile @@ -6,11 +6,13 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby "3.3.5" # CORE +gem "bootsnap", require: false gem "puma" gem "rails" gem "responders" # FRAMEWORK +gem "anycable-rails" gem "devise" gem "devise-jwt" gem "good_job" diff --git a/Gemfile.lock b/Gemfile.lock index 1134c80..89e0afd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -73,11 +73,28 @@ GEM tzinfo (~> 2.0, >= 2.0.5) addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) + anycable (1.5.1) + anycable-core (= 1.5.1) + grpc (~> 1.53) + anycable-core (1.5.1) + anyway_config (~> 2.2) + google-protobuf (~> 3.25) + anycable-rails (1.5.3) + anycable (~> 1.5.0) + anycable-rails-core (= 1.5.3) + anycable-rails-core (1.5.3) + actioncable (>= 6.0, < 8) + anycable-core (~> 1.5.0) + globalid + anyway_config (2.6.4) + ruby-next-core (~> 1.0) base64 (0.2.0) bcrypt (3.1.20) bigdecimal (3.1.8) binding_of_caller (1.0.1) debug_inspector (>= 1.2.0) + bootsnap (1.18.4) + msgpack (~> 1.2) brakeman (6.2.1) racc bugsnag (6.27.1) @@ -140,6 +157,20 @@ GEM fugit (>= 1.11.0) railties (>= 6.1.0) thor (>= 1.0.0) + google-protobuf (3.25.4-aarch64-linux) + google-protobuf (3.25.4-arm64-darwin) + google-protobuf (3.25.4-x86_64-linux) + googleapis-common-protos-types (1.16.0) + google-protobuf (>= 3.18, < 5.a) + grpc (1.66.0-aarch64-linux) + google-protobuf (>= 3.25, < 5.0) + googleapis-common-protos-types (~> 1.0) + grpc (1.66.0-arm64-darwin) + google-protobuf (>= 3.25, < 5.0) + googleapis-common-protos-types (~> 1.0) + grpc (1.66.0-x86_64-linux) + google-protobuf (>= 3.25, < 5.0) + googleapis-common-protos-types (~> 1.0) hashdiff (1.1.1) i18n (1.14.5) concurrent-ruby (~> 1.0) @@ -169,6 +200,7 @@ GEM marcel (1.0.4) mini_mime (1.1.5) minitest (5.25.1) + msgpack (1.7.2) net-imap (0.4.16) date net-protocol @@ -268,6 +300,7 @@ GEM rspec-mocks (~> 3.13) rspec-support (~> 3.13) rspec-support (3.13.1) + ruby-next-core (1.0.3) securerandom (0.3.1) stringio (3.1.1) thor (1.3.2) @@ -300,7 +333,9 @@ PLATFORMS x86_64-linux DEPENDENCIES + anycable-rails binding_of_caller + bootsnap brakeman bugsnag database_cleaner @@ -329,4 +364,4 @@ RUBY VERSION ruby 3.3.5p100 BUNDLED WITH - 2.5.16 + 2.5.18 diff --git a/README.md b/README.md index 8ace5ad..71883f4 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,8 @@ such as: backend: cd Backend && rvm 3.3.5@flyweight exec rails server frontend: cd Frontend && yarn dev jobs: cd Backend && rvm 3.3.5@flyweight exec bundle exec good_job start -cable: cd Backend && rvm 3.3.5@flyweight exec ./bin/cable +anycable: cd Backend && rvm 3.3.5@flyweight exec anycable +ws: cd Backend && rvm 3.3.5@flyweight exec bin/anycable-go --port=8080 ``` Install the `foreman` gem to run the Procfile. diff --git a/bin/anycable-go b/bin/anycable-go new file mode 100755 index 0000000..0f1a45b --- /dev/null +++ b/bin/anycable-go @@ -0,0 +1,22 @@ +#!/bin/bash + +cd $(dirname $0)/.. + +# It's recommended to use the exact version of AnyCable here +version="latest" + +if [ ! -f ./bin/dist/anycable-go ]; then + echo "AnyCable server is not installed, downloading..." + ./bin/rails g anycable:download --version=$version --bin-path=./bin/dist +fi + +curVersion=$(./bin/dist/anycable-go -v) + +if [[ "$version" != "latest" ]]; then + if [[ "$curVersion" != "$version"* ]]; then + echo "AnyCable server version is not $version, downloading a new one..." + ./bin/rails g anycable:download --version=$version --bin-path=./bin/dist + fi +fi + +./bin/dist/anycable-go $@ diff --git a/bin/cable b/bin/cable index 977abb3..e767d17 100755 --- a/bin/cable +++ b/bin/cable @@ -1 +1 @@ -bundle exec puma -p 28080 cable/config.ru --pidfile tmp/pids/cable.pid $* +bundle exec puma -p 8080 cable/config.ru --pidfile tmp/pids/cable.pid $* diff --git a/bin/dist/anycable-go b/bin/dist/anycable-go new file mode 100755 index 0000000..38f27b3 Binary files /dev/null and b/bin/dist/anycable-go differ diff --git a/config/anycable.yml b/config/anycable.yml new file mode 100644 index 0000000..1e95dd2 --- /dev/null +++ b/config/anycable.yml @@ -0,0 +1,38 @@ +# This file contains per-environment settings for AnyCable. +# +# Since AnyCable config is based on anyway_config (https://github.com/palkan/anyway_config), all AnyCable settings +# can be set or overridden through the corresponding environment variables. +# E.g., `rpc_host` is overridden by ANYCABLE_RPC_HOST, `debug` by ANYCABLE_DEBUG etc. +# +# Note that AnyCable recognizes REDIS_URL env variable for Redis pub/sub adapter. If you want to +# use another Redis instance for AnyCable, provide ANYCABLE_REDIS_URL variable. +# +# Read more about AnyCable configuration here: https://docs.anycable.io/ruby/configuration +# +default: &default + # Turn on/off access logs ("Started..." and "Finished...") + access_logs_disabled: false + # Whether to enable gRPC level logging or not + log_grpc: false + # Use Redis to broadcast messages to AnyCable server + broadcast_adapter: redis + # You can use REDIS_URL env var to configure Redis URL. + # Localhost is used by default. + # redis_url: "redis://localhost:6379/1" + # Use the same channel name for WebSocket server, e.g.: + # $ anycable-go --redis_channel="__anycable__" + # redis_channel: "__anycable__" + +development: + <<: *default + # WebSocket endpoint of your AnyCable server for clients to connect to + # Make sure you have the `action_cable_meta_tag` in your HTML layout + # to propogate this value to the client app + websocket_url: "ws://localhost:8080/cable" + +test: + <<: *default + +production: + <<: *default + websocket_url: ~ diff --git a/config/application.rb b/config/application.rb index 4fdac76..04c372b 100644 --- a/config/application.rb +++ b/config/application.rb @@ -82,5 +82,7 @@ class Application < Rails::Application port: backend.port, protocol: backend.scheme } + + config.host_authorization = {exclude: ->(request) { request.path.start_with?("/up") }} end end diff --git a/config/cable.yml b/config/cable.yml index 864cdb0..be11836 100644 --- a/config/cable.yml +++ b/config/cable.yml @@ -1,12 +1,10 @@ development: - adapter: redis - url: <%= ENV.fetch("REDIS_URL") { "redis://127.0.0.1:6379/1" } %> + adapter: <%= ENV.fetch("ACTION_CABLE_ADAPTER", "any_cable") %> + url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> channel_prefix: flyweight_development test: adapter: test production: - adapter: redis - url: <%= ENV.fetch("REDIS_URL") { "redis://127.0.0.1:6379/1" } %> - channel_prefix: flyweight_production + adapter: any_cable diff --git a/config/environments/cypress.rb b/config/environments/cypress.rb index 067d643..cf51f1b 100644 --- a/config/environments/cypress.rb +++ b/config/environments/cypress.rb @@ -8,7 +8,7 @@ # In the development environment your application's code is reloaded any time # it changes. This slows down response time but is perfect for development # since you don't have to restart the web server when you make code changes. - config.cache_classes = false + config.enable_reloading = true # Do not eager load code on boot. config.eager_load = false @@ -16,28 +16,20 @@ # Show full error reports. config.consider_all_requests_local = true - # Enable server timing + # Enable server timing. config.server_timing = true # Enable/disable caching. By default caching is disabled. # Run rails dev:cache to toggle caching. - if Rails.root.join("tmp", "caching-dev.txt").exist? - config.cache_store = :memory_store - config.public_file_server.headers = { - "Cache-Control" => "public, max-age=#{2.days.to_i}" - } - else - config.action_controller.perform_caching = false + config.action_controller.perform_caching = false - config.cache_store = :null_store - end - - # Store uploaded files on the local file system (see config/storage.yml for options). - # config.active_storage.service = :local + config.cache_store = :null_store # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false + # Disable caching for Action Mailer templates even if Action Controller + # caching is enabled. config.action_mailer.perform_caching = false config.action_mailer.delivery_method = :test @@ -57,14 +49,26 @@ # Highlight code that triggered database queries in logs. config.active_record.verbose_query_logs = true + # Highlight code that enqueued background job in logs. + config.active_job.verbose_enqueue_logs = true + # Raises error for missing translations. # config.i18n.raise_on_missing_translations = true # Annotate rendered view with file names. - # config.action_view.annotate_rendered_view_with_filenames = true + config.action_view.annotate_rendered_view_with_filenames = true # Uncomment if you wish to allow Action Cable access from any origin. # config.action_cable.disable_request_forgery_protection = true + # Raise error when a before_action's only/except options reference missing actions. + config.action_controller.raise_on_missing_callback_actions = true + + # Apply autocorrection by RuboCop to files generated by `bin/rails generate`. + config.generators.apply_rubocop_autocorrect_after_generate! + + # Mount Action Cable outside main process or domain. + config.action_cable.mount_path = nil + config.good_job.poll_interval = 1 end diff --git a/config/environments/development.rb b/config/environments/development.rb index d77f4e4..7c77d47 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -69,4 +69,7 @@ # Apply autocorrection by RuboCop to files generated by `bin/rails generate`. config.generators.apply_rubocop_autocorrect_after_generate! + + # Mount Action Cable outside main process or domain. + config.action_cable.mount_path = nil end diff --git a/config/environments/production.rb b/config/environments/production.rb index 6fcc67b..f9455c7 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -32,7 +32,7 @@ # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX # Mount Action Cable outside main process or domain. - # config.action_cable.mount_path = nil + config.action_cable.mount_path = nil # config.action_cable.url = "wss://example.com/cable" # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ] diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb index 88f3a14..59f34d2 100644 --- a/config/initializers/content_security_policy.rb +++ b/config/initializers/content_security_policy.rb @@ -10,7 +10,7 @@ # - connect-src 'https://sessions.bugsnag.com' # # Vue.js in development requires: -# - connect-src 'ws://127.0.0.1:3035' 'http://127.0.0.1:3035' +# - connect-src 'ws://localhost:3035' 'http://localhost:3035' extra_image_sources = %w[] extra_script_sources = [] @@ -20,7 +20,7 @@ if Rails.env.development? || Rails.env.cypress? extra_script_sources << :unsafe_eval << :unsafe_inline - # extra_connect_sources << "ws://127.0.0.1:3035" << "http://127.0.0.1:3035" + # extra_connect_sources << "ws://localhost:3035" << "http://localhost:3035" end Rails.application.configure do diff --git a/config/urls.yml b/config/urls.yml index 066c24e..42c8a71 100644 --- a/config/urls.yml +++ b/config/urls.yml @@ -1,7 +1,7 @@ development: - backend: "http://127.0.0.1:5000" - frontend: "http://127.0.0.1:5173" - cable: "ws://127.0.0.1:28080/cable" + backend: "http://localhost:5000" + frontend: "http://localhost:5173" + cable: "ws://localhost:8080/cable" test: backend: "http://app.test.host" @@ -11,7 +11,7 @@ test: cypress: backend: "http://127.0.0.1:5000" frontend: "http://127.0.0.1:4173" - cable: "ws://127.0.0.1:28080/cable" + cable: "ws://127.0.0.1:8080/cable" production: backend: "https://app.flyweight.org" diff --git a/fly.toml b/fly.toml index 2fffe62..c91565a 100644 --- a/fly.toml +++ b/fly.toml @@ -1,9 +1,9 @@ -# fly.toml app configuration file generated for flyweight-app on 2024-06-19T11:32:12-07:00 +# fly.toml app configuration file generated for flyweight-backend on 2024-09-13T18:14:57-07:00 # # See https://fly.io/docs/reference/configuration/ for information about how to use this file. # -app = 'flyweight-app' +app = 'flyweight-backend' primary_region = 'sjc' console_command = '/rails/bin/rails console' @@ -18,14 +18,21 @@ console_command = '/rails/bin/rails console' [http_service] internal_port = 3000 - force_https = true - auto_stop_machines = true + auto_stop_machines = 'stop' auto_start_machines = true min_machines_running = 0 processes = ['app'] +#[[http_service.checks]] +# grace_period = "30s" +# interval = "30s" +# method = "GET" +# timeout = "5s" +# path = "/up" +# protocol = "http" + [[vm]] - memory = '1gb' + memory = '512mb' cpu_kind = 'shared' cpus = 1