howto

Rails in Docker

Why install ruby locally?

tags
rails
docker
transient

In leveraging disposability for exploration we looked at how to build software without having it installed on your local computer. Lets go through how to setup and develop a rails application with this process.

docker-compose.yml all the way down

We're going to create our app by adding things to a docker-compose.yml file as needed. Lets create the first one, which will contain our rails container as well as a volume for keeping track of all the gems.

We are going to have a few sections:

  1. args where we pass in the user_id and group_id of the user that the docker container is going to use. This should be the same as the user id in the host operation system, so files that are created by in the container in the bound volume have the right owners.
  2. volumes where we mount the gratitude directory into the container, and a gratitude-gems volume that we use to cache the bundled gems outside of the container. This makes upgrading gems that much faster when you are rebuilding the docker container so you don't need to download them everytime.
  3. ports to expose the rails server.

docker-compose.yml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
version: "3.7"

services:
  gratitude:
    build:
      context: .
      args:
        USER_ID: "${USER_ID:-1000}"
        GROUP_ID: "${GROUP_ID:-1000}"
    volumes:
      - type: bind
        source: ./gratitude
        target: /app/gratitude
      - type: volume
        source: gratitude-gems
        target: /usr/local/bundle
    ports:
      - "3000:3000"

volumes:
  gratitude-gems:

Now we build a Dockerfile to run the rails app. We base it off of the ruby:2.7 image, add the user id, install node, and then install rails and bundler. All other dependancies will be specified with the Gemfile inside of the project once we create it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
FROM ruby:2.7

ARG USER_ID
ARG GROUP_ID

RUN addgroup --gid $GROUP_ID user && adduser --disabled-password --gecos '' --uid $USER_ID --gid $GROUP_ID user

WORKDIR /app/gratitude

# nodejs and yarn
RUN curl -sL https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
RUN curl -sL https://deb.nodesource.com/setup_15.x | bash -
RUN apt-get update && apt-get install -y nodejs yarn

# install rails
RUN gem install rails bundler

EXPOSE 3000

RUN chown -R $USER_ID /usr/local/bundle

USER $USER_ID

CMD bundle exec rails server -b 0.0.0.0

Now we can bring all this up by doing:

1
mkdir -p gratitude && docker-compose up --build

This will give the error that Could not locate Gemfile or .bundle/ directory which makes sense since there's no source code. So lets make it:

1
2
3
docker-compose run gratitude bash
cd /app
rails new gratitude

This will take a bit of time to build all of the native versions. Once this is done though, ctrl-d to exit out of the shell and try docker-compose up again.

Go to http://localhost:3000

"Yay!", it says, "You're on Rails!"

ctrl-c to exit out.

Add a environment file

The next thing that we'll want to do is to add an environment file of somekind. Right now we're only going to use it to store the RAILS_MASTER_KEY, which is what is used to decrypt config/credentials.yml.enc. We will then remove the config/master.key file from the repo.

Look into config/master.key to find your value!

Create a file called .env:

1
RAILS_MASTER_KEY=a43a4d582cd7d044bed9297c9e6fb797

Keep this save! You should them make sure that you don't accidently check this file into the repository. Keep it safe in a password manager.

1
echo .env >> .gitignore

Now we need to adjust the docker-compose.yml file to include this environment variable:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
version: "3.7"

services:
  gratitude:
    build:
      context: .
      args:
        USER_ID: "${USER_ID:-1000}"
        GROUP_ID: "${GROUP_ID:-1000}"
    volumes:
      - type: bind
        source: ./gratitude
        target: /app/gratitude
      - type: volume
        source: gratitude-gems
        target: /usr/local/bundle
    ports:
      - "3000:3000"
    env_file:
      - .env

volumes:
  gratitude-gems:

You can then delete the file gratitude/config/master.key.

Changing that landing page

Running commands with docker-compose run gratitude is a bit wordy, so lets create a small bash script that will do it for us. Call it r or something.

1
2
#!/bin/bash
docker-compose run --rm gratitude "$@"

And then a quick chmod +x r and you should be good to go.

1
./r rails generate controller index home

And then we can update the config/routes.rb file to use this:

1
2
3
Rails.application.routes.draw do
  root 'index#home'
end

Adding postgres and pgadmin

Let's write up postgres into the system and create out first model. First we need to add a couple of sections to the docker-compose.yml file.

  1. Add a postgres service.
  2. Add a pgadmin service.
  3. Make the gratitude service depend upon postgres
  4. Add a volume to keep the database around and the pgadmin stuff around.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
version: "3.7"

services:
  postgres:
    image: postgres:13.1
    environment:
      POSTGRES_PASSWORD: awesome_password
    ports:
      - "5432:5432"
    volumes:
      - gratitude-postgres:/var/lib/postgresql/data

  pgadmin:
    image: dpage/pgadmin4:4.28
    environment:
      PGADMIN_DEFAULT_EMAIL: admin@example.com
      PGADMIN_DEFAULT_PASSWORD: SuperSecret
      GUNICORN_ACCESS_LOGFILE: /dev/null
    ports:
      - "4000:80"
    depends_on:
      - postgres
    volumes:
      - gratitude-pgadmin:/var/lib/pgadmin

  gratitude:
    build:
      context: .
      args:
        USER_ID: "${USER_ID:-1000}"
        GROUP_ID: "${GROUP_ID:-1000}"
    depends_on:
      - postgres
    volumes:
      - type: bind
        source: ./gratitude
        target: /app/gratitude
      - type: volume
        source: gratitude-gems
        target: /usr/local/bundle
    ports:
      - "3000:3000"
    env_file:
      - .env

