Rails: From debug to deployment
TLDR: this repo has the starting point, and this is it completed project.
I have used many languages to make apps over the years. After .NET and some node development, I found myself in a job using rails. ICK! RAILS! I thought. This is old, antiquated, and hard to use!
Well, 3 years later I changed my mind. Many of the issues I had with Rails came from the old code base that I was working in. My mentor and boss when I started removed over 200K lines of code before he left, and I removed 220K lines before I left. Removing the baggage made it better, and really, using it for backend development for a REST API really made me like it. It's no-nonsense and handles things like database migrations, uploading to S3, and delayed tasks really well.
So let's get your mac setup for Rails using dev containers, so you don't hate it, like I did.
1. Setting Up Your Mac for Rails Development
1.1 Install Homebrew
Before diving into anything else, ensure you have Homebrew installed on your Mac. It's a package manager that will simplify software installations.
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
1.2 Install Docker
With Homebrew, installing Docker is as easy as:
brew install --cask docker
2. Setting Up the Rails Project with Docker
2.1 Dockerfile
Create a Dockerfile.local
for your Rails project:
FROM ruby:3.2.1
ENV ROOT="/myapp"
ENV LANG=C.UTF-8
ENV TZ=UTC
# Install essential libraries
RUN apt-get update -qq && apt-get install -y nodejs postgresql-client
RUN mkdir ${ROOT}
WORKDIR ${ROOT}
This will be used in creating the dev container. Don't worry about the production dockerfile, Rails 7.1 ships with one and we will use that later.
2.2 Docker Compose
Create a docker-compose.yml
file to set up the services used for your development container. We will need the application container, or web container, and a database. We will use postgres as it is well regarded in the rails community.
version: '3'
services:
web:
build:
context: .
dockerfile: dockerfile.local
command: bundle exec rails s -p 3000 -b '0.0.0.0 --port 1234 --dispatcher-port 26162 -- bin/rails s'
volumes:
- .:/myapp
ports:
- "3000:3000"
depends_on:
- db
db:
image: postgres
volumes:
- ./tmp/db:/var/lib/postgresql/data
3. Setting Up VS Code Remote Debugging
3.1 Install remote debugging
First we need to set up the environment to default to using docker. Go to VS Code and install the plugin below:
Once installed, create a folder named .devcontainer
at the root of your project. It will hold the setup files to allow for debugging and coding inside that container.
Next we will add the /.devcontainer/devcontainer.json
file to the project. This will set up the dev environment for you and the other devs on your team. We will add the plugins we want to use as well as the working directory of the project within the container. Most settings to get setup will be inside of this file. Here are the ones to get started:
{
"name": "Existing Docker Compose",
"dockerComposeFile": [
"../docker-compose.yml",
"docker-compose.yml"
],
"service": "web",
"workspaceFolder": "/myapp",
"customizations": {
"vscode": {
"extensions": [
"castwide.solargraph", // solargraph
"rebornix.Ruby", // Ruby
"misogi.ruby-rubocop", // Rubocop
"KoichiSasada.vscode-rdbg" // Ruby Debug
]
}
}
}
Add another docker-compose file at ./devcontainer/docker-compose.yml
and put this in it:
version: '3'
services:
web:
command: /bin/sh -c "while sleep 1000; do :; done"
3.2 Start the container
Now we are going to run some rails commands and we will need to have rails available. We are avoiding rails being installed on your computer, so we will need to get the container up and going. Once the files above are saved, each time you open the folder you will be greeted with a pop up that will ask you if you want to launch inside of the container.
However, it may not do that after you just barely created those files, so lets open it another way. In the lower left hand corner you can click on the green or blue square with ><
and run Reopen in container
(shown below) and it will attempt to run docker compose
for you and connect to the container.
You should be connected to the container at this point and ready to run commands in the container from your terminal. To open a terminal in VS code type control + `
and it should say your folder is myapp.
3.3 Set up rails project if you don't already have one
At this point we need something to debug. We will need rails, at the time of writing Rails 7.1.0 was just released, so we will be using that version. So let's install rails gem install rails
.
Now, let's add the default rails app by running rails new .
inside the folder.
If you are using the folder that I made for this article, allow all files to be overridden upon creation.
3.4 Debug Configuration
Create or edit the .vscode/launch.json
file with the following configuration:
{
//Start Rails server
"name": "Debug Rails",
"type": "rdbg",
"request": "launch",
// "command": "bundle exec rails", #bundle exec rails won't stop in the debugger
"command": "bin/rails",
"script": "s",
"args": ["-b","0.0.0.0"],
},
{
// Run tests on the active rspec file
"name": "Debug Rspec with current file",
"type": "rdbg",
"request": "launch",
"command": "bundle exec rspec",
"script": "${file}",
"args": [],
// Confirm the executed command in the window. Make it easy to specify options such as execution of
"askParameters": true
}
Save the file and click on the run and debug menu (play button with a bug on the left):
Click on the play button that reads Debug Rails
which will start the application so you can see it on localhost:3000
and it is in debug mode. Find config/pplication.rb
and add a debug point. Load the project in a browser, and watch the break point catch. This will give you full debugging capability as you develop.
4. GitHub Actions Linting & Docker Image
4.1 Create Workflow for linting
In your repository, create a .github/workflows/rubocop.yml
:
name: Rubocop
on:
pull_request:
push:
branches:
- 'main'
jobs:
build:
name: CI Rubocop
runs-on: ubuntu-latest
env:
api-dir: ./
services:
postgres:
image: postgres
ports: ["5432:5432"]
env:
POSTGRES_HOST_AUTH_METHOD: trust
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:alpine
ports: ["6379:6379"]
steps:
- uses: actions/checkout@v2
- uses: ruby/setup-ruby@v1
with:
ruby-version: 3.2.1
bundler-cache: true
- name: Install PostgreSQL
run: sudo apt-get -yqq install libpq-dev
- name: Run bundle install
working-directory: ${{env.api-dir}}
run: |
gem install bundler
bundle install --jobs 4 --retry 3
- name: Setup Database
working-directory: ${{env.api-dir}}
env:
RAILS_ENV: test
PGHOST: localhost
PGUSER: postgres
run: bin/rails db:create db:schema:load
- name: Check Rubocop Styles
working-directory: ${{env.api-dir}}
env:
RAILS_ENV: test
PGHOST: localhost
PGUSER: postgres
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
run: bundle exec rubocop
Now we need to add the rubocop gems to the project so that they run when they are in production. Open up your Gemfile
and look for the development section. Make sure it looks like this:
group :development do
# Use console on exceptions pages [https://github.com/rails/web-console]
gem 'rubocop', require: false
gem 'rubocop-discourse', require: false
gem 'rubocop-performance', require: false
gem 'rubocop-rails', require: false
gem 'rubocop-rails_config', require: false
gem 'rubocop-rake', require: false
gem 'rubocop-rspec', require: false
gem 'ruby-lsp', require: false
gem 'ruby-lsp-rails'
gem 'web-console'
# Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler]
# gem "rack-mini-profiler"
# Speed up commands on slow machines / big apps [https://github.com/rails/spring]
# gem "spring"
end
4.2 Create Workflow for building for production
In your repository, create a .github/workflows/build_and_push.yml
:
name: Build and Push Docker Image
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build and push Docker image to GitHub Packages
id: docker_build
uses: docker/build-push-action@v2
with:
username: ${{ github.actor }}
password: ${{ github.token }}
registry: docker.pkg.github.com
repository: ${{ github.repository }}
tags: latest
4.3 Create a production environment
If you want specifics, this will be covered in a separate post as it would get too long. But you have some options once you have your docker image created. There is Dokku, AWS App Engine, Kubernetes, Porter.run, and many others that can take a docker image and get it up and running. Pick one, and get going, that is the key. Dokku would be my pick for a small project as it is simple and easy to get going.
5. Wrapping Up
Now, whenever you push to the main
branch, GitHub Actions will automatically lint your ruby code and build your Docker image and push it to the GitHub image repository.
Now the reason why this is important comes down to running in an engineering team. If you can remove friction from any process, you can help relationships, and ultimately remove excess meetings or at least "check-ins." By enabling rubocop, there are no more PR reviews that are "I wish you would have used a ternary instead of if then" or other opinions. And the agreed upon code "rules" are built into the project.
The second part is building your project and having it ready for production. By eliminating the friction of going to production, you will ship more often to your users. In my experience, you don't actually go faster, but the size of your deployments go down, which lowers your risk of production bugs.
Rails is a great platform for creating a start up. It has batteries included, and if you can EMBRACE it's conventions, it works really well. When I hated Rails, I was trying to do things against it's conventions.
Enough about Rails, this tutorial will get you a full debug environment, and everything setup for PRs and Production. This really simplifies your engineering time, and will help you focus on shipping features to your users, and above all having a successful business.