From b925110ecb802aaf5536056983e71cca6fdf5aa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Verg=C3=A9s?= Date: Mon, 12 Feb 2024 23:50:29 +0100 Subject: [PATCH] Adds a production ready docker environment (#730) --- .env.example | 7 +- .github/workflows/docker_build.yml | 16 +++ .gitignore | 1 + Capfile | 22 ---- Dockerfile | 100 +++++++++++++++ Gemfile | 13 +- Gemfile.lock | 116 +++++++----------- README.md | 59 +++++++++ app/admin/organization.rb | 4 +- app/assets/javascripts/libs.js | 6 +- .../vendor}/highcharts-exporting.js | 0 .../assets/javascripts/vendor}/highcharts.js | 0 .../javascripts/vendor}/jquery.validate.js | 0 app/helpers/application_helper.rb | 4 +- config/application.rb | 13 +- config/database.yml | 17 +-- config/deploy.rb | 81 ------------ config/deploy/production.rb | 1 - config/deploy/staging.rb | 1 - config/environments/production.rb | 35 ++++-- config/puma.rb | 47 +++++++ config/sidekiq.yml | 1 + config/unicorn.rb | 29 ----- docker-compose.yml | 49 ++++++++ entrypoint.sh | 58 +++++++++ spec/helpers/application_helper_spec.rb | 2 +- spec/spec_helper.rb | 2 +- supervisord.conf | 25 ++++ vendor/assets/stylesheets/.keep | 0 29 files changed, 460 insertions(+), 249 deletions(-) create mode 100644 .github/workflows/docker_build.yml delete mode 100644 Capfile create mode 100644 Dockerfile rename {vendor/assets/javascripts => app/assets/javascripts/vendor}/highcharts-exporting.js (100%) rename {vendor/assets/javascripts => app/assets/javascripts/vendor}/highcharts.js (100%) rename {vendor/assets/javascripts => app/assets/javascripts/vendor}/jquery.validate.js (100%) delete mode 100644 config/deploy.rb delete mode 100644 config/deploy/production.rb delete mode 100644 config/deploy/staging.rb create mode 100644 config/puma.rb delete mode 100644 config/unicorn.rb create mode 100644 docker-compose.yml create mode 100755 entrypoint.sh create mode 100644 supervisord.conf delete mode 100644 vendor/assets/stylesheets/.keep diff --git a/.env.example b/.env.example index 329465ffd..16075c2b8 100644 --- a/.env.example +++ b/.env.example @@ -12,6 +12,11 @@ DATABASE_USER=postgres DATABASE_NAME=timeoverflow_development +#RAILS CONFIG +RAILS_LOG_LEVEL=debug +STORAGE_PROVIDER=amazon +FORCE_SSL=true + # Host part of the url for mail links: MAIL_LINK_HOST=localhost:3000 MAIL_LINK_PROTO=http @@ -26,7 +31,7 @@ SMTP_PORT=587 # List of emails for superadmin users ADMINS="admin@timeoverflow.org" -# AWS settings +# AWS settings (if STORAGE_PROVIDER=amazon) AWS_ACCESS_KEY_ID=XXXXXXXX AWS_SECRET_ACCESS_KEY=XXXXXXXX AWS_BUCKET=timeoverflow_development diff --git a/.github/workflows/docker_build.yml b/.github/workflows/docker_build.yml new file mode 100644 index 000000000..ad058acc6 --- /dev/null +++ b/.github/workflows/docker_build.yml @@ -0,0 +1,16 @@ +name: Docker Build + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Build docker + run: docker build . + - name: Test docker compose + run: docker-compose up -d + - run: sleep 15 # wait for the server to start + - name: Check server is up + run: curl -s http://localhost:3000 diff --git a/.gitignore b/.gitignore index 85dbc60e4..5c942096a 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ tags .sass-cache capybara-*.html /vendor/bundle +/public/assets /storage/ /coverage/ /spec/tmp/* diff --git a/Capfile b/Capfile deleted file mode 100644 index 0f81c1684..000000000 --- a/Capfile +++ /dev/null @@ -1,22 +0,0 @@ -# Load DSL and set up stages -require 'capistrano/setup' - -# Include default deployment tasks -require 'capistrano/deploy' - -# Include tasks from other gems included in your Gemfile -# -# For documentation on these, see for example: -# -# https://github.com/capistrano/rvm -# https://github.com/capistrano/rbenv -# https://github.com/capistrano/chruby -# https://github.com/capistrano/bundler -# https://github.com/capistrano/rails -# https://github.com/capistrano/passenger -# -require 'capistrano/rails' -require 'capistrano/rbenv' - -# Load custom tasks from `lib/capistrano/tasks` if you have any defined -Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r } diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..933db8346 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,100 @@ +FROM ruby:3.2 AS builder + +RUN apt-get update && apt-get upgrade -y && apt-get install -y ca-certificates curl gnupg && \ + curl -fsSL https://deb.nodesource.com/setup_21.x | bash - && \ + apt-get install -y \ + build-essential \ + nodejs \ + postgresql-client \ + libpq-dev && \ + apt-get clean + +# throw errors if Gemfile has been modified since Gemfile.lock +RUN bundle config --global frozen 1 + +WORKDIR /app + +# Copy package dependencies files only to ensure maximum cache hit +COPY ./Gemfile /app/Gemfile +COPY ./Gemfile.lock /app/Gemfile.lock + +RUN gem install bundler:$(grep -A 1 'BUNDLED WITH' Gemfile.lock | tail -n 1 | xargs) && \ + bundle config --local without 'development test' && \ + bundle install -j4 --retry 3 && \ + # Remove unneeded gems + bundle clean --force && \ + # Remove unneeded files from installed gems (cache, *.o, *.c) + rm -rf /usr/local/bundle/cache && \ + find /usr/local/bundle/ -name "*.c" -delete && \ + find /usr/local/bundle/ -name "*.o" -delete && \ + find /usr/local/bundle/ -name ".git" -exec rm -rf {} + && \ + find /usr/local/bundle/ -name ".github" -exec rm -rf {} + && \ + find /usr/local/bundle/ -name "spec" -exec rm -rf {} + + +# copy the rest of files +COPY ./app /app/app +COPY ./bin /app/bin +COPY ./config /app/config +COPY ./db /app/db +COPY ./lib /app/lib +COPY ./public/*.* /app/public/ +COPY ./config.ru /app/config.ru +COPY ./Rakefile /app/Rakefile + +# Compile assets +# +# For an app using encrypted credentials, Rails raises a `MissingKeyError` +# if the master key is missing. Because on CI there is no master key, +# we hide the credentials while compiling assets (by renaming them before and after) +# +RUN mv config/credentials.yml.enc config/credentials.yml.enc.bak 2>/dev/null || true +RUN mv config/credentials config/credentials.bak 2>/dev/null || true + +RUN RAILS_ENV=production \ + SECRET_KEY_BASE=dummy \ + RAILS_MASTER_KEY=dummy \ + DB_ADAPTER=nulldb \ + bundle exec rails assets:precompile + +RUN mv config/credentials.yml.enc.bak config/credentials.yml.enc 2>/dev/null || true +RUN mv config/credentials.bak config/credentials 2>/dev/null || true + +RUN rm -rf tmp/cache vendor/bundle test spec .git + +# This image is for production env only +FROM ruby:3.2-slim AS final + +RUN apt-get update && \ + apt-get install -y postgresql-client \ + imagemagick \ + libvips \ + curl \ + supervisor && \ + apt-get clean + +EXPOSE 3000 + +ENV RAILS_LOG_TO_STDOUT true +ENV RAILS_SERVE_STATIC_FILES true +ENV RAILS_ENV production + +ARG RUN_RAILS +ARG RUN_SIDEKIQ + +# Add user +RUN addgroup --system --gid 1000 app && \ + adduser --system --uid 1000 --home /app --group app + +WORKDIR /app +COPY ./entrypoint.sh /app/entrypoint.sh +COPY ./supervisord.conf /etc/supervisord.conf +COPY --from=builder --chown=app:app /usr/local/bundle/ /usr/local/bundle/ +COPY --from=builder --chown=app:app /app /app + +USER app +HEALTHCHECK --interval=1m --timeout=5s --start-period=10s \ + CMD (curl -IfSs http://localhost:3000/ -A "HealthCheck: Docker/1.0") || exit 1 + + +ENTRYPOINT ["/app/entrypoint.sh"] +CMD ["/usr/bin/supervisord"] \ No newline at end of file diff --git a/Gemfile b/Gemfile index 07ddbb5fb..09e9249b0 100644 --- a/Gemfile +++ b/Gemfile @@ -13,7 +13,6 @@ gem 'json_translate', '~> 4.0.0' gem 'devise', '~> 4.9.1' gem 'devise-i18n', '~> 1.11.0' gem 'http_accept_language', '~> 2.1.1' -gem 'unicorn', '~> 6.1.0' gem 'kaminari', '~> 1.2.1' gem 'simple_form', '~> 5.0.2' gem 'rollbar', '~> 3.4' @@ -26,21 +25,24 @@ gem 'sidekiq-cron', '~> 1.9.1' gem 'aws-sdk-s3', '~> 1.94', require: false gem 'image_processing', '~> 1.12' gem 'active_storage_validations', '~> 1.1.3' +gem "puma", ">= 5.0.0" +gem 'matrix', '~> 0.4.1' # Assets gem 'jquery-rails', '~> 4.4.0' gem 'bootstrap-sass', '~> 3.4' gem 'sassc-rails', '~> 2.1.2' -gem 'uglifier', '~> 4.2.0' gem 'select2-rails', '~> 4.0.13' +group :production do + # we are using an ExecJS runtime only on the precompilation phase + gem "uglifier", "~> 4.2.0", require: false +end + group :development do gem 'localeapp', '~> 3.3', require: false gem 'letter_opener', '~> 1.7.0' gem 'web-console', '~> 4.1.0' - gem 'capistrano', '~> 3.15.0' - gem 'capistrano-rails', '~> 1.1' - gem 'capistrano-rbenv', '~> 2.1' end group :development, :test do @@ -60,5 +62,4 @@ group :test do gem 'capybara', '~> 3.29' gem 'selenium-webdriver', '~> 4.16' gem 'simplecov', '~> 0.22', require: false - gem 'webrick' end diff --git a/Gemfile.lock b/Gemfile.lock index 832e67773..b0d22f469 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -46,7 +46,7 @@ GEM erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) - active_storage_validations (1.1.3) + active_storage_validations (1.1.4) activejob (>= 5.2.0) activemodel (>= 5.2.0) activestorage (>= 5.2.0) @@ -82,8 +82,6 @@ GEM tzinfo (~> 2.0) addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) - airbrussh (1.5.0) - sshkit (>= 1.6.1, != 1.7.0) arbre (1.7.0) activesupport (>= 3.0.0) ruby2_keywords (>= 0.0.2) @@ -91,53 +89,41 @@ GEM autoprefixer-rails (10.4.16.0) execjs (~> 2) aws-eventstream (1.3.0) - aws-partitions (1.864.0) - aws-sdk-core (3.190.0) + aws-partitions (1.887.0) + aws-sdk-core (3.191.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.8) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.74.0) - aws-sdk-core (~> 3, >= 3.188.0) + aws-sdk-kms (1.77.0) + aws-sdk-core (~> 3, >= 3.191.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.141.0) - aws-sdk-core (~> 3, >= 3.189.0) + aws-sdk-s3 (1.143.0) + aws-sdk-core (~> 3, >= 3.191.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.8) aws-sigv4 (1.8.0) aws-eventstream (~> 1, >= 1.0.2) + base64 (0.2.0) bcrypt (3.1.20) bindex (0.8.1) - bootsnap (1.17.0) + bootsnap (1.18.3) msgpack (~> 1.2) bootstrap-sass (3.4.1) autoprefixer-rails (>= 5.2.1) sassc (>= 2.0.0) builder (3.2.4) byebug (11.1.3) - capistrano (3.15.0) - airbrussh (>= 1.0.0) - i18n - rake (>= 10.0.0) - sshkit (>= 1.9.0) - capistrano-bundler (2.1.0) - capistrano (~> 3.1) - capistrano-rails (1.6.3) - capistrano (~> 3.1) - capistrano-bundler (>= 1.1, < 3) - capistrano-rbenv (2.2.0) - capistrano (~> 3.1) - sshkit (~> 1.3) - capybara (3.39.2) + capybara (3.40.0) addressable matrix mini_mime (>= 0.1.3) - nokogiri (~> 1.8) + nokogiri (~> 1.11) rack (>= 1.6.0) rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) - concurrent-ruby (1.2.2) + concurrent-ruby (1.2.3) connection_pool (2.4.1) crass (1.0.6) database_cleaner (2.0.2) @@ -155,9 +141,9 @@ GEM warden (~> 1.2.3) devise-i18n (1.11.1) devise (>= 4.9.0) - diff-lcs (1.5.0) + diff-lcs (1.5.1) docile (1.4.0) - domain_name (0.6.20231109) + domain_name (0.6.20240107) dotenv (2.7.6) dotenv-rails (2.7.6) dotenv (= 2.7.6) @@ -216,7 +202,6 @@ GEM activerecord kaminari-core (= 1.2.2) kaminari-core (1.2.2) - kgio (2.11.4) language_server-protocol (3.17.0.3) launchy (2.5.2) addressable (~> 2.8) @@ -238,34 +223,29 @@ GEM marcel (1.0.2) matrix (0.4.2) method_source (1.0.0) - mime-types (3.5.1) + mime-types (3.5.2) mime-types-data (~> 3.2015) mime-types-data (3.2023.1205) mini_magick (4.12.0) mini_mime (1.1.5) - mini_portile2 (2.8.5) - minitest (5.20.0) + minitest (5.22.0) msgpack (1.7.2) - net-imap (0.4.7) + net-imap (0.4.10) 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-smtp (0.4.0) + net-smtp (0.4.0.1) net-protocol - net-ssh (7.2.0) netrc (0.11.0) nio4r (2.7.0) - nokogiri (1.15.5) - mini_portile2 (~> 2.8.2) + nokogiri (1.16.2-x86_64-linux) racc (~> 1.4) orm_adapter (0.5.0) - parallel (1.23.0) - parser (3.2.2.4) + parallel (1.24.0) + parser (3.3.0.5) ast (~> 2.4.1) racc pdf-core (0.9.0) @@ -279,6 +259,8 @@ GEM prawn-table (0.2.2) prawn (>= 1.3.0, < 3.0.0) public_suffix (5.0.4) + puma (6.4.2) + nio4r (~> 2.0) pundit (2.1.1) activesupport (>= 3.0.0) raabro (1.4.0) @@ -322,15 +304,14 @@ GEM thor (~> 1.0) zeitwerk (~> 2.5) rainbow (3.1.1) - raindrops (0.20.1) rake (13.1.0) ransack (3.2.1) activerecord (>= 6.1.5) activesupport (>= 6.1.5) i18n - rdiscount (2.2.7.1) + rdiscount (2.2.7.3) redis (4.8.1) - regexp_parser (2.8.3) + regexp_parser (2.9.0) responders (3.1.1) actionpack (>= 5.2) railties (>= 5.2) @@ -340,16 +321,16 @@ GEM mime-types (>= 1.16, < 4.0) netrc (~> 0.8) rexml (3.2.6) - rollbar (3.4.2) - rspec-core (3.12.2) - rspec-support (~> 3.12.0) - rspec-expectations (3.12.3) + rollbar (3.5.1) + rspec-core (3.13.0) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.12.0) - rspec-mocks (3.12.6) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.12.0) - rspec-rails (6.1.0) + rspec-support (~> 3.13.0) + rspec-rails (6.1.1) actionpack (>= 6.1) activesupport (>= 6.1) railties (>= 6.1) @@ -357,12 +338,12 @@ GEM rspec-expectations (~> 3.12) rspec-mocks (~> 3.12) rspec-support (~> 3.12) - rspec-support (3.12.1) - rubocop (1.58.0) + rspec-support (3.13.0) + rubocop (1.60.2) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) - parser (>= 3.2.2.4) + parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) @@ -371,7 +352,7 @@ GEM unicode-display_width (>= 2.4.0, < 3.0) rubocop-ast (1.30.0) parser (>= 3.2.1.0) - rubocop-rails (2.22.2) + rubocop-rails (2.23.1) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.33.0, < 2.0) @@ -390,7 +371,8 @@ GEM sprockets-rails tilt select2-rails (4.0.13) - selenium-webdriver (4.16.0) + selenium-webdriver (4.17.0) + base64 (~> 0.2) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) @@ -412,7 +394,7 @@ GEM simplecov_json_formatter (~> 0.1) simplecov-html (0.12.3) simplecov_json_formatter (0.1.4) - skylight (6.0.1) + skylight (6.0.3) activesupport (>= 5.2.0) sprockets (4.2.1) concurrent-ruby (~> 1.0) @@ -421,9 +403,6 @@ GEM actionpack (>= 5.2) activesupport (>= 5.2) sprockets (>= 3.0.0) - sshkit (1.21.6) - net-scp (>= 1.1.2) - net-ssh (>= 2.8.0) thor (1.3.0) tilt (2.3.0) timeout (0.4.1) @@ -433,9 +412,6 @@ GEM uglifier (4.2.0) execjs (>= 0.3.0, < 3) unicode-display_width (2.5.0) - unicorn (6.1.0) - kgio (~> 2.6) - raindrops (~> 0.7) warden (1.2.9) rack (>= 2.0.9) web-console (4.1.0) @@ -443,7 +419,6 @@ GEM activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) - webrick (1.8.1) websocket (1.2.10) websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) @@ -453,7 +428,7 @@ GEM zeitwerk (2.6.12) PLATFORMS - ruby + x86_64-linux DEPENDENCIES active_storage_validations (~> 1.1.3) @@ -462,9 +437,6 @@ DEPENDENCIES bootsnap (~> 1.12) bootstrap-sass (~> 3.4) byebug (~> 11.0) - capistrano (~> 3.15.0) - capistrano-rails (~> 1.1) - capistrano-rbenv (~> 2.1) capybara (~> 3.29) database_cleaner (~> 2.0) devise (~> 4.9.1) @@ -480,10 +452,12 @@ DEPENDENCIES kaminari (~> 1.2.1) letter_opener (~> 1.7.0) localeapp (~> 3.3) + matrix (~> 0.4.1) pg (~> 1.4) pg_search (~> 2.3.5) prawn (~> 2.4.0) prawn-table (~> 0.2.2) + puma (>= 5.0.0) pundit (~> 2.1.0) rails (~> 7.0.8) rails-controller-testing @@ -504,9 +478,7 @@ DEPENDENCIES simplecov (~> 0.22) skylight (~> 6.0) uglifier (~> 4.2.0) - unicorn (~> 6.1.0) web-console (~> 4.1.0) - webrick BUNDLED WITH - 2.1.4 + 2.4.10 diff --git a/README.md b/README.md index 1479d11a2..97bbc62e3 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,65 @@ as well as being able to post offers / demand ads explained in detail. On the other hand the members can be paid the services of virtual way to save the passage through the office of the Bank of Time and also have the possibility to consult the extract of their account. +## Docker deploying + +This site is ready to be deployed in production with an optimized Docker image which uses two stages in order to minimize the final image size. + +You can locally test the production deployment by the include docker-compose.yml file: + +```bash +docker-compose up +``` + +The first time running it will build the image and setup the database along with some seeds in it (testing data). If the database already exists, it will run migrations (if needed) and just start the application. + +Go to `http://localhost:3000` to see the application running. + +> Note that the current docker-compose.yml is not suitable for a real production deployment, it's just for testing the production Dockerfile locally. +> For production deployment you should use a real database and a reverse proxy like Nginx or Apache (with SSL enabled). +> Refer to the next section in order to see the relevant ENV variables to configure the application. + +### ENV variables + +In order to configure the application you can use the following ENV variables: + +> Make sure to configure at least the ones without a default value (empty). + +| ENV | Description | Default | +| --- | --- | --- | +| `ALLOWED_HOSTS` | Put here the list of hosts allowed to access the application. Separate with spaces, for instance: `www.timeoverflow.org timeoverflow.org` | `localhost` | +| `RAILS_ENV` | Define the rails environment (not necessary to setup unless you have some special requirements) | `production` | +| `SECRET_KEY_BASE` | Secret key for the application, generate a new one with the command `rails secret` | | +| `DATABASE_URL` | Database URL, the format is `postgresql://user:password@host:port/database` | | +| `RAILS_SERVE_STATIC_FILES` | Tell the application to serve static files (you might want to turn this off if you are using an external web server to serve files from the `public` folder) | `true` | +| `RAILS_LOG_TO_STDOUT` | Tell the application to log to STDOUT (useful for Docker) | `true` | +| `RAILS_LOG_LEVEL` | Log level for the application (use `debug` for maximum information) | `info` | +| `RAILS_MAX_THREADS` | Maximum number of threads to use in the application (use `1` if multithreading is not desired) | `5` | +| `RAILS_MIN_THREADS` | Minimum number of threads to use in the application | `RAILS_MAX_THREADS` value | +| `WEB_CONCURRENCY` | Number of web server processes to use | `2` | +| `RUN_SIDEKIQ` | Run Sidekiq worker process in the docker instance (you might want to change this if want to run different docker instances for Sidekiq and Rails) | `true` | +| `RUN_RAILS` | Run Rails web server process in the docker instance | `true` | +| `QUEUE_ADAPTER` | Adapter to use for background jobs (currently the application is using exclusively Sidekiq, so no other options here right now) | `sidekiq` | +| `SIDEKIQ_CONCURRENCY` | Number of threads to use in Sidekiq | `5` | +| `STORAGE_PROVIDER` | Storage provider for the application (currently the application supports `local` and `amazon`) | `local` | +| `FORCE_SSL` | Force SSL connections | `false` | +| `MAIL_LINK_HOST` | Host to use in the links sent by email (use your domain without protocol `mydomain.tld`) | | +| `MAIL_LINK_PROTOCOL` | Protocol to use in the previous host defined for links sent by email | `https` | +| `SMTP_ADDRESS` | SMTP server address (ie: `smtp.mailgun.org`) | | +| `SMTP_PORT` | SMTP server port (ie: `587`) | | +| `SMTP_DOMAIN` | SMTP domain (usually the application's domain) | | +| `SMTP_USER_NAME` | SMTP username | | +| `SMTP_PASSWORD` | SMTP password | | +| `SMTP_AUTHENTICATION` | SMTP authentication method | `plain` | +| `SMTP_ENABLE_STARTTLS_AUTO` | Enable STARTTLS | `true` | +| `SMTP_OPENSSL_VERIFY_MODE` | OpenSSL verify mode | `none` | +| `AWS_ACCESS_KEY_ID` | AWS access key ID (only if `STORAGE_PROVIDER` is `amazon`) | | +| `AWS_SECRET_ACCESS_KEY` | AWS secret access key (only if `STORAGE_PROVIDER` is `amazon`) | | +| `AWS_BUCKET` | AWS bucket name (only if `STORAGE_PROVIDER` is `amazon`) | | +| `AWS_REGION` | AWS region (only if `STORAGE_PROVIDER` is `amazon`) | | +| `ADMINS` | Space separated list of emails for the superadmins (ie: `admin@timeoverflow.org` | | + + ## Contributions **Join our collaborators team!** diff --git a/app/admin/organization.rb b/app/admin/organization.rb index 62f85c35d..2d8fe6dcc 100644 --- a/app/admin/organization.rb +++ b/app/admin/organization.rb @@ -5,7 +5,7 @@ output = tag.p organization.name if organization.logo.attached? - output << image_tag(organization.logo.variant(resize: "40^x")) + output << image_tag(organization.logo.variant(resize_to_fill: [40, nil])) end output.html_safe @@ -29,7 +29,7 @@ show do div do if organization.logo.attached? - image_tag(organization.logo.variant(resize: "100^x")) + image_tag(organization.logo.variant(resize_to_fill: [100, nil])) end end default_main_content diff --git a/app/assets/javascripts/libs.js b/app/assets/javascripts/libs.js index 1c941bd13..7f1ebda1b 100644 --- a/app/assets/javascripts/libs.js +++ b/app/assets/javascripts/libs.js @@ -1,7 +1,7 @@ //= require jquery2 //= require jquery_ujs -//= require jquery.validate +//= require vendor/jquery.validate //= require bootstrap -//= require highcharts -//= require highcharts-exporting +//= require vendor/highcharts +//= require vendor/highcharts-exporting //= require select2 diff --git a/vendor/assets/javascripts/highcharts-exporting.js b/app/assets/javascripts/vendor/highcharts-exporting.js similarity index 100% rename from vendor/assets/javascripts/highcharts-exporting.js rename to app/assets/javascripts/vendor/highcharts-exporting.js diff --git a/vendor/assets/javascripts/highcharts.js b/app/assets/javascripts/vendor/highcharts.js similarity index 100% rename from vendor/assets/javascripts/highcharts.js rename to app/assets/javascripts/vendor/highcharts.js diff --git a/vendor/assets/javascripts/jquery.validate.js b/app/assets/javascripts/vendor/jquery.validate.js similarity index 100% rename from vendor/assets/javascripts/jquery.validate.js rename to app/assets/javascripts/vendor/jquery.validate.js diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 5fe37cdd8..eff524c3e 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -8,7 +8,7 @@ def page_title def avatar_url(user, size = 32) user.avatar.attached? ? - user.avatar.variant(resize: "#{size}x#{size}") : + user.avatar.variant(resize_to_fit: [size, size]) : gravatar_url(user, size) end @@ -31,7 +31,7 @@ def organization_logo return if "#{controller_name}##{action_name}".in? %w(organizations#index pages#show) content_tag(:div, class: "row organization-logo") do - image_tag org.logo.variant(resize: "x200^") + image_tag org.logo.variant(resize_to_fit: [200, nil]) end end diff --git a/config/application.rb b/config/application.rb index c8e9a4af5..b53ef22c6 100644 --- a/config/application.rb +++ b/config/application.rb @@ -23,17 +23,16 @@ class Application < Rails::Application # SKYLIGHT_AUTHENTICATION env var for this to work. config.skylight.environments += ["staging"] - # ActiveJob configuration - config.active_job.queue_adapter = :sidekiq - # Use db/structure.sql with SQL as schema format # This is needed to store in the schema SQL statements not covered by the ORM config.active_record.schema_format = :sql # Guard against DNS rebinding attacks by permitting hosts - config.hosts << 'timeoverflow.local' - config.hosts << 'staging.timeoverflow.org' - config.hosts << 'www.timeoverflow.org' - config.hosts << 'timeoverflow.org' + # localhost is necessary for the docker image + config.hosts = ENV.fetch('ALLOWED_HOSTS', 'localhost').split(' ') + # config.hosts << 'timeoverflow.local' + # config.hosts << 'staging.timeoverflow.org' + # config.hosts << 'www.timeoverflow.org' + # config.hosts << 'timeoverflow.org' end end diff --git a/config/database.yml b/config/database.yml index a840d2a02..a166b48d6 100644 --- a/config/database.yml +++ b/config/database.yml @@ -1,8 +1,14 @@ defaults: &defaults adapter: postgresql - username: <%= ENV['DATABASE_USER'] || ENV["POSTGRES_USER"] %> + username: <%= ENV['DATABASE_USER'] || ENV["POSTGRES_USER"] || ENV["DATABASE_USERNAME"] %> + password: <%= ENV['DATABASE_PASSWORD'] || ENV["POSTGRES_PASSWORD"] %> + pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + host: <%= ENV.fetch("DATABASE_HOST") { "localhost" } %> + port: <%= ENV.fetch("DATABASE_PORT") { "5432" } %> template: 'template0' - encoding: 'UTF8' + encoding: unicode + # For details on connection pooling, see rails configuration guide + # http://guides.rubyonrails.org/configuring.html#database-pooling development: <<: *defaults @@ -16,12 +22,9 @@ test: staging: <<: *defaults - collation: 'es_ES.UTF-8' - ctype: 'es_ES.UTF-8' database: <%= ENV.fetch('DATABASE_NAME', 'timeoverflow_staging') %> production: <<: *defaults - collation: 'es_ES.UTF-8' - ctype: 'es_ES.UTF-8' - database: <%= ENV.fetch('DATABASE_NAME', 'timeoverflow_production') %> + <%= "url: #{ENV['DATABASE_URL']}" if ENV['DATABASE_URL'].present? %> + <%= "database: #{ENV.fetch('DATABASE_NAME', 'timeoverflow_production')}" unless ENV['DATABASE_URL'].present? %> diff --git a/config/deploy.rb b/config/deploy.rb deleted file mode 100644 index a58a3f616..000000000 --- a/config/deploy.rb +++ /dev/null @@ -1,81 +0,0 @@ -# config valid only for current version of Capistrano -lock '3.15.0' - -set :application, 'timeoverflow' -set :repo_url, 'git@github.com:coopdevs/timeoverflow.git' - -set :rbenv_type, :user - -# Default branch is :master -ask :branch, `git rev-parse --abbrev-ref HEAD`.chomp - -# Default deploy_to directory is /var/www/my_app_name -# set :deploy_to, '/var/www/my_app_name' - -# Default value for :scm is :git -# set :scm, :git - -# Default value for :format is :pretty -# set :format, :pretty - -# Default value for :log_level is :debug -# set :log_level, :debug - -# Default value for :pty is false -# set :pty, true - -# Default value for :linked_files is [] -set :linked_files, fetch(:linked_files, []).push( - # "config/database.yml", - # "config/secrets.yml", - # ".env", -) - -# Default value for linked_dirs is [] -set :linked_dirs, fetch(:linked_dirs, []).push( - 'log', - 'tmp/pids', - 'tmp/cache', - 'tmp/sockets', - 'vendor/bundle', - 'public/system' -) - -# Default value for default_env is {} -# set :default_env, { path: "/opt/ruby/bin:$PATH" } - -# Default value for keep_releases is 5 -# set :keep_releases, 5 - -namespace :unicorn do - desc 'reload Unicorn' - task :reload do - on roles(:app) do - execute "sudo systemctl reload timeoverflow" - end - end -end - -namespace :sidekiq do - desc 'reload Sidekiq' - task :restart do - on roles(:app) do - execute "sudo systemctl restart sidekiq" - end - end -end - -task "deploy:db:load" do - on primary :db do - within release_path do - with rails_env: fetch(:rails_env) do - execute :rake, "db:schema:load" - end - end - end -end - -before "deploy:migrate", "deploy:db:load" if ENV["COLD"] - -after "deploy:finishing", "unicorn:reload" -after "deploy:finishing", "sidekiq:restart" diff --git a/config/deploy/production.rb b/config/deploy/production.rb deleted file mode 100644 index 240ae0b0a..000000000 --- a/config/deploy/production.rb +++ /dev/null @@ -1 +0,0 @@ -server 'www.timeoverflow.org', user: 'timeoverflow', roles: %w(app db web) diff --git a/config/deploy/staging.rb b/config/deploy/staging.rb deleted file mode 100644 index 59ea3607c..000000000 --- a/config/deploy/staging.rb +++ /dev/null @@ -1 +0,0 @@ -server 'staging.timeoverflow.org', user: 'timeoverflow', roles: %w(app db web) diff --git a/config/environments/production.rb b/config/environments/production.rb index c005b5320..5e21ce380 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -1,3 +1,6 @@ +# Uglifier is only used on the precompile phase, so we can require it conditionally +require "uglifier" if ENV["SECRET_KEY_BASE"] == "dummy" + Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. @@ -20,28 +23,30 @@ # Disable serving static files from the `/public` folder by default since # Apache or NGINX already handles this. - config.public_file_server.enabled = true + config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present? - # Compress CSS & JS using preprocessors. - config.assets.js_compressor = Uglifier.new(harmony: true) - config.assets.css_compressor = :sass + # Compress CSS & JS using preprocessors only on the precompile phase + if ENV["SECRET_KEY_BASE"] == "dummy" + config.assets.js_compressor = Uglifier.new(harmony: true) + config.assets.css_compressor = :sass + end # Do not fallback to assets pipeline if a precompiled asset is missed. - config.assets.compile = true + config.assets.compile = false # Asset digests allow you to set far-future HTTP expiration dates on all assets, # yet still be able to expire them through the digest params. config.assets.digest = true # Enable serving of images, stylesheets, and JavaScripts from an asset server. - # config.action_controller.asset_host = 'http://assets.example.com' + config.asset_host = ENV.fetch("RAILS_ASSET_HOST", nil) if ENV["RAILS_ASSET_HOST"].present? # Specifies the header that your server uses for sending files. # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX # Store uploaded files on the local file system (see config/storage.yml for options). - config.active_storage.service = :amazon + config.active_storage.service = ENV.fetch("STORAGE_PROVIDER", :local) # Mount Action Cable outside main process or domain. # config.action_cable.mount_path = nil @@ -49,20 +54,24 @@ # 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.force_ssl = true + config.force_ssl = ENV.fetch("FORCE_SSL", "true") == "true" # Use the lowest log level to ensure availability of diagnostic information # when problems arise. - config.log_level = :debug + config.log_level = if %w(debug info warn error fatal).include?(ENV.fetch("RAILS_LOG_LEVEL", nil)) + ENV["RAILS_LOG_LEVEL"] + else + :info + end # Prepend all log lines with the following tags. - config.log_tags = [ :request_id ] + config.log_tags = [:request_id] # Use a different cache store in production. # config.cache_store = :mem_cache_store # Use a real queuing backend for Active Job (and separate queues per environment). - # config.active_job.queue_adapter = :resque + config.active_job.queue_adapter = :sidekiq # config.active_job.queue_name_prefix = "timeoverflow_production" config.action_mailer.perform_caching = false @@ -74,10 +83,10 @@ config.action_mailer.delivery_method = :smtp config.action_mailer.default_url_options = { host: ENV["MAIL_LINK_HOST"], - protocol: (ENV["MAIL_LINK_PROTO"] || "https") + protocol: ENV["MAIL_LINK_PROTO"] || "https" } - smtp_env = Hash[ENV.map do |k,v| + smtp_env = Hash[ENV.map do |k, v| if /^SMTP_(.*)$/ === k [$1.downcase.to_sym, YAML.load(v)] end diff --git a/config/puma.rb b/config/puma.rb new file mode 100644 index 000000000..db11c1ed7 --- /dev/null +++ b/config/puma.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +# Puma can serve each request in a thread from an internal thread pool. +# The `threads` method setting takes two numbers: a minimum and maximum. +# Any libraries that use thread pools should be configured to match +# the maximum value specified for Puma. Default is set to 5 threads for minimum +# and maximum; this matches the default thread size of Active Record. +# +max_threads_count = ENV.fetch("RAILS_MAX_THREADS", 5) +min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } +threads min_threads_count, max_threads_count + +# Specifies the `worker_timeout` threshold that Puma will use to wait before +# terminating a worker in development environments. +# +worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development" + +# Specifies the `port` that Puma will listen on to receive requests; default is 3000. +# +port ENV.fetch("PORT", 3000) + +# Specifies the `environment` that Puma will run in. +# +environment ENV.fetch("RAILS_ENV", "development") + +# Specifies the `pidfile` that Puma will use. +pidfile ENV.fetch("PIDFILE", "tmp/pids/server.pid") + +# Specifies the number of `workers` to boot in clustered mode. +# Workers are forked web server processes. If using threads and workers together +# the concurrency of the application would be max `threads` * `workers`. +# Workers do not work on JRuby or Windows (both of which do not support +# processes). +# +if ENV.fetch("RAILS_ENV") == "production" + workers ENV.fetch("WEB_CONCURRENCY", 2) + + # Use the `preload_app!` method when specifying a `workers` number. + # This directive tells Puma to first boot the application and load code + # before forking the application. This takes advantage of Copy On Write + # process behavior so workers use less memory. + # + preload_app! +else + # Allow puma to be restarted by `rails restart` command. + plugin :tmp_restart +end diff --git a/config/sidekiq.yml b/config/sidekiq.yml index d9d221a4c..25f4cd85c 100644 --- a/config/sidekiq.yml +++ b/config/sidekiq.yml @@ -1,3 +1,4 @@ +:concurrency: <%= ENV.fetch("SIDEKIQ_CONCURRENCY", "5").to_i %> :queues: - default - cron diff --git a/config/unicorn.rb b/config/unicorn.rb deleted file mode 100644 index 68fc5017c..000000000 --- a/config/unicorn.rb +++ /dev/null @@ -1,29 +0,0 @@ -app_path = File.expand_path(File.dirname(__FILE__) + '/..') - -worker_processes 2 - -listen 8080, tcp_nopush: true - -working_directory app_path - -pid app_path + '/tmp/unicorn.pid' - -stderr_path '/var/www/timeoverflow/shared/log/unicorn.err.log' -stdout_path '/var/www/timeoverflow/shared/log/unicorn.std.log' - -# Load the app up before forking -# Combine Ruby 2.0.0+ with "preload_app true" for memory savings -preload_app true - -before_fork do |server, worker| - # the following is highly recomended for Rails + "preload_app true" - # as there's no need for the master process to hold a connection - defined?(ActiveRecord::Base) and - ActiveRecord::Base.connection.disconnect! -end - -after_fork do |server, worker| - # the following is *required* for Rails + "preload_app true" - defined?(ActiveRecord::Base) and - ActiveRecord::Base.establish_connection -end diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..fdbe4054f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,49 @@ +version: '3' +services: + app: + build: . + volumes: + - ./entrypoint.sh:/app/entrypoint.sh + - ./db/seeds.rb:/app/db/seeds.rb + - ./storage:/app/storage + environment: + - DATABASE_URL=postgres://postgres:timeoverflow@db/timeoverflow + - SECRET_KEY_BASE=d2a645fb46fbd3d4380fb22230ddea4062570eb00853ca5dfe97f8bb1cbff1ad6891c573a4b4b06beb2d0baf59afc9e00794314490a644fc5808ad6cbc3a6379 + - FORCE_SSL=false + - RAILS_LOG_LEVEL=debug + - REDIS_URL=redis://redis:6379/0 + - ADMINS=admin@timeoverflow.org + ports: + - 3000:3000 + depends_on: + - db + - redis + sidekiq: + build: . + volumes: + - ./entrypoint.sh:/app/entrypoint.sh + - ./storage:/app/storage + environment: + - DATABASE_URL=postgres://postgres:timeoverflow@db/timeoverflow + - SECRET_KEY_BASE=d2a645fb46fbd3d4380fb22230ddea4062570eb00853ca5dfe97f8bb1cbff1ad6891c573a4b4b06beb2d0baf59afc9e00794314490a644fc5808ad6cbc3a6379 + - FORCE_SSL=false + - RAILS_LOG_LEVEL=debug + - REDIS_URL=redis://redis:6379/0 + - ADMINS=admin@timeoverflow.org + - RUN_SIDEKIQ=true + depends_on: + - db + - redis + db: + image: postgres:14-alpine + environment: + - POSTGRES_PASSWORD=timeoverflow + volumes: + - pg_data:/var/lib/postgresql/data + redis: + image: redis + volumes: + - redis_data:/data +volumes: + pg_data: + redis_data: diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 000000000..f189146b8 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +# Run rails by default if sidekiq is specified +if [ -z "$RUN_RAILS" ] && [ -z "$RUN_SIDEKIQ" ]; then + RUN_RAILS=true + echo "⚠️ RUN_RAILS and RUN_SIDEKIQ are not set, defaulting to RUN_RAILS=true, RUN_SIDEKIQ=false" +fi + +# ensure booleans +if [ "$RUN_RAILS" == "true" ] || [ "$RUN_RAILS" == "1" ]; then + RUN_RAILS=true +else + RUN_RAILS=false +fi +if [ "$RUN_SIDEKIQ" == "true" ] || [ "$RUN_SIDEKIQ" == "1" ]; then + RUN_SIDEKIQ=true +else + RUN_SIDEKIQ=false +fi + +if [ "$RUN_RAILS" == "true" ]; then + echo "✅ Running Rails" +fi + +if [ "$RUN_SIDEKIQ" == "true" ]; then + echo "✅ Running Sidekiq" +fi + +export RUN_RAILS +export RUN_SIDEKIQ + +# Check all the gems are installed or fails. +bundle check +if [ $? -ne 0 ]; then + echo "❌ Gems in Gemfile are not installed, aborting..." + exit 1 +else + echo "✅ Gems in Gemfile are installed" +fi + +# if no database, run setup +if [ -z "$SKIP_DATABASE_SETUP" ]; then + bundle exec rails db:setup +else + echo "⚠️ Skipping database setup" +fi + +# Check no migrations are pending migrations +if [ -z "$SKIP_MIGRATIONS" ]; then + bundle exec rails db:migrate +else + echo "⚠️ Skipping migrations" +fi + +echo "✅ Migrations are all up" + +echo "🚀 $@" +exec "$@" \ No newline at end of file diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 5a46b6d61..7fa705576 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -10,7 +10,7 @@ img = helper.avatar_url(user, 50) expect(img.class).to eq(ActiveStorage::VariantWithRecord) - expect(img.variation.transformations[:resize]).to eq("50x50") + expect(img.variation.transformations[:resize_to_fit]).to eq([50, 50]) expect(img.blob.filename).to eq("name.png") end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 435df653c..356e0bbc5 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -21,7 +21,7 @@ require 'faker' require 'shoulda/matchers' -Capybara.server = :webrick +Capybara.server = :puma Capybara.register_driver :headless_chrome do |app| browser_options = Selenium::WebDriver::Chrome::Options.new( args: %w(headless disable-gpu no-sandbox) diff --git a/supervisord.conf b/supervisord.conf new file mode 100644 index 000000000..118c3133a --- /dev/null +++ b/supervisord.conf @@ -0,0 +1,25 @@ +[supervisord] +nodaemon=true +logfile=/tmp/supervisord.log +pidfile=/tmp/supervisord.pid + +[program:puma] +command=bin/rails server -b 0.0.0.0 +directory=/app +# If RUN_RAILS is not set, defaults to RUN_SIDEKIQ not being defined +autostart=%(ENV_RUN_RAILS)s +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 + +[program:sidekiq] +command=bundle exec sidekiq -C config/sidekiq.yml +directory=/app +autostart=%(ENV_RUN_SIDEKIQ)s +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 + +[supervisorctl] diff --git a/vendor/assets/stylesheets/.keep b/vendor/assets/stylesheets/.keep deleted file mode 100644 index e69de29bb..000000000