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:[email protected]: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 [email protected]$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:[email protected]: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!