09 February 2024

Rails development in docker with Rubymine or Intellij

Nearly 14 years ago, I wrote about setting up emacs for Rails development. Having a need for a new Rails app, I figured it was time to look at a more modern IDE to make things easier.

The last time I tried to install Rails on my mac, I got into all sorts of build and compile errors that made it painful to even get started. These days I try to use Docker to make setup and laptop moves easier.

Having a need for a new Rails app, I though it would be interesting to try a fully dockerized dev environment. Rubymine supports developing in such a way, and this post describes how to setup it up.

Generate the Rails App

Without Ruby locally, and by extension Rails, the first challenge is to generate a new Rails app. Doing that is as simple as:

docker run --rm -v "$PWD":/usr/src/app -w /usr/src/app --user $UID:$UID ruby:3.2.3 sh -c "gem install rails && /usr/local/bundle/bin/rails new shiny_new_app"

This uses the Ruby 3.2.3 base image to install Rails and generate the new application.

A Development Docker Image

Next, you need a container which contains Ruby and all the gems needed to run the Rails application. This can be defined in a Dockerfile, at the root of the Rails project, which I have named Dockerfile.dev. With bind mounts, (ie mounting the source into the container), on Mac OS the file permissions are translated by Docker Desktop. If you are running on Linux or WSL2 using Docker directly, the container needs to run as your own user ID, otherwise files created inside the container on the bind mount will be owned by root. Much of this Dockerfile is taken from this blog post

# ARG defined before from, can only be used in FROM.
ARG RUBY_VERSION=3.2.3
FROM ruby:$RUBY_VERSION

# Needed if you need to install some OS / Container packages for Gems
# Adding some generally useful tools here
RUN apt-get update && apt-get install -y \
    build-essential \
    git \
    netcat-traditional \
    vim \
    sudo

# Non root user. Pass in the uid and gid for your local user for build
ARG UID
ENV UID $UID
ARG GID
ENV GID $GID
ARG USER=ruby
ENV USER $USER

RUN groupadd -g $GID $USER && \
    useradd -u $UID -g $USER -m $USER && \
    usermod -p "*" $USER && \
    usermod -aG sudo $USER && \
    echo "$USER ALL=NOPASSWD: ALL" >> /etc/sudoers.d/50-$USER


# throw errors if Gemfile has been modified since Gemfile.lock
# RUN bundle config --global frozen 1
#

# Bundler and gem install here.

ENV LANG C.UTF-8

ENV BUNDLE_PATH /gems
ENV BUNDLE_HOME /gems
ENV BUNDLE_BIN /gems/bin

ARG BUNDLE_JOBS=20
ENV BUNDLE_JOBS $BUNDLE_JOBS
ARG BUNDLE_RETRY=5
ENV BUNDLE_RETRY $BUNDLE_RETRY

ENV GEM_HOME /gems
ENV GEM_PATH /gems

ENV PATH /gems/bin:$PATH

RUN mkdir -p "$GEM_HOME" && chown $USER:$USER "$GEM_HOME"
RUN mkdir -p /usr/src/app && chown $USER:$USER /usr/src/app

WORKDIR /usr/src/app

USER $USER

WORKDIR /usr/src/app

COPY Gemfile Gemfile.lock ./
RUN bundle install

# This was needed to get around an error with Rubymine, but I don't recall what.
#RUN mkdir -p /usr/local/bundle/bin && ln -s /usr/local/bin/bundle /usr/bin/bundle

CMD ["/usr/local/bin/bundle", "exec", "rails", "server"]

This Dockerfile uses the Gemfile from the Rails apps and installs all the required Gems, which includes Rails. At this stage, this container can run rails server and hence your Rails app.

As the image has a few parameters, the easiest way to build it, is inline with a docker-compose definition, rather than the usual command (ignoring the arguments):

docker build . -f Dockerfile.dev -t <imageName>:<version>

Compose Environment

Having the image, we now need a docker-compose.yaml file to be used by Rubymine. This is fairly simple:

services:
  web:
    build:
      context: ./
      dockerfile: ./Dockerfile.dev
      args:
      # Check these for your current user ID. They don't matter
      # when running for Docker Desktop on Mac
        - UID=1000
        - GID=1000  
    image: testapp:1
    volumes:
      - .:/usr/src/app
    ports:
      - "127.0.0.1:3000:3000"
      # Needed for debugging
      - "1234:1234"
      - "26166:26166"
    command: tail -f /dev/null

Notice we map the localhost port 3000 to the host so we can access the Rails app locally. The other ports are for the Rubymine debugger.

The command is set to tail -f /dev/null so the container start and the IDE can run the server and various commands within it, somewhat like a remote host.

Rubymine / Intellij Setup

The setup for Rubymine is the same as for Intellij Ultimate with the Ruby and Docker plugins. There is also a pretty nice youtube video from Jetbrains that walks through the setup and IDE features.

  1. Open the project via File -> New Project From Existing Sources. When the import completes, the IDE may give a warning about "No Ruby Interpreter configured for the project. Before configuring the interpreter, we should confirm docker is working with the IDE.

  2. Open View -> Tool Windows -> Services (command-8 / ALT-8 on Windows) to reveal the docker window. Confirm docker shows as connected.

  3. Open the docker-compose.yaml, and using the small green arrows in the margin, start the environment

  4. Access the project preferences (command-; / ctrl-alt-shift-s on Windows), and go to SDKs. Click +, "Add Ruby SDK", "Remote Interpreter or version manager". Choose docker-compose, select "web" as the service (from above docker-compose.yaml).

  5. Now goto Project (still in project structure via command-;), and select the newly added remote interpreter.

  6. The final thing to to check is that any commands run inside the established docker-compose container. Using "command-, / ctrl-alt-s on Windows", go to the general preferences and then Build, Execution, Deployment -> Docker -> Ruby Settings. Ensure "docker-compose-exec, run the project with docker-compose up if needed".

With that all done, you should be able to double press control to open the "run anything" menu and then run rails server, rake tasks, migrations, generators etc.

blog comments powered by Disqus