Ruby on Whales

Public
Dockerizing Ruby on Rails environment
Used 2290 times
Created by
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 ""
Comments

Sign up or Login to leave a comment.