Set Up a Rails Development Environment with Docker + VS Code Dev Containers

I recently worked on rewriting a super old Rails app (Rails 2 + Ruby 1.8.7). We felt a bit burned by the fact that we couldn’t even install such an old version of Ruby to run the old app. So, we decided to start fresh and prioritize setting up a dockerized development environment. Here, I’ll walk through the steps we followed to set up a pretty nice-to-work-with Rails development environment using Docker and VS Code Dev Containers.

1. Adding an Initial Dockerfile + Compose File

For this example, let’s say that we are working in the shiny-project repo.

This is the initial Dockerfile we create. Since we are creating a new Rails project, all we have to do at this point is specify that we want Ruby for our base image.


FROM ruby:3.3.0-bookworm

RUN mkdir -p /shiny-project
WORKDIR /shiny-project

EXPOSE 3000

And here is the compose.yml file. We are mounting the entire shiny-project directory to sync any changes to the repo between our host machine and the app container. Your database service + volume will also be defined in this same compose file.


version: '3'
services:
  app:
    build:
      context: '.'
      dockerfile: 'docker/Dockerfile'
    stdin_open: true
    tty: true
    entrypoint: /shiny-project/docker/script/entry.sh
    volumes:
      - .:/shiny-project
    ports:
      - '3000:3000'

Here are the contents of the entry.sh script called out as the app service’s entrypoint. shiny-app is what we will eventually call the rails app we create. This script just keeps the container alive indefinitely, listening for any commands to run, and able to respond to any kill signals we send.


#!/bin/bash

mkdir -p /shiny-app/tmp
mount -t tmpfs tmpfs /shiny-app/tmp

_term() {
  echo "Caught term, send kill"
  kill -SIGTERM "$child" 2>/dev/null
}

_int() {
  echo "Caught int, send kill"
  kill -SIGINT "$child" 2>/dev/null
}

trap _term TERM
trap _int INT

echo 'APP READY!'

sleep infinity &
child=$!

wait $child
exit 0

Now, we can run docker-compose up -d to start our services.

2. Creating the Rails App

We need to exec into the app container to create our new Rails app,

To install rails in the container:


docker-compose exec app gem install rails

To generate a new Rails app:


docker-compose exec app rails new shiny-app

3. Improving the Dockerfile

Now that we actually have a Rails app generated, we can take another pass at improving the Dockerfile. We set the working directory to that of the Rails app to make running Rails CLI commands easier. And copy the Gemfile and Gemfile.lock into the image so that we don’t have to install Gems manually.


FROM ruby:3.3.0-bookworm

RUN mkdir -p /shiny-project/shiny-app

WORKDIR /shiny-project/shiny-app

ADD ./shiny-app/Gemfile /shiny-project/shiny-app/Gemfile
ADD ./shiny-app/Gemfile.lock /shiny-project/shiny-app/Gemfile.lock

RUN gem install bundler:2.5.3
RUN bundle install

EXPOSE 3000

4. Adding Convenience Scripts

Next, we can add a handful of scripts to make it easier to run Rails commands from our host machine’s terminal. We added the following four scripts to the shiny-project/bin directory.

dx – shorthand for us to exec into the app container.


#!/bin/bash

docker-compose exec app "$@"

drails – shorthand for running Rails CLI commands.


#!/bin/bash

SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
$SCRIPT_DIR/dx bundle exec rails "$@"

drake – shorthand for running Rake tasks.


#!/bin/bash

SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
$SCRIPT_DIR/dx bundle exec rake "$@"

dserver – shorthand to run the Rails server.


#!/bin/bash

SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
$SCRIPT_DIR/dx bundle exec rails s -b 0.0.0.0 "$@"

With Direnv installed and PATH_add ./bin added to the .envrc, the following two commands are equivalent:


docker-compose exec app bundle install

dx bundle install

5. Creating a VS Code Dev Container

Next, we will set up a VS Code Dev Container definition to allow us to optimize our IDE for Rails development more easily. Add a new file, .devcontainers/devcontainer.json, that contains the following:


```json
{
  "name": "Existing Docker Compose (Extend)",
  "dockerComposeFile": ["../compose.yml"],
  "service": "app",
  "workspaceFolder": "/shiny-project",
  "customizations": {
    "vscode": {
      "extensions": [
        "Shopify.ruby-extensions-pack",
        "Hridoy.rails-snippets",
        "aliariff.vscode-erb-beautify"
      ]
    }
  }
}

We are just making use of our existing compose.yml app service, which already handles mounting project files and running a command to keep itself alive. We also call out a handful of VS Code extensions for Rails development that the Dev Container should install automatically. If any of these extensions require globally installed gems, we can install those as an

Then, with the Dev Containers extension installed and our app container already up and running, select the “Dev Containers: Reopen in Container” command.

6. Configuring the VS Code Workspace

You will have noticed that we have our Rails app nested inside of the top-level shiny-project folder. We chose this directory structure because we didn’t want to junk up our Rails production image with the random scripts, doc, etc. at the top of the repo.

But, many extensions we added don’t like this organization and really want the Rails app up at the root. So to get the best possible editor support, the final thing we add is a shiny-project.code-workspace to help handle this:


{
  "folders": [
    {
      "path": "../shiny-project"
    },
    // We needed to bring the shiny-app folder up to the root level so the Ruby LSP extension will work properly.
    {
      "path": "shiny-app"
    }
  ]
}

With this setup, we can easily run any of the Rails CLI commands from our terminals and have a highly customized IDE configuration. Plus, we are doing this all without ever having to install Ruby or any global Ruby gems on our own dev machines. And, our normal VS Code configuration does not have to get junked up with all the Ruby/Rails extensions and settings. Most of all, we hope this setup will help new developers hit the ground running when they pick it back up again in the years to come.

Conversation

Join the conversation

Your email address will not be published. Required fields are marked *