Initialize a Rails app on macOS with docker

October 8th, 2023

My current Mac doesn't have enough space to install Xcode Command Line Tools and it turns out it is almost impossible to install Ruby and Rails locally without it.

Instead I decided to install it inside docker right from the start.

The first step is simply to create the following files:

Initial files

Create a Dockerfile with the following content:

# Dockerfile-local
# syntax=docker/dockerfile:1
ARG RUBY_VERSION=3.2.2
FROM ruby:$RUBY_VERSION

# Install system dependencies
# Add the necessary dependencies for building gem native extensions, 
# PostgreSQL client, and Node.js (for the Rails asset pipeline)
RUN apt-get update -qq && \
    apt-get install -y build-essential libpq-dev nodejs && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

WORKDIR /myapp
COPY Gemfile /myapp/Gemfile
COPY Gemfile.lock /myapp/Gemfile.lock
RUN bundle install

# Add a script to be executed every time the container starts.
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000

# Configure the main process to run when running the image
CMD ["rails", "server", "-b", "0.0.0.0"]

We name the file Dockerfile-local because Rails will create a production Dockerfile for us from 7.1 on.

Create a Gemfile with the following content:

# Gemfile
source "https://rubygems.org"
gem "rails", "~> 7.1.0"

Create an empty Gemfile.lock:

touch Gemfile.lock

Create an entrypoint.sh file with the following content:

#entrypoint.sh
#!/bin/bash
set -e

# Remove a potentially pre-existing server.pid for Rails.
rm -f /myapp/tmp/pids/server.pid

# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"

Create a docker-compose.yml file with the following content:

# docker-compose.yml
services:
  db:
    image: postgres
    volumes:
      - ./tmp/db:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: password
  web:
    build:
      context: .
      dockerfile: Dockerfile-local
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/myapp
    ports:
      - "3000:3000"
    depends_on:
      - db

Build the project

docker compose run --no-deps web rails new . --force --database=postgresql

--no-deps here prevents docker from running the linked services in (database, etc.), only because we haven't installed anything yet.

docker compose build

Now replace the default: &default block of code in config/database.yml

#config/database.yml
default: &default
  adapter: postgresql
  encoding: unicode
  host: db
  username: postgres
  password: password
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

Run your containers docker compose up

And create the database docker compose run web rake db:create

Congratulations, your dev server is up and running at http://0.0.0.0:3000.

Automate the process

All this manual work can be completely automated with a bash script.

Create a file named setup-rails.sh with the following content:

#!/bin/bash

# Setup script for Dockerized Rails App
# To run simply do:
## bash setup-rails.sh myapp
# or
## bash setup-rails.sh to use default app name 'myapp'

# Ensure the script is run with a name argument or set default name
APP_NAME=${1:-myapp}

# Ensure script is run from the project directory
mkdir -p $APP_NAME
cd $APP_NAME

# Dockerfile-local
DOCKERFILE_LOCAL_CONTENT=$(cat <<DOCKERFILE
ARG RUBY_VERSION=3.2.2
FROM ruby:\$RUBY_VERSION

RUN apt-get update -qq && \\
    apt-get install -y build-essential libpq-dev nodejs && \\
    apt-get clean && \\
    rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

WORKDIR /$APP_NAME
COPY Gemfile /$APP_NAME/Gemfile
COPY Gemfile.lock /$APP_NAME/Gemfile.lock
RUN bundle install

COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000
CMD ["rails", "server", "-b", "0.0.0.0"]
DOCKERFILE
)

echo "$DOCKERFILE_LOCAL_CONTENT" > Dockerfile-local

# Gemfile
GEMFILE_CONTENT="source \"https://rubygems.org\"\ngem \"rails\", \"~> 7.1.0\""
echo -e "$GEMFILE_CONTENT" > Gemfile
touch Gemfile.lock

# entrypoint.sh
ENTRYPOINT_CONTENT=$(cat <<'ENTRYPOINT'
#!/bin/bash
set -e
rm -f /myapp/tmp/pids/server.pid
exec "$@"
ENTRYPOINT
)

echo "$ENTRYPOINT_CONTENT" > entrypoint.sh
chmod +x entrypoint.sh

# docker-compose.yml
DOCKER_COMPOSE_CONTENT=$(cat <<DOCKER_COMPOSE
services:
  db:
    image: postgres
    volumes:
      - ./tmp/db:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: password
  web:
    build:
      context: .
      dockerfile: Dockerfile-local
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/$APP_NAME
    ports:
      - "3000:3000"
    depends_on:
      - db
DOCKER_COMPOSE
)

echo "$DOCKER_COMPOSE_CONTENT" > docker-compose.yml


# Build the project
docker compose run --no-deps web rails new . --force --database=postgresql

# Build Docker images
docker compose build

# Overwrite database config
DATABASE_CONFIG_CONTENT=$(cat <<'DATABASE_CONFIG'
# PostgreSQL. Versions 9.3 and up are supported.
#
# Install the pg driver:
#   gem install pg
# On macOS with Homebrew:
#   gem install pg -- --with-pg-config=/usr/local/bin/pg_config
# On Windows:
#   gem install pg
#       Choose the win32 build.
#       Install PostgreSQL and put its /bin directory on your path.
#
# Configure Using Gemfile
# gem "pg"
#
# default: &default
#   adapter: postgresql
#   encoding: unicode
#   # For details on connection pooling, see Rails configuration guide
#   # https://guides.rubyonrails.org/configuring.html#database-pooling
#   pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

default: &default
  adapter: postgresql
  encoding: unicode
  host: db
  username: postgres
  password: password
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

development:
  <<: *default
  database: ${APP_NAME}_development

test:
  <<: *default
  database: ${APP_NAME}_test

production:
  <<: *default
  database: ${APP_NAME}_production
  username: $APP_NAME
  password: <%= ENV["${APP_NAME^^}_DATABASE_PASSWORD"] %>
DATABASE_CONFIG
)

echo "$DATABASE_CONFIG_CONTENT" > config/database.yml

# Run containers
docker compose up -d

# Create DB
docker compose run web rake db:create

# Feedback to user
echo -e "\nSetup complete! Your Rails application '$APP_NAME' should be accessible at http://0.0.0.0:3000\n"

Make sure this script has the execute permission
chmod +x setup-rails.sh

Run it
bash setup-rails.sh myapp

Open your browser at http://0.0.0.0:3000

Et voilà !

Rails up and running at http://0.0.0.0:3000

Sources