
Setting Up Rails with Dev Containers
Having reproducible development environments is one of the best ways to guarantee ease of application setup and code sharing in teams. Dev containers are a way to achieve this.
In this article we’ll try to give you a small introduction to what dev containers are at their core and provide a minimal example on how to set up a Rails application to use dev containers.
For new applications using Rails 8+, the app can be generated for you with a minimal setup. For detailed instructions, you can check out their official guide on this, so our guide won’t be covering the case of new rails apps. We’re more interested in tackling the problem of when you already have an application that has been around for some time (or a very long time) and you want to get it properly containerized.
What are dev containers?
The first thing we need to clarify, however, are what dev containers actually are. Because VS Code is so prevalent and it has a great extension ecosystem, it may seem that this is something specific to VS Code and their Dev Containers extension. It isn’t.
At it’s root, dev containers are just docker containers, with their normal Dockerfiles or even docker-compose.yml. In fact, if you already have a dockerized application, whatever image or set of images that might be generated from this configuration and that are used for development, might as well be your dev container.
Things get interesting when we start following certain conventions that have been adopted so that editor and IDE integration is made easy and, in turn, using these containers is also very easy.
What’s in a folder?
The first convention is the use of the .devcontainer
folder. Here is where you’re going
to store your Dockerfile and your docker-compose.yml, if you use one.
Using this folder makes it easy for editor extensions related to dev containers to pick up your configurations.
The devcontainer.json
The second convention is the use of this json file to describe your application’s setup. A minimal example would be:
{
"name": "Barebones Dev Container",
"build": {
"dockerfile": "Dockerfile",
"context": ".."
},
"postCreateCommand": "echo 'Container is ready'"
}
Now just run it
This is all that’s needed, actually. With your Dockerfile and docker-compose.yml in place, if you need one, you can build and run your dev container. The json file will just make it so if you’re using VS Code or some other editor that has an extension for dev containers, the editor will know to do the following:
- Build your containers:
docker build -f .devcontainer/Dockerfile -t my-dev-container .
- Run them:
docker run -it --name my-dev-container-instance -v "$(pwd):/workspace" my-dev-container
And that’s it. A barebones example and explanation
Ok, but how’s it done with Rails?
Given the above, setting up a Rails application to use dev containers is straightforward. Like before
we create a .devcontainer
folder and place our Dockerfile and docker-compose.yml inside. In our
case we need a docker-compose.yml because, besides the application, we use a database. Most Rails
apps should need this and maybe even more services for background jobs, key value data stores and
so forth. Furthermore, we need to setup our database and bundle our gems, which means we also need
and entrypoint.sh
. All of this can go inside the .devcontainer
folder:
# .devcontainer/Dockerfile
FROM ruby:3.3.0
# Install essential packages, PostgreSQL client libraries, and netcat
RUN apt-get update && apt-get install -y \
nodejs \
yarn \
build-essential \
libpq-dev \
netcat-traditional \
&& rm -rf /var/lib/apt/lists/*
# Install Rails and Bundler
RUN gem install rails bundler
# Set working directory
WORKDIR /app_name # Call the workspace dir whatever your application name is
# Copy the entrypoint script and make it executable
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
# Set the entrypoint (no CMD here to auto-run Rails)
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
#!/bin/bash
set -e
echo "Running bundle install..."
bundle install
echo "Waiting for PostgreSQL to be ready at ${DATABASE_HOST:-db}:5432..."
# Wait until the PostgreSQL service is accepting connections
while ! nc -z ${DATABASE_HOST:-db} 5432; do
sleep 1
done
echo "PostgreSQL is ready. Setting up the Rails database..."
bundle exec rails db:create
bundle exec rails db:migrate
echo "Rails database setup complete."
echo "Rails database setup complete. Launching interactive shell..."
exec bash
# docker-compose.yml
version: '3.8'
services:
web:
build:
context: .
dockerfile: Dockerfile
volumes:
- ..:/app_name:cached # Make our project's root a mounted volume
- bundle_cache:/store/vendor/bundle # Cache our gems in a named volume so we don't build everything from scratch every time.
ports:
- "3000:3000"
depends_on:
- db
environment:
- DATABASE_HOST=db
- DATABASE_USERNAME=postgres
- DATABASE_PASSWORD=postgres
- DATABASE_NAME=rails_dev
- BUNDLE_PATH=/app_name/vendor/bundle # Properly configure BUNDLE_PATH in the container.
db:
image: postgres:13
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: rails_dev
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
bundle_cache:
And, finally our json file. Again, this is good to have even if you run your containers manually, for documentation. Obviously this is needed to use VS Code’s Dev-Containers extension:
// .devcontainer/devcontainer.json
{
"name": "Rails Dev Container",
"dockerComposeFile": "docker-compose.yml",
"service": "web",
"workspaceFolder": "/app_name",
"customizations": {
"vscode": {
"extensions": [
"rebornix.Ruby" // Optional: Provides Ruby language support in VS Code
]
}
},
"postCreateCommand": "bundle install && rails db:create && rails db:migrate"
}
Now you can build and run your containers:
docker compose -f .devcontainer/docker-compose.yml build
docker compose -f .devcontainer/docker-compose.yml run --rm --service-ports web
And that’s all you need! Finally, if you open the project’s folder in VS Code and the Dev Containers extension is installed, you can open the project in the container and it will all work just the same.
To access your app, you can just visit localhost:3000
. To run tests, if you’re not using VS Code,
would be with docker compose. Below we use rspec
, but you can replace it with whatever command you
use to run tests. Furthermore, you need to use bundle exec
unless you configure your container to
add the gem executables bundler installs to the path. We decided not to just for simplicity:
docker compose -f .devcontainer/docker-compose.yml run --rm --service-ports web bundle exec rspec
Final Considerations
One may argue that there isn’t much benefit in running dev containers manually, given the smoother
approach VS Code offers. No to mention that if you let VS Code set up the dev containers for you,
you’ll notice it makes heavy use of the devcontainer.json
file to do some of the things we did
in entrypoint.sh
.
However, we just wanted to show that this is a setup that can work for all editors of all team members
as long as all team members have docker installed. Furthermore, the experience can be made smoother
by using tools to automate more complex commands like the docker compose
ones we used. We were recently
introduced to dip which would allow you to say, for example, dip bash
and it’ll just give you the shell into the application container as if you had run the docker compose run
command presented here. All of this with a simple yml file.
Here at FastRuby.io we specialize in making your apps better to develop, faster to run and easier to maintain. Got an application in need of some maintenance love? Talk to us!