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!