Rails: From debug to deployment

Rails: From debug to deployment
Photo by Joshua Fuller / Unsplash

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:

Remote Development - Visual Studio Marketplace
Extension for Visual Studio Code - An extension pack that lets you open any folder in a container, on a remote machine, or in WSL and take advantage of VS Code’s full feature set.

(At the time of this writing there is a bug with the most recent version of this plugin pre-release 0.317, and version 0.311 will need to be used.)

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.

🚧
If you run into problems launching, you may have got some parts wrong and will need to debug your docker setup. Run docker compose up -d to run the containers manually. Look inside of the logs in docker desktop for any issues that may arise.

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.

Don't miss the next post