Copy and paste this code into your terminal
DISCLAIMER: You should always review templates before running them. By running the template, you are agreeing to the terms of use.
The contents of this script as show. Any updates will be reflected in the below code and the snippet.
say "👋 Welcome to interactive Ruby on Whales installer 🐳.\n" \
"Make sure you've read the guide: https://evilmartians.com/chronicles/ruby-on-whales-docker-for-ruby-rails-development"
DOCKER_DEV_ROOT = ".dockerdev"
# Prepare variables and utility files
# Collect the app's metadata
app_name = Rails.application.class.name.parameterize
# Load the project's deps and required Ruby version
ruby_version = nil
gemspecs = {}
begin
if File.file?("Gemfile.lock")
bundler_parser = Bundler::LockfileParser.new(Bundler.read_file("Gemfile.lock"))
gemspecs = Hash[bundler_parser.specs.map { |spec| [spec.name, spec.version] }]
maybe_ruby_version = bundler_parser.ruby_version.match(/ruby (\d+\.\d+\.\d+)./i)&.[](1)
end
begin
if maybe_ruby_version
ruby_version = ask("Which Ruby version would you like to use? (Press ENTER to use #{maybe_ruby_version})")
ruby_version = maybe_ruby_version if ruby_version.empty?
else
ruby_version = ask("Which Ruby version would you like to use? (For example, 3.1.0)")
end
Gem::Version.new(ruby_version)
rescue ArgumentError
say "Invalid version. Please, try again"
retry
end
end
# Generates the Aptfile with system deps
DEFAULT_APTFILE = <<~CODE
# An editor to work with credentials
vim
CODE
begin
deps = []
loop do
dep = ask "Which system package do you want to install? (Press ENTER to continue)"
break if dep.empty?
deps << dep
end
aptfile = File.join(DOCKER_DEV_ROOT, "Aptfile")
FileUtils.mkdir_p(File.dirname(aptfile))
app_deps =
if deps.empty?
"\n"
else
(["# Application dependencies"] + deps + [""]).join("\n")
end
File.write(aptfile, DEFAULT_APTFILE + app_deps)
end
# Set up database related variables, create files
database_adapter = nil
database_url = nil
begin
supported_adapters = %w(postgresql postgis postgres)
config_path = "config/database.yml"
if File.file?(config_path)
require "yaml"
maybe_database_adapter = begin
::YAML.load_file(config_path, aliases: true) || {}
rescue ArgumentError
::YAML.load_file(config_path) || {}
end.dig("development", "adapter")
end
selected_database_adapter =
if maybe_database_adapter
ask "Which database adapter do you use? (Press ENTER to use #{maybe_database_adapter})"
else
ask "Which database adapter do you use?"
end
selected_database_adapter = maybe_database_adapter if selected_database_adapter.empty?
if supported_adapters.include?(selected_database_adapter)
database_adapter = selected_database_adapter
else
say_status :warn, "Unfortunately, we do no support #{selected_database_adapter} yet. Please, configure it yourself"
end
end
# Specify PostgreSQL version
postgres_version = nil
postgres_base_image = "postgres"
DEFAULT_POSTGRES_VERSION = "14"
POSTGRES_ADAPTERS = %w[postgres postgresql postgis]
if POSTGRES_ADAPTERS.include?(database_adapter)
begin
selected_postgres_version = ask "Which PostgreSQL version do you want to install? (Press ENTER to use #{DEFAULT_POSTGRES_VERSION})"
postgres_version = selected_postgres_version.empty? ? DEFAULT_POSTGRES_VERSION : selected_postgres_version
database_url = "#{database_adapter}://postgres:postgres@postgres:5432"
if database_adapter == "postgis"
postgres_base_image = "postgis/postgis"
end
end
end
# Node/Yarn configuration
# TODO: Read Node/Yarn versions from .nvmrc/package.json.
node_version = nil
yarn_version = nil
DEFAULT_NODE_VERSION = "16"
begin
selected_node_version = ask(
"Which Node version do you want to install? (Press ENTER to use 16, type 'n/no' to skip installing Node)"
)
unless selected_node_version =~ /^\s*no?\s*$/
node_version = selected_node_version.empty? ? DEFAULT_NODE_VERSION : selected_node_version
yarn_version = ask "Which Yarn version do you want to install? (Press ENTER to install the latest one)"
yarn_version = "latest" if yarn_version.empty?
end
end
# Redis info
redis_version = nil
DEFAULT_REDIS_VERSION = "6.0"
begin
if gemspecs.key?("redis")
maybe_redis_version = ask "Which Redis version do you want to install? (Press ENTER to use #{DEFAULT_REDIS_VERSION})"
redis_version = maybe_redis_version.empty? ? DEFAULT_REDIS_VERSION : maybe_redis_version
end
end
# Generate configuration
file "#{DOCKER_DEV_ROOT}/Dockerfile", ERB.new(
*[
<<~'CODE'
ARG RUBY_VERSION
ARG DISTRO_NAME=bullseye
FROM ruby:$RUBY_VERSION-slim-$DISTRO_NAME
ARG DISTRO_NAME
# Common dependencies
RUN apt-get update -qq \
&& DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
build-essential \
gnupg2 \
curl \
less \
git \
&& apt-get clean \
&& rm -rf /var/cache/apt/archives/* \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
&& truncate -s 0 /var/log/*log
<% if postgres_version %>
ARG PG_MAJOR
RUN curl -sSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - \
&& echo deb http://apt.postgresql.org/pub/repos/apt/ $DISTRO_NAME-pgdg main $PG_MAJOR > /etc/apt/sources.list.d/pgdg.list
RUN apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get -yq dist-upgrade && \
DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
libpq-dev \
postgresql-client-$PG_MAJOR \
&& apt-get clean \
&& rm -rf /var/cache/apt/archives/* \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
&& truncate -s 0 /var/log/*log
<% end %>
<% if node_version %>
ARG NODE_MAJOR
RUN curl -sL https://deb.nodesource.com/setup_$NODE_MAJOR.x | bash -
RUN apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get -yq dist-upgrade && \
DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
nodejs \
&& apt-get clean \
&& rm -rf /var/cache/apt/archives/* \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
&& truncate -s 0 /var/log/*log
<% if yarn_version == 'latest' %>
RUN npm install -g yarn
<% else %>
ARG YARN_VERSION
RUN npm install -g yarn@$YARN_VERSION
<% end %>
<% end %>
# Application dependencies
# We use an external Aptfile for this, stay tuned
COPY Aptfile /tmp/Aptfile
RUN apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get -yq dist-upgrade && \
DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
$(grep -Ev '^\s*#' /tmp/Aptfile | xargs) \
&& apt-get clean \
&& rm -rf /var/cache/apt/archives/* \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
&& truncate -s 0 /var/log/*log
# Configure bundler
ENV LANG=C.UTF-8 \
BUNDLE_JOBS=4 \
BUNDLE_RETRY=3
# Store Bundler settings in the project's root
ENV BUNDLE_APP_CONFIG=.bundle
# Uncomment this line if you want to run binstubs without prefixing with `bin/` or `bundle exec`
# ENV PATH /app/bin:$PATH
# Upgrade RubyGems and install the latest Bundler version
RUN gem update --system && \
gem install bundler
# Create a directory for the app code
RUN mkdir -p /app
WORKDIR /app
# Document that we're going to expose port 3000
EXPOSE 3000
# Use Bash as the default command
CMD ["/usr/bin/bash"]
CODE
], trim_mode: "<>").result(binding)
file "#{DOCKER_DEV_ROOT}/compose.yml", ERB.new(
*[
<<~'CODE'
x-app: &app
build:
context: .
args:
RUBY_VERSION: '<%= ruby_version %>'
<% if postgres_version %>
PG_MAJOR: '<%= postgres_version.split('.', 2).first %>'
<% end %>
<% if node_version %>
NODE_MAJOR: '<%= node_version.split('.', 2).first %>'
<% end %>
<% if yarn_version && yarn_version != 'latest' %>
YARN_VERSION: '<%= yarn_version %>'
<% end %>
image: <%= app_name %>-dev:1.0.0
environment: &env
<% if node_version %>
NODE_ENV: ${NODE_ENV:-development}
<% end %>
RAILS_ENV: ${RAILS_ENV:-development}
tmpfs:
- /tmp
- /app/tmp/pids
x-backend: &backend
<<: *app
stdin_open: true
tty: true
volumes:
- ..:/app:cached
- bundle:/usr/local/bundle
- rails_cache:/app/tmp/cache
<% if File.directory?("app/assets") %>
- assets:/app/public/assets
<% end %>
<% if node_version %>
- node_modules:/app/node_modules
<% end %>
<% if gemspecs.key?("webpacker") %>
- packs:/app/public/packs
- packs-test:/app/public/packs-test
<% end %>
- history:/usr/local/hist
<% if postgres_version %>
- ./.psqlrc:/root/.psqlrc:ro
<% end %>
- ./.bashrc:/root/.bashrc:ro
environment: &backend_environment
<<: *env
<% if redis_version %>
REDIS_URL: redis://redis:6379/
<% end %>
<% if database_url %>
DATABASE_URL: postgres://postgres:postgres@postgres:5432
<% end %>
<% if gemspecs.key?("webpacker") %>
WEBPACKER_DEV_SERVER_HOST: webpacker
<% end %>
MALLOC_ARENA_MAX: 2
WEB_CONCURRENCY: ${WEB_CONCURRENCY:-1}
BOOTSNAP_CACHE_DIR: /usr/local/bundle/_bootsnap
XDG_DATA_HOME: /app/tmp/caches
<% if yarn_version %>
YARN_CACHE_FOLDER: /app/node_modules/.yarn-cache
<% end %>
HISTFILE: /usr/local/hist/.bash_history
<% if postgres_version %>
PSQL_HISTFILE: /usr/local/hist/.psql_history
<% end %>
IRB_HISTFILE: /usr/local/hst/.irb_history
EDITOR: vi
<% if postgres_version || redis_version %>
depends_on:
<% if postgres_version %>
postgres:
condition: service_healthy
<% end %>
<% if redis_version %>
redis:
condition: service_healthy
<% end %>
<% end %>
services:
rails:
<<: *backend
command: bundle exec rails
web:
<<: *backend
command: bundle exec rails server -b 0.0.0.0
ports:
- '3000:3000'
<% if gemspecs.key?("webpacker") || gemspecs.key?("sidekiq") %>
depends_on:
<% if gemspecs.key?("webpacker") %>
webpacker:
condition: service_started
<% end %>
<% if gemspecs.key?("sidekiq") %>
sidekiq:
condition: service_started
<% end %>
<% end %>
<% if gemspecs.key?("sidekiq") %>
sidekiq:
<<: *backend
command: bundle exec sidekiq
<% end %>
<% if postgres_version %>
postgres:
image: postgres:<%= postgres_version %>
volumes:
- .psqlrc:/root/.psqlrc:ro
- postgres:/var/lib/postgresql/data
- history:/user/local/hist
environment:
PSQL_HISTFILE: /user/local/hist/.psql_history
POSTGRES_PASSWORD: postgres
ports:
- 5432
healthcheck:
test: pg_isready -U postgres -h 127.0.0.1
interval: 5s
<% end %>
<% if redis_version %>
redis:
image: redis:<%= redis_version %>-alpine
volumes:
- redis:/data
ports:
- 6379
healthcheck:
test: redis-cli ping
interval: 1s
timeout: 3s
retries: 30
<% end %>
<% if gemspecs.key?("webpacker") %>
webpacker:
<<: *app
command: bundle exec ./bin/webpack-dev-server
ports:
- '3035:3035'
volumes:
- ..:/app:cached
- bundle:/usr/local/bundle
- node_modules:/app/node_modules
- packs:/app/public/packs
- packs-test:/app/public/packs-test
environment:
<<: *env
WEBPACKER_DEV_SERVER_HOST: 0.0.0.0
YARN_CACHE_FOLDER: /app/node_modules/.yarn-cache
<% end %>
volumes:
bundle:
<% if node_version %>
node_modules:
<% end %>
history:
rails_cache:
<% if postgres_version %>
postgres:
<% end %>
<% if redis_version %>
redis:
<% end %>
<% if File.directory?("app/assets") %>
assets:
<% end %>
<% if gemspecs.key?("webpacker") %>
packs:
packs-test:
<% end %>
CODE
], trim_mode: "<>").result(binding)
file "dip.yml", ERB.new(
*[
<<~'CODE'
version: '7.1'
# Define default environment variables to pass
# to Docker Compose
environment:
RAILS_ENV: development
compose:
files:
- <%= DOCKER_DEV_ROOT %>/compose.yml
project_name: <%= app_name %>
interaction:
# This command spins up a Rails container with the requried dependencies (such as databases),
# and opens a terminal within it.
runner:
description: Open a Bash shell within a Rails container (with dependencies up)
service: rails
command: /bin/bash
# Run a Rails container without any dependent services (useful for non-Rails scripts)
bash:
description: Run an arbitrary script within a container (or open a shell without deps)
service: rails
command: /bin/bash
compose_run_options: [ no-deps ]
# A shortcut to run Bundler commands
bundle:
description: Run Bundler commands
service: rails
command: bundle
compose_run_options: [ no-deps ]
<% if gemspecs.key?("rspec") %>
# A shortcut to run RSpec (which overrides the RAILS_ENV)
rspec:
description: Run Rspec commands
service: rails
environment:
RAILS_ENV: test
command: bundle exec rspec
<% end %>
rails:
description: Run Rails commands
service: rails
command: bundle exec rails
subcommands:
s:
description: Run Rails server at http://localhost:3000
service: web
compose:
run_options: [ service-ports, use-aliases ]
<% if !gemspecs.key?("rspec") %>
test:
description: Run unit tests
service: rails
command: bundle exec rails test
environment:
RAILS_ENV: test
<% end %>
<% if yarn_version %>
yarn:
description: Run Yarn commands
service: rails
command: yarn
compose_run_options: [ no-deps ]
<% end %>
<% if postgres_version %>
psql:
description: Run Postgres psql console
service: postgres
default_args: anycasts_dev
command: psql -h postgres -U postgres
<% end %>
<% if redis_version %>
'redis-cli':
description: Run Redis console
service: redis
command: redis-cli -h redis
<% end %>
provision:
- dip compose down --volumes
<% if postgres_version %>
- dip compose up -d postgres
<% end %>
<% if redis_version %>
- dip compose up -d redis
<% end %>
- dip bash -c bin/setup
CODE
], trim_mode: "<>").result(binding)
file "#{DOCKER_DEV_ROOT}/.bashrc", ERB.new(
*[
<<~'CODE'
alias be="bundle exec"
CODE
], trim_mode: "<>").result(binding)
if postgres_version
file "#{DOCKER_DEV_ROOT}/.psqlrc", ERB.new(
*[
<<~'CODE'
-- Don't display the "helpful" message on startup.
\set QUIET 1
-- Allow specifying the path to history file via `PSQL_HISTFILE` env variable
-- (and fallback to the default $HOME/.psql_history otherwise)
\set HISTFILE `[ -z $PSQL_HISTFILE ] && echo $HOME/.psql_history || echo $PSQL_HISTFILE`
-- Show how long each query takes to execute
\timing
-- Use best available output format
\x auto
-- Verbose error reports
\set VERBOSITY verbose
-- If a command is run more than once in a row,
-- only store it once in the history
\set HISTCONTROL ignoredups
\set COMP_KEYWORD_CASE upper
-- By default, NULL displays as an empty space. Is it actually an empty
-- string, or is it null? This makes that distinction visible
\pset null '[NULL]'
\unset QUIET
CODE
], trim_mode: "<>").result(binding)
end
file "#{DOCKER_DEV_ROOT}/README.md", ERB.new(
*[
<<~'CODE'
# Docker for Development
Source: [Ruby on Whales: Dockerizing Ruby and Rails development](https://evilmartians.com/chronicles/ruby-on-whales-docker-for-ruby-rails-development).
## Installation
- Docker installed.
For MacOS just use the [official app](https://docs.docker.com/engine/installation/mac/).
- [`dip`](https://github.com/bibendi/dip) installed.
You can install `dip` as Ruby gem:
```sh
gem install dip
```
## Provisioning
When using Dip it could be done with a single command:
```sh
dip provision
```
## Running
```sh
dip rails s
```
## Developing with Dip
### Useful commands
```sh
# run rails console
dip rails c
# run rails server with debugging capabilities (i.e., `debugger` would work)
dip rails s
# or run the while web app (with all the dependencies)
dip up web
# run migrations
dip rails db:migrate
# pass env variables into application
dip VERSION=20100905201547 rails db:migrate:down
# simply launch bash within app directory (with dependencies up)
dip runner
# execute an arbitrary command via Bash
dip bash ls -al tmp/cache
# Additional commands
# update gems or packages
dip bundle install
<% if yarn_version %>
dip yarn install
<% end %>
<% if postgres_version %>
# run psql console
dip psql
<% end %>
<% if redis_version %>
# run Redis console
dip redis-cli
<% end %>
# run tests
<% if gemspecs.key?("rspec") %>
# TIP: `dip rspec` is already auto prefixed with `RAILS_ENV=test`
dip rspec spec/path/to/single/test.rb:23
<% else %>
# TIP: `dip rails test` is already auto prefixed with `RAILS_ENV=test`
dip rails test
<% end %>
# shutdown all containers
dip down
```
### Development flow
Another way is to run `dip <smth>` for every interaction. If you prefer this way and use ZSH, you can reduce the typing
by integrating `dip` into your session:
```sh
$ dip console | source /dev/stdin
# no `dip` prefix is required anymore!
$ rails c
Loading development environment (Rails 7.0.1)
pry>
```
CODE
], trim_mode: "<>").result(binding)
todos = [
"📝 Important things to take care of:",
" - Make sure you have `ENV[\"RAILS_ENV\"] = \"test\"` (not `ENV[\"RAILS_ENV\"] ||= \"test\"`) in your test helper."
]
if database_url
todos << " - Don't forget to add `url: ENV[\"DATABASE_URL\"]` to your database.yml"
end
if todos.any?
say_status(:warn, todos.join("\n"))
end
say_status :info, "✅ You're ready to sail! Check out #{DOCKER_DEV_ROOT}/README.md or run `dip provision && dip up web` 🚀"
A place where you can thank the author, post problems, give constructive feedback, etc. Be nice!