diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8b40251 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,46 @@ +.git/ + +# Ignore bundler config. +/.bundle + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/ +!/tmp/pids/.keep + +# Ignore uploaded files in development. +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/ +!/tmp/storage/.keep + +/public/assets + +# Ignore master key for decrypting credentials and more. +/config/master.key + +/app/assets/builds/* +!/app/assets/builds/.keep + +# Ignore SampleCov files +/coverage/* + +# Vite Ruby +/public/vite* +node_modules +# Vite uses dotenv and suggests to ignore local-only env files. See +# https://vitejs.dev/guide/env-and-mode.html#env-files +*.local + +# Ignore uncategorized files +.DS_Store diff --git a/.env b/.env new file mode 100644 index 0000000..a74ef2c --- /dev/null +++ b/.env @@ -0,0 +1,83 @@ + +# Rather than use the directory name, let's control the name of the project. +export COMPOSE_PROJECT_NAME=baseapp + +# Which environment is running? These should be "development" or "production". + +# About COMPOSE_PROFILES: https://docs.docker.com/compose/profiles/ +# In development we want all services to start but in production you don't +# need the asset watchers to run since assets get built into the image. +# +export RAILS_ENV=production +export COMPOSE_PROFILES=postgres,redis,web,worker,cable + +# Should Docker restart your containers if they go down in unexpected ways? +export DOCKER_RESTART_POLICY=unless-stopped +# export DOCKER_RESTART_POLICY=no + +# What ip:port should be published back to the Docker host for the app server? +# If you're using Docker Toolbox or a custom VM you can't use 127.0.0.1. This +# is being overwritten in dev to be compatible with more dev environments. +# +# If you have a port conflict because something else is using 3000 then you +# can either stop that process or change 3000 to be something else. +# +# Use the default in production to avoid having puma directly accessible to +# the internet since it'll very likely be behind nginx or a load balancer. +export DOCKER_WEB_PORT_FORWARD=127.0.0.1:3000 +# export DOCKER_WEB_PORT_FORWARD=3000 + +# This is the same as above except for Action Cable. +export DOCKER_CABLE_PORT_FORWARD=127.0.0.1:28080 +# export DOCKER_CABLE_PORT_FORWARD=28080 + +# What CPU and memory constraints will be added to your services? When left at +# 0 they will happily use as much as needed. +# export DOCKER_POSTGRES_CPUS=0 +# export DOCKER_POSTGRES_MEMORY=0 +# export DOCKER_REDIS_CPUS=0 +# export DOCKER_REDIS_MEMORY=0 +# export DOCKER_WEB_CPUS=0 +# export DOCKER_WEB_MEMORY=0 +# export DOCKER_WORKER_CPUS=0 +# export DOCKER_WORKER_MEMORY=0 +# export DOCKER_CABLE_CPUS=0 +# export DOCKER_CABLE_MEMORY=0 + +## Secret keys +# You can use `rails secret` command to generate a secret key +export SECRET_KEY_BASE=insecure-key +export DEVISE_JWT_SECRET_KEY=my-jwt-secret-key + +## Host +export DEFAULT_HOST=localhost + +## Action cable +export ACTION_CABLE_URL=ws://localhost:28080 +export ACTION_CABLE_ALLOWED_REQUEST_ORIGINS=http:\/\/localhost* +# Examples: +# http:\/\/localhost* +# http:\/\/example.*,https:\/\/example.* + +## Puma +# export PORT=3000 + +## Workers and threads count +export WEB_CONCURRENCY=2 +export RAILS_MAX_THREADS=5 +export RAILS_MIN_THREADS=5 + +## Postgres +export POSTGRES_HOST=postgres +export POSTGRES_PORT=5432 +export POSTGRES_USER=baseapp +export POSTGRES_PASSWORD=postgres +export POSTGRES_DB=baseapp + +## Redis URL +export REDIS_URL=redis://redis:6379/1 +export REDIS_CHANNEL_PREFIX=baseapp + +# Sidekiq web +export SIDEKIQ_WEB_USERNAME=sidekiq-web-dashboard +export SIDEKIQ_WEB_PASSWORD=sidekiq-web-123 diff --git a/.env.example b/.env.example deleted file mode 100644 index 012f8d0..0000000 --- a/.env.example +++ /dev/null @@ -1,42 +0,0 @@ - -# export RAILS_ENV=development - -## Host -export DEFAULT_HOST=example.com - -## Puma -# export PORT=3000 -# export PIDFILE=tmp/pids/server.pid -## Workers and threads count -# export WEB_CONCURRENCY=2 -# export RAILS_MAX_THREADS=5 -# export RAILS_MIN_THREADS=5 - -## Postgres -# export POSTGRES_HOST=postgres -# export POSTGRES_PORT=5432 -export POSTGRES_USER=baseapp -export POSTGRES_PASSWORD=123456 -export POSTGRES_DB=baseapp - -## Redis URL -# export REDIS_URL=redis://redis:6379/1 -# export REDIS_CHANNEL_PREFIX=baseapp - -## Action cable -# export ACTION_CABLE_URL=ws://localhost:28080 -# export ACTION_CABLE_ALLOWED_REQUEST_ORIGINS=http:\/\/localhost* -# Examples: -# http:\/\/localhost* -# http:\/\/example.*,https:\/\/example.* - -## Sidekiq web -# export SIDEKIQ_WEB_USERNAME=sidekiq-web-dashboard -# export SIDEKIQ_WEB_PASSWORD=sidekiq-web-123 - -## Secret keys -# You can use `rake secret` command to generate a secret key -export DEVISE_JWT_SECRET_KEY=my-jwt-secret-key - -# frontend config -VITE_API_URL=http://localhost:3000 \ No newline at end of file diff --git a/.env.test b/.env.test index 35cc8c0..8576b46 100644 --- a/.env.test +++ b/.env.test @@ -1,4 +1,6 @@ +# This file will be used by github workflows + ## Host export DEFAULT_HOST=localhost diff --git a/.gitignore b/.gitignore index 59d8684..e912fc0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,3 @@ -# See https://help.github.com/articles/ignoring-files for more about ignoring files. -# -# If you find yourself ignoring temporary files generated by your text editor -# or operating system, you probably want to add a global ignore instead: -# git config --global core.excludesfile '~/.gitignore_global' - # Ignore bundler config. /.bundle @@ -39,15 +33,8 @@ yarn-error.log* # Ignore SampleCov files /coverage/* -# Ignore env files -.env* -!.env.example -!.env.test - # Vite Ruby -/public/vite -/public/vite-dev -/public/vite-test +/public/vite* node_modules # Vite uses dotenv and suggests to ignore local-only env files. See # https://vitejs.dev/guide/env-and-mode.html#env-files diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d95b01b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,69 @@ +ARG RUBY_VERSION +ARG IMAGE_FLAVOUR=alpine + +FROM ruby:$RUBY_VERSION-$IMAGE_FLAVOUR AS base + +# Install system dependencies required both at runtime and build time +ARG NODE_VERSION +ARG YARN_VERSION +RUN apk add --update \ + git \ + postgresql-dev \ + tzdata \ + nodejs=$NODE_VERSION \ + yarn=$YARN_VERSION + +###################################################################### + +# This stage will be responsible for installing gems and npm packages +FROM base AS dependencies + +# Install system dependencies required to build some Ruby gems (pg) +RUN apk add --update build-base +RUN mkdir /app +WORKDIR /app + +COPY .ruby-version Gemfile Gemfile.lock ./ + +# Install gems +ARG RAILS_ENV +ENV RAILS_ENV="${RAILS_ENV}" \ + NODE_ENV="development" + +RUN bundle config set without "development test" +RUN bundle install --jobs "$(nproc)" --retry "$(nproc)" + +COPY package.json yarn.lock ./ + +# Install npm packages +RUN yarn install --frozen-lockfile + +COPY . ./ + +RUN SECRET_KEY_BASE=irrelevant DEVISE_JWT_SECRET_KEY=irrelevant bundle exec rails assets:precompile + +###################################################################### + +# We're back at the base stage +FROM base AS app + +# Create a non-root user to run the app and own app-specific files +RUN adduser -D app + +# Switch to this user +USER app + +# We'll install the app in this directory +WORKDIR /app + +# Copy over gems from the dependencies stage +COPY --from=dependencies /usr/local/bundle/ /usr/local/bundle/ +COPY --chown=app --from=dependencies /app/public/ /app/public/ + +# Finally, copy over the code +# This is where the .dockerignore file comes into play +# Note that we have to use `--chown` here +COPY --chown=app . ./ + +# Launch the server +CMD ["rails", "s"] diff --git a/Gemfile.lock b/Gemfile.lock index d4506bd..5fcacc3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -180,6 +180,7 @@ GEM matrix (0.4.2) method_source (1.0.0) mini_mime (1.1.2) + mini_portile2 (2.8.0) minitest (5.16.3) msgpack (1.6.0) net-imap (0.3.1) @@ -191,6 +192,9 @@ GEM net-smtp (0.3.3) net-protocol nio4r (2.5.8) + nokogiri (1.13.9) + mini_portile2 (~> 2.8.0) + racc (~> 1.4) nokogiri (1.13.9-arm64-darwin) racc (~> 1.4) nokogiri (1.13.9-x86_64-linux) @@ -365,6 +369,7 @@ GEM PLATFORMS arm64-darwin-21 arm64-darwin-22 + ruby x86_64-linux DEPENDENCIES diff --git a/README.md b/README.md index eaa6589..4d9ae70 100644 --- a/README.md +++ b/README.md @@ -2,18 +2,40 @@ Ruby On Rails Vite Ruby Vue.js + Docker