volumes:
  gratitude-gems: 
  gratitude-postgres:
  gratitude-pgadmin:

Add the pg gem:

1
./r bundle add pg

And finally we need to tell rails where to find that database. First we add to our .env file:

1
DATABASE_URL=postgresql://postgres:awesome_password@postgres:5432/gratitude?encoding=utf8&pool=5&timeout=5000

Now we can create a simple model

1
./r rails g scaffold project name:string repo:string

And then we can set it up and start it up:

1
2
3
./r rake db:reset
./r rake db:migrate
docker-compose up

And see the glory that is http://localhost:3000/projects

Adding in redis and sidekiq

Another common set of things in the environment is redis and sidekiq. These are both additions to the docker-compose.yml file. One is an entry for the redis service (and it's added volume) and the other is a another container, with the same Dockerfile as the rails app, but with a slightly different command. Lets look at adding that now.

First we need to add some gems

1
2
./r bundle add sidekiq
./r bundle add redis-rails

Lets configure sidekiq and the redis cache in config/initializers/sidekiq.rb:

1
2
3
4
5
6
Rails.application.config.cache_store = :redis_store, ENV['CACHE_URL'],
                         { namespace: 'gratitude::cache' }
Rails.application.config.active_job.queue_adapter = :sidekiq
Sidekiq.configure_server do |config|
  config.redis = {url: ENV['JOB_WORKER_URL']}
end

And in our good old .env, point to our new fancy redis server:

1
2
3
REDIS_URL=redis://redis:6379/0
CACHE_URL=redis://redis:6379/0
JOB_WORKER_URL=redis://redis:6379/0

And the add everything to docker-compose.yml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
version: "3.7"

services:
  postgres:
    image: postgres:13.1
    environment:
      POSTGRES_PASSWORD: awesome_password
    ports:
      - "5432:5432"
    volumes:
      - gratitude-postgres:/var/lib/postgresql/data

  pgadmin:
    image: dpage/pgadmin4:4.28
    environment:
      PGADMIN_DEFAULT_EMAIL: admin@example.com
      PGADMIN_DEFAULT_PASSWORD: SuperSecret
      GUNICORN_ACCESS_LOGFILE: /dev/null
    ports:
      - "4000:80"
    depends_on:
      - postgres
    volumes:
      - gratitude-pgadmin:/var/lib/pgadmin

  redis:
    image: 
  redis:
    image: redis:6.0.9
    ports:
      - '6379:6379'
    volumes:
      - gratitude-redis:/var/lib/redis/data

  gratitude:
    build:
      context: .
      args:
        USER_ID: "${USER_ID:-1000}"
        GROUP_ID: "${GROUP_ID:-1000}"
    depends_on:
      - postgres
      - redis
    volumes:
      - type: bind
        source: ./gratitude
        target: /app/gratitude
      - type: volume
        source: gratitude-gems
        target: /usr/local/bundle
    ports:
      - "3000:3000"
    env_file:
      - .env

  sidekiq:
    build:
      context: .
      args:
        USER_ID: "${USER_ID:-1000}"
        GROUP_ID: "${GROUP_ID:-1000}"
    command: bundle exec sidekiq
    depends_on:
      - postgres
      - redis
    volumes:
      - type: bind
        source: ./gratitude
        target: /app/gratitude
      - type: volume
        source: gratitude-gems
        target: /usr/local/bundle
    env_file:
      - .env

volumes:
  gratitude-gems: 
  gratitude-postgres:
  gratitude-pgadmin:
  gratitude-redis:

And if you want to have a nice sidekiq admin, add the following to your config/routes.rb file:

1
2
require 'sidekiq/web'
mount Sidekiq::Web => '/sidekiq'

Finally

And when you are done with whatever you are doing:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ docker-compose down
Stopping rails_in_docker_gratitude_1 ... done
Stopping rails_in_docker_sidekiq_1   ... done
Stopping rails_in_docker_redis_1     ... done
Stopping rails_in_docker_pgadmin_1   ... done
Stopping rails_in_docker_postgres_1  ... done
Removing rails_in_docker_gratitude_1 ... done
Removing rails_in_docker_sidekiq_1   ... done
Removing rails_in_docker_redis_1     ... done
Removing rails_in_docker_pgadmin_1   ... done
Removing rails_in_docker_postgres_1  ... done
Removing network rails_in_docker_default

Everything but the volumes are removed. If you really want to get aggressive you can docker system df -v which will show you everything that's on your system, and you can blow everything away (less the volumes) but using docker system prune --all – be sure to read the documentation first!.

Previously

howto

Release code diffs

What changes between releases

tags
packagemangers
npm
bundler
git

Next

howto

Tailwind and Rails

postcss setup

tags
rails
tailwind
postcss