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 @@
+
# An example Rails 7 app
[](https://github.com/zakariaf/rails-base-app/blob/main/Gemfile.lock) [](https://github.com/zakariaf/rails-base-app/blob/main/.ruby-version) [](https://github.com/zakariaf/rails-base-app/blob/main/package.json) [](https://github.com/zakariaf/rails-base-app/blob/main/package.json) [](https://github.com/zakariaf/rails-base-app/blob/main/package.json) [](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

-## 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}"