# An example Rails 7 app [![](https://badgen.net/badge/Rails/7.0.4/red)](https://github.com/zakariaf/rails-base-app/blob/main/Gemfile.lock) [![](https://badgen.net/badge/Ruby/3.1.2/red)](https://github.com/zakariaf/rails-base-app/blob/main/.ruby-version) [![](https://img.shields.io/badge/dynamic/json?color=red&label=Vite&query=%24.devDependencies.vite&url=https%3A%2F%2Fraw.githubusercontent.com%2Fzakariaf%2Frails-base-app%2Fmain%2Fpackage.json)](https://github.com/zakariaf/rails-base-app/blob/main/package.json) [![](https://img.shields.io/badge/dynamic/json?color=brightgreen&label=Vue&query=%24.dependencies.vue&url=https%3A%2F%2Fraw.githubusercontent.com%2Fzakariaf%2Frails-base-app%2Fmain%2Fpackage.json)](https://github.com/zakariaf/rails-base-app/blob/main/package.json) [![](https://img.shields.io/badge/dynamic/json?color=blue&label=TypeScript&query=%24.devDependencies.typescript&url=https%3A%2F%2Fraw.githubusercontent.com%2Fzakariaf%2Frails-base-app%2Fmain%2Fpackage.json)](https://github.com/zakariaf/rails-base-app/blob/main/package.json) [![GitHub license](https://img.shields.io/github/license/zakariaf/rails-base-app)](https://github.com/zakariaf/rails-base-app/blob/main/LICENSE) -**This app is built with Rails 7, Ruby 3, Vite, Vue 3 and typescript.** You could use this example app as a base for your upcoming projects. Or, you could use it as a tutorial that tells you which steps you need to take to create a project from scratch. +**This app is built with Rails 7, Ruby 3, Vite, Vue 3 and typescript. and is using Docker for building production images** You could use this example app as a base for your upcoming projects. Or, you could use it as a tutorial that tells you which steps you need to take to create a project from scratch. Several gems and packages are included in this example app that I've been using for a long time. It wires up a number of things you might use in a real world Rails app. However, at the same time it's not loaded up with a million personal opinions. - As [Webpacker](https://github.com/rails/webpacker#webpacker-has-been-retired-) has been retired, we are using [Vite](https://vite-ruby.netlify.app/) instead. It wouldn't be fair if I didn't say that: **Vite** is fantastic. + + +## Table of Contents + +- [Tech stack](#tech-stack) + - [Back-end](#back-end) + - [Front-end](#front-end) + - [Healthy app](#healthy-app) + - [Auth](#auth) + - [Apps](#apps) +- [Running app](#running-app) + - [Clone the repo](#clone-the-repo) + - [Install dependencies](#install-dependencies) + - [Copy .env to .env.local](#copy-env-to-envlocal) + - [Setup database](#setup-database) + - [Run the app](#run-the-app) +- [Renaming the project](#renaming-the-project) +- [Docker](#docker) +- [How to contribute](#how-to-contribute) +- [License](#license) + ## Tech stack Initially, I used the `rails new baseapp -c tailwindcss -d postgresql` command to initialize the project using the importmaps and default configurations, but I have since removed the importmaps, tailwindcss, and all default configurations in favor of using Vite. @@ -154,51 +176,69 @@ Two simple html/css templates have been added for **Website** and **Panel**. you ![Website and Panel preview](https://zakaria.dev/repos_images/website.png) -## Running this app +## Running app + +I generally recommend to use Docker only for building production images, and not for development. hence I didn't add any docker configs for development. -You need to do few small steps to run the app +To run the app locally, you need to have [Ruby](https://www.ruby-lang.org/en/) and [PostgreSQL](https://www.postgresql.org/) installed on your machine. -### Clone the repo +### 1. Clone the repo -```sh +```bash git clone https://github.com/zakariaf/rails-base-app baseapp cd baseapp ``` -### Copy example file +### 2. Install dependencies + +```bash +bundle install # install ruby gems +yarn install # install node packages +``` + +### 3. Copy .env to .env.local + +`.env` file is used for production and `.env.local` will be used for development + +Usually, you need to change the Postgres variables in `.env.local` file to match your local database. -```sh -cp .env.example .env.local +```bash +cp .env .env.local ``` -Environment variables defined here(`.env`), feel free to change or add variables as needed. -This file is ignored from git (Check `.gitignore`) so it will never be commit. +### 4. Setup database + +```bash +bundle rails db:setup +``` -If you use different values for environment variables in other envs, e.g. **test**, you need to copy one more: `.env.test.local` +### 5. Run the app -**Note** `.env.test` is used by github workflows. +- Run the server -### Setup the project +```bash +bundle rails s +``` -create databases +- Run the frontend -```sh -rails db:setup +```bash +yarn dev ``` -### start the project +## Docker + +As I mentioned before, We use Docker only for building production images. We are using [Docker Compose](https://docs.docker.com/compose/) to build the images and run the containers. You can check the `docker-compose.yml` file to see the configurations. and you can check the `Dockerfile` file to see the configurations for the production image. -- rails server +Dockerize was done by this MR [Dockerize the app](https://github.com/zakariaf/rails-base-app/pull/23) - ```sh - rails s - ``` +**NOTE** Documentation about docker is not complete yet, I will update it soon. -- frontend app +### 1. Build the images - ```sh - yarn dev - ``` +```bash +docker compose build +``` ## Renaming the project @@ -225,9 +265,22 @@ name later on. I got the rename script idea and codes from [Docker Rails Example](https://github.com/nickjj/docker-rails-example#running-a-script-to-automate-renaming-the-project) project with some small changes. +## How to contribute + +I'm happy to accept any contributions you might want to make. Please follow these steps: + +1. Fork the repo +2. Create a new branch +3. Make your changes +4. Run the test suite +5. Submit a pull request + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details + ## TODO -- [ ] Add cypress -- [ ] Dockerize -- [ ] automatic deploy process using capistrano -- [ ] add .gitlab-ci +- [ ] automat deploy process using capistrano +- [ ] Add cypress (e2e testing) +- [ ] add .gitlab-ci (gitlab users) diff --git a/bin/bundle b/bin/bundle index 553c398..981e650 100755 --- a/bin/bundle +++ b/bin/bundle @@ -8,7 +8,7 @@ # this file is here to facilitate running it. # -require 'rubygems' +require "rubygems" m = Module.new do module_function @@ -18,12 +18,12 @@ m = Module.new do end def env_var_version - ENV['BUNDLER_VERSION'] + ENV["BUNDLER_VERSION"] end def cli_arg_version return unless invoked_as_script? # don't want to hijack other binstubs - return unless 'update'.start_with?(ARGV.first || ' ') # must be running `bundle update` + return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` bundler_version = nil update_index = nil ARGV.each_with_index do |a, i| @@ -38,16 +38,16 @@ m = Module.new do end def gemfile - gemfile = ENV['BUNDLE_GEMFILE'] + gemfile = ENV["BUNDLE_GEMFILE"] return gemfile if gemfile && !gemfile.empty? - File.expand_path('../../Gemfile', __FILE__) + File.expand_path("../Gemfile", __dir__) end def lockfile lockfile = case File.basename(gemfile) - when 'gems.rb' then gemfile.sub(/\.rb$/, gemfile) + when "gems.rb" then gemfile.sub(/\.rb$/, gemfile) else "#{gemfile}.lock" end File.expand_path(lockfile) @@ -73,26 +73,26 @@ m = Module.new do requirement = bundler_gem_version.approximate_recommendation - return requirement unless Gem::Version.new(Gem::VERSION) < Gem::Version.new('2.7.0') + return requirement unless Gem.rubygems_version < Gem::Version.new("2.7.0") - requirement += '.a' if bundler_gem_version.prerelease? + requirement += ".a" if bundler_gem_version.prerelease? requirement end def load_bundler! - ENV['BUNDLE_GEMFILE'] ||= gemfile + ENV["BUNDLE_GEMFILE"] ||= gemfile activate_bundler end def activate_bundler gem_error = activation_error_handling do - gem 'bundler', bundler_requirement + gem "bundler", bundler_requirement end return if gem_error.nil? require_error = activation_error_handling do - require 'bundler/version' + require "bundler/version" end return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" @@ -110,5 +110,5 @@ end m.load_bundler! if m.invoked_as_script? - load Gem.bin_path('bundler', 'bundle') + load Gem.bin_path("bundler", "bundle") end diff --git a/bin/rename-project b/bin/rename-project index e52b611..bfe5e0b 100755 --- a/bin/rename-project +++ b/bin/rename-project @@ -21,15 +21,27 @@ fi cat << EOF When renaming your project you'll need to re-create a new database. -This can easily be done by running `rails db:setup`. -If you are using the project with no Docker, previous databases will be remain and -you can use their names in the env variables. - -I didn't dockerize the repo yet, but after dockerization, script deletes your current +This can easily be done with Docker, but before this script does it +please agree that it's ok for this script to delete your current project's database(s) by removing any associated Docker volumes. EOF + +while true; do + read -p "Run docker compose down -v (y/n)? " -r yn + case "${yn}" in + [Yy]* ) + printf "\n--------------------------------------------------------\n" + docker compose down -v + printf -- "--------------------------------------------------------\n" + + break;; + [Nn]* ) exit;; + * ) echo "";; + esac +done + # ----------------------------------------------------------------------------- # The core of the script which renames a few things. # ----------------------------------------------------------------------------- @@ -51,6 +63,7 @@ function init_git_repo { [ -d .git/ ] && rm -rf .git/ cat << EOF + -------------------------------------------------------- $(git init) -------------------------------------------------------- diff --git a/bin/vite b/bin/vite index 31dbbbe..e1aaa73 100755 --- a/bin/vite +++ b/bin/vite @@ -8,14 +8,12 @@ # this file is here to facilitate running it. # -require 'pathname' -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', - Pathname.new(__FILE__).realpath) +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) -bundle_binstub = File.expand_path('../bundle', __FILE__) +bundle_binstub = File.expand_path('bundle', __dir__) if File.file?(bundle_binstub) - if /This file was generated by Bundler/.match?(File.read(bundle_binstub, 300)) + if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ load(bundle_binstub) else abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. diff --git a/cable/config.ru b/cable/config.ru new file mode 100644 index 0000000..338dff9 --- /dev/null +++ b/cable/config.ru @@ -0,0 +1,6 @@ +# This file is used to start the Action Cable server. + +require_relative '../config/environment' +Rails.application.eager_load! + +run ActionCable.server diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc deleted file mode 100644 index e0ca5f2..0000000 --- a/config/credentials.yml.enc +++ /dev/null @@ -1 +0,0 @@ -ICsvsmBPFLSs6XiOL1JPCDv6MmaE0Qtw7dJg3vAT8iC+ps+6HYLSSSjJg8k9Cn2vu2hoIfRtNdTC8f9iXG2K4c272yxyxcFSvyj3+vPxwSca0Nkc9zdeGYFOupF6zg7NkgQ8VtZYSAzsIOWld8XvXkcnKDi22muYvZ1cGb8v/VBhkEgQmgAF80QsTHBRPOnw0dsGvbb2VKmBtnFLiRwiiKZ+rnEf+6WwPhD3GfYpYatUmjRZE/SrsJnmvRCFj/Yk6s5qRF/uPrcYdkavwrFae19xv305/oNT0EtjXQbrUyuRvM4/r+X3dcfl+WrbP8e+O1/xZ6j8bh+h/xy/VviL1lurZnEwZRynf3qIKIojZ4nIXvBe8s+TjhTn+SVGjx9woynSE9PcZbNwewmIi9mAe/vgTg77vTXV5nuI--QMY3UQqmdRoUIVZy--utWFei86XoDxCUlvNEhCdg== \ No newline at end of file diff --git a/config/database.yml b/config/database.yml index fcd380f..9501795 100644 --- a/config/database.yml +++ b/config/database.yml @@ -11,12 +11,12 @@ default: &default development: <<: *default - database: <%= ENV.fetch("POSTGRES_DB") { "baseapp_development" } %> + database: <%= ENV.fetch("POSTGRES_DB") { "baseapp" } %>_development test: <<: *default - database: <%= ENV.fetch("POSTGRES_DB") { "baseapp_test" } %> + database: <%= ENV.fetch("POSTGRES_DB") { "baseapp" } %>_test production: <<: *default - database: <%= ENV.fetch("POSTGRES_DB") { "baseapp_production" } %> + database: <%= ENV.fetch("POSTGRES_DB") { "baseapp" } %>_production diff --git a/config/environments/production.rb b/config/environments/production.rb index 02c8908..3bf4f9b 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 diff --git a/config/puma.rb b/config/puma.rb index daaf036..30e2d58 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -1,43 +1,53 @@ +# frozen_string_literal: true + +# Specify the bind host and environment. +bind "tcp://0.0.0.0:#{ENV.fetch('PORT', '3000')}" +environment ENV.fetch('RAILS_ENV', 'development') + # 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 } +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). -# -# workers ENV.fetch("WEB_CONCURRENCY") { 2 } +# processes). It defaults to the number of (virtual cores * 2). +workers ENV.fetch('WEB_CONCURRENCY', (Etc.nprocessors * 2).to_i) + +# 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' # 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! +preload_app! + +# If you are preloading your application and using Active Record, it's +# recommended that you close any connections to the database before workers +# are forked to prevent connection leakage. +before_fork do + ActiveRecord::Base.connection_pool.disconnect! if defined?(ActiveRecord) +end + +# the code in the `on_worker_boot` will be called if you are using +# clustered mode by specifying a number of `workers`. after each worker +# process is booted, this block will be run. if you are using the `preload_app!` +# option, you will want to use this block to reconnect to any threads +# or connections that may have been created at application boot, as ruby +# cannot share connections between processes. +on_worker_boot do + ActiveSupport.on_load(:active_record) do + ActiveRecord::Base.establish_connection + end +end # Allow puma to be restarted by `bin/rails restart` command. plugin :tmp_restart diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4f7b5ab --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,93 @@ +x-app: &default-app + build: + context: "." + target: "app" + args: + - "RAILS_ENV=production" + - "RUBY_VERSION=3.1.2" + - "NODE_VERSION=16.17.1-r0" + - "YARN_VERSION=1.22.19-r0" + env_file: + - ".env" + restart: "${DOCKER_RESTART_POLICY:-unless-stopped}" + stop_grace_period: "3s" + +services: + postgres: + deploy: + resources: + limits: + cpus: "${DOCKER_POSTGRES_CPUS:-0}" + memory: "${DOCKER_POSTGRES_MEMORY:-0}" + environment: + POSTGRES_USER: "${POSTGRES_USER}" + POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" + # POSTGRES_DB: "${POSTGRES_DB}" + image: "postgres:15.0-alpine" + restart: "${DOCKER_RESTART_POLICY:-unless-stopped}" + stop_grace_period: "3s" + volumes: + - "postgres:/var/lib/postgresql/data" + profiles: ["postgres"] + + redis: + deploy: + resources: + limits: + cpus: "${DOCKER_REDIS_CPUS:-0}" + memory: "${DOCKER_REDIS_MEMORY:-0}" + image: "redis:7.0.5-alpine" + restart: "${DOCKER_RESTART_POLICY:-unless-stopped}" + stop_grace_period: "3s" + volumes: + - "redis:/data" + profiles: ["redis"] + + web: + <<: *default-app + depends_on: + - "postgres" + - "redis" + deploy: + resources: + limits: + cpus: "${DOCKER_WEB_CPUS:-0}" + memory: "${DOCKER_WEB_MEMORY:-0}" + ports: + - "${DOCKER_WEB_PORT_FORWARD:-127.0.0.1:3000}:${PORT:-3000}" + profiles: ["web"] + tty: true + + worker: + <<: *default-app + depends_on: + - "postgres" + - "redis" + command: "bundle exec sidekiq -C config/sidekiq.yml" + entrypoint: [] + deploy: + resources: + limits: + cpus: "${DOCKER_WORKER_CPUS:-0}" + memory: "${DOCKER_WORKER_MEMORY:-0}" + profiles: ["worker"] + + cable: + <<: *default-app + depends_on: + - "postgres" + - "redis" + command: "puma -p 28080 cable/config.ru" + entrypoint: [] + deploy: + resources: + limits: + cpus: "${DOCKER_CABLE_CPUS:-0}" + memory: "${DOCKER_CABLE_MEMORY:-0}" + ports: + - "${DOCKER_CABLE_PORT_FORWARD:-127.0.0.1:28080}:28080" + profiles: ["cable"] + +volumes: + postgres: {} + redis: {} diff --git a/run b/run new file mode 100755 index 0000000..c08b641 --- /dev/null +++ b/run @@ -0,0 +1,151 @@ +#!/usr/bin/env bash + +set -o errexit +set -o pipefail +set -o nounset + +DC="${DC:-exec}" + +# If we're running in CI we need to disable TTY allocation for docker compose +# commands that enable it by default, such as exec and run. +TTY="" +if [[ ! -t 1 ]]; then + TTY="-T" +fi + +# ----------------------------------------------------------------------------- +# Helper functions start with _ and aren't listed in this script's help menu. +# ----------------------------------------------------------------------------- + +function _dc { + docker compose "${DC}" ${TTY} "${@}" +} + +function _build_run_down { + docker compose build + docker compose run ${TTY} "${@}" + docker compose down +} + +# ----------------------------------------------------------------------------- + +function cmd { + # Run any command you want in the web container + _dc web "${@}" +} + +function rails { + # We need to create the test packs before we run our tests. + if [ "${1-''}" == "test" ]; then + _dc -e "RAILS_ENV=test" vite rails assets:precompile + fi + + # Run tests + cmd rails "${@}" +} + +function shell { + # Start a shell session in the web container + cmd bash "${@}" +} + +function psql { + ## Connect to PostgreSQL with psql + # shellcheck disable=SC1091 + . .env + _dc postgres psql -U "${POSTGRES_USER}" "${@}" +} + +function redis-cli { + ## Connect to Redis with redis-cli + _dc redis redis-cli "${@}" +} + +function hadolint { + # Lint Dockerfile with hadolint + docker container run --rm -i \ + hadolint/hadolint hadolint --ignore DL3008 -t style "${@}" - < Dockerfile +} + +function bundle:install { + ## Install Ruby dependencies and write out a lock file + _build_run_down web bundle install +} + +function bundle:outdated { + ## List any installed gems that are outdated + cmd bundle outdated +} + +function bundle:update { + ## Update any installed gems that are outdated + cmd bundle update + bundle:install +} + +function yarn:install { + ## Install Yarn dependencies and write out a lock file + _build_run_down vite yarn install +} + +function yarn:outdated { + ## Install yarn dependencies and write lock file + _dc vite yarn outdated +} + +function clean { + ## Remove cache and other machine generates files + rm -rf node_modules/ public/assets public/vite* tmp/* .byebug_history +} + +function ci:install-deps { + # Install Continuous Integration (CI) dependencies + sudo apt-get install -y curl shellcheck + sudo curl \ + -L https://raw.githubusercontent.com/nickjj/wait-until/v0.1.2/wait-until \ + -o /usr/local/bin/wait-until && sudo chmod +x /usr/local/bin/wait-until +} + +function ci:test { + # Execute Continuous Integration (CI) pipeline + # + # It's expected that your CI environment has these tools available: + # - https://github.com/koalaman/shellcheck + # - https://github.com/nickjj/wait-until + shellcheck run bin/docker-entrypoint-web + hadolint "${@}" + + cp --no-clobber .env.example .env + + docker compose build + docker compose up -d + + # shellcheck disable=SC1091 + . .env + wait-until "docker compose exec -T \ + -e PGPASSWORD=${POSTGRES_PASSWORD} postgres \ + psql -U ${POSTGRES_USER} ${POSTGRES_USER} -c 'SELECT 1'" + + docker compose logs + + rails db:setup + + # Since we're running tests in CI without volumes and Rails needs the packs + # to exist when running tests, we need to run our tests from the webpacker + # container instead of the web container since the web container won't have + # the packs in it. + _dc -e "RAILS_ENV=test" vite rails assets:precompile + _dc vite rails test +} + +function help { + printf "%s [args]\n\nTasks:\n" "${0}" + + compgen -A function | grep -v "^_" | cat -n + + printf "\nExtended help:\n Each task has comments for general usage\n" +} + +# This idea is heavily inspired by: https://github.com/adriancooney/Taskfile +TIMEFORMAT=$'\nTask completed in %3lR' +time "${@:-help}"