Ruby on Whales
Dockerizing Ruby on Rails environment
Used 2290 times
V
Vladimir Dementyev
Usage
This template configure a Dockerized development environment for Rails applications following the "Ruby on Whales" article.
Run this command in your Rails app directory in the terminal:
rails app:template LOCATION="https://railsbytes.com/script/z5OsoB"
Template Source
Review the code before running this template on your machine.
say "👋 Welcome to interactive Ruby on Whales installer.", :cyan
say "We'll help you to configure a Docker development environment for your Rails project."
say "\n"
say "Read the full guide here:"
say "https://evilmartians.com/chronicles/ruby-on-whales-docker-for-ruby-rails-development", :blue
say ""
DOCKER_DEV_ROOT = ".dockerdev"
# Prepare variables and utility files
# Collect the app's metadata
app_name = Rails.application.class.name.partition('::').first.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
if File.file?(".ruby-version")
maybe_ruby_version = File.read(".ruby-version").strip
end
begin
if maybe_ruby_version
ruby_version = ask("Which Ruby version would you like to use?", default: 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.2.0)") || ""
end
Gem::Version.new(ruby_version)
rescue ArgumentError
say "Invalid version. Please, try again"
retry
end
end
say_status :info, "Ruby: #{ruby_version}"
# Set up database related variables, create files
database_adapter = nil
database_url = nil
begin
supported_adapters = %w(postgresql postgis postgres mysql2 trilogy sqlite3 sqlite)
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.then do |conf|
next unless conf.is_a?(Hash)
conf.dig("development", "adapter")
end
end
# check gems if database.yml is non-standard
unless maybe_database_adapter
maybe_database_adapter =
case
when gemspecs.include?("sqlite3") then "sqlite3"
when gemspecs.include?("pg") then "postgresql"
when gemspecs.include?("trilogy") then "trilogy"
when gemspecs.include?("mysql2") then "mysql2"
end
end
selected_database_adapter = ask "Which database adapter do you use?", default: maybe_database_adapter
selected_database_adapter = maybe_database_adapter if selected_database_adapter.nil? || selected_database_adapter.empty?
if supported_adapters.include?(selected_database_adapter)
database_adapter = selected_database_adapter
say_status :info, "Database: #{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 = "17"
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?", default: DEFAULT_POSTGRES_VERSION
postgres_version = selected_postgres_version.empty? ? DEFAULT_POSTGRES_VERSION : selected_postgres_version
say_status :info, "PostgreSQL: #{postgres_version}"
database_url = "#{database_adapter}://postgres:postgres@postgres:5432"
if database_adapter == "postgis"
postgres_base_image = "postgis/postgis"
end
end
end
# Specify PostgreSQL version
mysql_version = nil
mysql_image = "mysql"
DEFAULT_MYSQL_VERSION = "8.0"
MYSQL_ADAPTERS = %w[mysql2 trilogy]
if MYSQL_ADAPTERS.include?(database_adapter)
begin
selected_mysql_version = ask "Which MySQL version do you want to install?", default: DEFAULT_MYSQL_VERSION
mysql_version = selected_mysql_version.empty? ? DEFAULT_MYSQL_VERSION : selected_mysql_version
say_status :info, "MySQL: #{mysql_version}"
database_url = "#{database_adapter}://root:root@mysql:3306"
end
end
# Node/Yarn configuration
# TODO: Read Node/Yarn versions from .nvmrc/package.json.
node_version = nil
yarn_version = nil
DEFAULT_NODE_VERSION = "22"
begin
if yes?("Do you need Node.js?")
node_version = ask(
"Which Node version do you want to install?",
default: DEFAULT_NODE_VERSION
) || ""
say_status :info, "Node: #{node_version}"
if File.file?("yarn.lock")
yarn_version = ask("Which Yarn version do you want to install?", default: "latest") || ""
say_status :info, "Yarn: #{node_version}"
end
end
end
# Redis info
redis_version = nil
DEFAULT_REDIS_VERSION = "7.4"
begin
if gemspecs.key?("redis")
maybe_redis_version = ask "Which Redis version do you want to use?", default: DEFAULT_REDIS_VERSION
redis_version = maybe_redis_version.empty? ? DEFAULT_REDIS_VERSION : maybe_redis_version
say_status :info, "Redis: #{redis_version}"
end
end
# Generates the Aptfile with system deps
apt_deps = [
"# An editor to work with credentials",
"vim"
]
begin
deps = []
if %[sqlite sqlite3].include?(database_adapter)
deps << "sqlite3"
end
if gemspecs.key?("ruby-vips")
deps << "libvips-dev"
end
if gemspecs.key?("psych")
deps << "libyaml-dev"
end
say "Here is the list of system packages we're going to install:", :blue
print_in_columns deps
more_deps = ask("Would you like to install other packages? Type comma-separated names or press ENTER to continue") || ""
user_deps = more_deps.split(/\s*,\s*/).map(&:strip)
unless user_deps.empty?
say "Additional packages:", :blue
print_in_columns user_deps
deps.concat(user_deps)
end
if !deps.empty?
apt_deps.concat(["# Application dependencies"] + deps)
end
end
claude = yes?("Would you like to install Claude Code CLI inside a container and run it from there?")
if claude
apt_deps.concat([
"# Claude",
"bubblewrap",
"socat"
])
end
# Generate configuration
aptfile = File.join(DOCKER_DEV_ROOT, "Aptfile")
FileUtils.mkdir_p(File.dirname(aptfile))
File.write(aptfile, apt_deps.join("\n") + "\n")
file "#{DOCKER_DEV_ROOT}/Dockerfile", ERB.new(
*[
<<~'TCODE'
# syntax=docker/dockerfile:1
ARG RUBY_VERSION
ARG DISTRO_NAME=bookworm
FROM ruby:$RUBY_VERSION-slim-$DISTRO_NAME
ARG DISTRO_NAME
# Common dependencies
# Using --mount to speed up build with caching, see https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/reference.md#run---mount
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
--mount=type=tmpfs,target=/var/log \
rm -f /etc/apt/apt.conf.d/docker-clean; \
echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache; \
apt-get update -qq && \
DEBIAN_FRONTEND=noninteractive apt-get -yq dist-upgrade && \
DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
build-essential \
gnupg2 \
curl \
less \
git
<% if postgres_version %>
ARG PG_MAJOR
RUN curl -sSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor -o /usr/share/keyrings/postgres-archive-keyring.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/postgres-archive-keyring.gpg] https://apt.postgresql.org/pub/repos/apt/" \
$DISTRO_NAME-pgdg main $PG_MAJOR | tee /etc/apt/sources.list.d/postgres.list > /dev/null
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
--mount=type=tmpfs,target=/var/log \
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
<% end %>
<% if node_version %>
ARG NODE_MAJOR
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
--mount=type=tmpfs,target=/var/log \
curl -sL https://deb.nodesource.com/setup_$NODE_MAJOR.x | bash - && \
DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
nodejs
<% if yarn_version == 'latest' %>
RUN npm install -g yarn
<% elsif yarn_version %>
ARG YARN_VERSION
RUN npm install -g yarn@$YARN_VERSION
<% end %>
<% end %>
# Application dependencies
# We use an external Aptfile for this, stay tuned
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
--mount=type=tmpfs,target=/var/log \
--mount=type=bind,source=Aptfile,target=/tmp/Aptfile \
DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
$(grep -Ev '^\s*#' /tmp/Aptfile | xargs)
<% if claude %>
# Install Claude CLI
RUN curl -fsSL https://claude.ai/install.sh | bash
ENV PATH /root/.local/bin:$PATH
ENV IS_SANDBOX 1
<% end %>
# 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 ["/bin/bash"]
TCODE
], trim_mode: "<>").result(binding)
file "#{DOCKER_DEV_ROOT}/compose.yml", ERB.new(
*[
<<~'TCODE'
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
working_dir: ${PWD}
environment: &env
<% if node_version %>
NODE_ENV: ${NODE_ENV:-development}
<% end %>
RAILS_ENV: ${RAILS_ENV:-}
tmpfs:
- /tmp
- ${PWD}/tmp/pids
x-backend: &backend
<<: *app
stdin_open: true
tty: true
volumes:
- ${PWD}:/${PWD}:cached
- bundle:/usr/local/bundle
- rails_cache:/${PWD}/tmp/cache
<% if File.directory?("app/assets") %>
- assets:/${PWD}/public/assets
<% end %>
<% if node_version %>
- node_modules:/${PWD}/node_modules
<% end %>
<% if gemspecs.key?("webpacker") %>
- packs:/${PWD}/public/packs
- packs-test:/${PWD}/public/packs-test
<% end %>
<% if gemspecs.key?("vite_ruby") %>
- vite_dev:/${PWD}/public/vite-dev
- vite_test:/${PWD}/public/vite-test
<% end %>
- history:/usr/local/hist
<% if claude %>
- claude:/root/.claude
- ./.claude.json:/root/.claude.json
<% end %>
<% 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: <%= database_url %>
<% 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: /${PWD}/tmp/cache
<% if yarn_version %>
YARN_CACHE_FOLDER: /${PWD}/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/hist/.irb_history
EDITOR: vi
<% if claude %>
CLAUDE_CODE_TMPDIR: /root/.claude/__tmp__
<% end %>
<% if postgres_version || redis_version %>
depends_on:
<% if postgres_version %>
postgres:
condition: service_healthy
<% end %>
<% if mysql_version %>
mysql:
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 %>
<% if gemspecs.key?("vite_ruby") %>
# Uncomment these lines to always run Vite dev server
# vite:
# 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:/usr/local/hist
environment:
PSQL_HISTFILE: /usr/local/hist/.psql_history
POSTGRES_PASSWORD: postgres
ports:
- 5432
healthcheck:
test: pg_isready -U postgres -h 127.0.0.1
interval: 5s
<% end %>
<% if mysql_version %>
mysql:
image: mysql:<%= mysql_version %>
volumes:
- mysql_data:/var/lib/mysql
- history:/usr/local/hist
command: --default-authentication-plugin=mysql_native_password
ports:
- 3306
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: 1
healthcheck:
test: mysqladmin ping -h 127.0.0.1 -u root
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:
- ${PWD}:/${PWD}:cached
- bundle:/usr/local/bundle
- node_modules:/${PWD}/node_modules
- packs:/${PWD}/public/packs
- packs-test:/${PWD}/public/packs-test
environment:
<<: *env
WEBPACKER_DEV_SERVER_HOST: 0.0.0.0
YARN_CACHE_FOLDER: /app/node_modules/.yarn-cache
<% end %>
<% if gemspecs.key?("vite_ruby") %>
vite:
<<: *backend
command: ./bin/vite dev
volumes:
- ${PWD}:/${PWD}:cached
- bundle:/usr/local/bundle
- node_modules:/${PWD}/node_modules
- vite_dev:/${PWD}/public/vite-dev
- vite_test:/${PWD}/public/vite-test
environment:
<<: *backend_environment
VITE_RUBY_HOST: 0.0.0.0 # bind to container
ports:
- "3036:3036"
<% end %>
volumes:
bundle:
<% if node_version %>
node_modules:
<% end %>
history:
rails_cache:
<% if postgres_version %>
postgres:
<% end %>
<% if mysql_version %>
mysql_data:
<% end %>
<% if redis_version %>
redis:
<% end %>
<% if File.directory?("app/assets") %>
assets:
<% end %>
<% if gemspecs.key?("webpacker") %>
packs:
packs-test:
<% end %>
<% if gemspecs.key?("vite_ruby") %>
vite_dev:
vite_test:
<% end %>
<% if claude %>
claude:
<% end %>
TCODE
], trim_mode: "<>").result(binding)
file "dip.yml", ERB.new(
*[
<<~'TCODE'
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 required 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 gemspecs.key?("ruby-lsp") %>
ruby-lsp:
description: Run Ruby LSP
service: rails
command: bundle exec ruby-lsp
compose_run_options: [ service-ports, no-deps ]
<% end %>
<% if gemspecs.key?("solargraph") %>
solargraph:
description: Run Solargraph
service: rails
command: bundle exec solargraph
compose_run_options: [ service-ports, no-deps ]
<% 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: <%= app_name %>_development
command: psql -h postgres -U postgres
<% end %>
<% if redis_version %>
'redis-cli':
description: Run Redis console
service: redis
command: redis-cli -h redis
<% end %>
<% if claude %>
claude:
description: Run Claude CLI
service: rails
command: claude --dangerously-skip-permissions
<% end %>
<% if gemspecs.include?("sidekiq-pro") %>
configure_bundler_credentials:
command: |
(test -f .bundle/config && cat .bundle/config | \
grep BUNDLE_ENTERPRISE__CONTRIBSYS__COM > /dev/null) ||
\
(echo "Sidekiq ent credentials ("user:pass"): "; read -r creds; dip bundle config --local enterprise.contribsys.com $creds)
<% end %>
provision:
- '[[ "$RESET_DOCKER" == "true" ]] && echo "Re-creating the Docker env from scratch..." && dip compose down --volumes || echo "Re-provisioning the Docker env..."'
<% if gemspecs.include?("sidekiq-pro") %>
- dip configure_bundler_credentials
<% end %>
<% if claude %>
- (test -f .dockerdev/.claude.json) || (cp .dockerdev/.claude.json.example .dockerdev/.claude.json)
<% end %>
<% if postgres_version %>
- dip compose up -d postgres
<% end %>
<% if redis_version %>
- dip compose up -d redis
<% end %>
- dip bundle check || dip bundle install
- dip rails db:prepare
- dip rails db:test:prepare
<% if yarn_version %>
- dip yarn
<% end %>
<% if gemspecs.include?("cssbundling-rails") %>
- dip yarn build:css
<% end %>
- echo "🚀 Ready to rock! Run 'dip rails s' to start a Rails web server or 'dip <% gemspecs.key?("rspec") ? "rspec" : "rails test" %>' to run tests."
TCODE
], trim_mode: "<>").result(binding)
file "#{DOCKER_DEV_ROOT}/.bashrc", ERB.new(
*[
<<~'TCODE'
alias be="bundle exec"
export PROMPT_DIRTRIM=2
export PS1='[🐳 \[\e[36m\]\w\[\e[0m\]] '
TCODE
], trim_mode: "<>").result(binding)
if postgres_version
file "#{DOCKER_DEV_ROOT}/.psqlrc", ERB.new(
*[
<<~'TCODE'
-- 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
TCODE
], trim_mode: "<>").result(binding)
end
if claude
file "#{DOCKER_DEV_ROOT}/.claude.json.example", "{}"
file "#{DOCKER_DEV_ROOT}/.gitignore", "/.claude.json\n"
end
file "#{DOCKER_DEV_ROOT}/README.md", ERB.new(
*[
<<~'TCODE'
# 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
<% if claude %>
### Using Claude
You run Claude within Docker as follows:
```sh
dip claude
```
You will need to authenticate it on the first launch.
Note that it runs with `--dangerously-skip-permissions` by default. The sessions, plugins, skills, etc persist between runs (stored in a volume).
<% end %>
### 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 -c '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>
```
TCODE
], trim_mode: "<>").result(binding)
todos = []
if database_url
todos << " - Don't forget to add `url: \<\%= ENV[\"DATABASE_URL\"] \%\>` to your database.yml"
end
if todos.any?
say_status(:warn, "📝 Important things to take care of:")
print_wrapped todos.join("\n")
end
# Check if Claude CLI is available and offer to run it for polishing the configuration
claude_available = system("which claude > /dev/null 2>&1")
if claude_available
if yes?("Would you like to run Claude to review and polish your Docker configuration?")
# Build the prompt with todos
todos_text = todos.map { |t| t.sub(/^\s*-?\s*/, "- ") }.join("\n")
prompt = <<~PROMPT
I just set up a Docker development environment for this Rails application using the "Ruby on Whales" template.
## Generated files
The following files were created in `#{DOCKER_DEV_ROOT}/`:
- `Dockerfile` - Multi-stage Docker image for development
- `compose.yml` - Docker Compose configuration
- `Aptfile` - System dependencies
- `README.md` - Usage documentation
And `dip.yml` in the project root for the Dip CLI.
## TODOs
#{todos_text}
## Your tasks
1. **Polish the generated configuration** — review the files in `#{DOCKER_DEV_ROOT}/` and `dip.yml` against the project's actual structure (check database.yml, Gemfile, etc.) and fix any issues directly.
2. **Complete the mandatory TODOs** listed above (e.g., add DATABASE_URL to database.yml). Apply fixes directly without asking. Do not ask questions.
3. **Create a `TODO.md` file** in `#{DOCKER_DEV_ROOT}/` listing optional next steps the user may want to configure later (system tests, Vite, CI, etc.).
Exit the session when done (so the installer can get control back). Do not provide lengthy explanations.
## References
For more context, see these guides:
- [Ruby on Whales: Dockerizing Ruby and Rails development](https://evilmartians.com/chronicles/ruby-on-whales-docker-for-ruby-rails-development) - The main guide this template is based on
- [System of a test: Dockerizing system tests](https://evilmartians.com/chronicles/system-of-a-test-setting-up-end-to-end-rails-testing#dockerizing-system-tests) - For setting up system/integration tests with Docker
- [Vite-lizing Rails: Dockerizing Vite](https://evilmartians.com/chronicles/vite-lizing-rails-get-live-reload-and-hot-replacement-with-vite-ruby#dockerizing-vite-or-not) - For Vite.js integration with Docker
PROMPT
say_status :info, "Handing over to Claude to review your Docker setup...\n"
say ""
claude_cmd = ["claude", "--allowedTools", "Read,Edit,Write,Glob,Grep,Bash(git:*)", "-p", prompt]
output = if defined?(Gum) && ENV["RBYTES_DISABLE_GUM"] != "1"
Gum.spin("Claude is thinking...") do
IO.popen(claude_cmd, &:read)
end
else
IO.popen(claude_cmd, &:read)
end
say "Here is what Claude said", :blue
print_wrapped output
say "\n"
say ""
end
end
say "✅ You're ready to sail!", :cyan
say "\n"
say "Check out #{DOCKER_DEV_ROOT}/README.md or run `dip provision && dip up web` 🚀"
say ""