Not logged in. Login

Rails Deployment with Docker

The instructions here should help you get Rails projects running on Docker.

I'm assuming here that you have a working project: it already runs with rails server, etc.

Development-style Deployment

These instructions should be enough to get things running, so you can work on the code, at least. These are not appropriate to production.

For this deployment, we will only need one container, but let's set it up with Docker Compose, so we're ready to add more later. The docker-compose.yml file will be something like this:

version: '3'
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile-devel
    ports:
      - "3000:3000"
    volumes:
      - ./:/code:rw

This will (when everything is done) expose your app at http://localhost:3000/

And the Dockerfile-devel like this:

ENV RAILS_ENV=development
RUN apt-get update \
  && apt-get install -y nodejs \
  && apt-get clean \
  && rm -rf /var/lib/apt/lists/*
COPY mycode/Gemfile /tmp/
RUN cd /tmp && bundle install
WORKDIR /code/mycode
CMD ./bin/rake db:migrate \
  && ./bin/rake db:seed \
  && ./bin/rails server --pid /var/run/rails.pid -b 0.0.0.0

You may want to adjust your Ruby dependancy in your Gemfile to be more flexible than the default:

ruby '~> 2.6'

You probably also want to move the SQLite database so it lives distinctly inside the container. In config/database.yml, something like:

development:
  <<: *default
  database: /development.sqlite3
test:
  <<: *default
  database: /test.sqlite3

Production-style Deployment

For a production-like environment, we'll have three services running: our application, a database server, and a frontend web server. The frontend server will be responsible for serving the static files (assembled by Rails' asset chain).

Docker Compose

My docker-compose.yml to do all of this is:

version: '3'
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile-app
    volumes:
      - assets:/code/blog/public/assets:rw
    environment:
      SECRET_KEY_BASE: '000000000000000000000000000000000000'
      RAILS_LOG_TO_STDOUT: 'yes'
  db: 
    image: "postgres:latest"
    volumes:
      - db:/var/lib/postgresql/data:rw
    environment:
      POSTGRES_USER: project
      POSTGRES_PASSWORD: secret
  web:
    build:
      context: .
      dockerfile: Dockerfile-web
    ports:
      - "8080:80"
    volumes:
      - assets:/assets:ro

volumes:
  assets:
  db:

Database Container

The database setup should be fairly straightforward. Note that the Compose config sets a semi-permanent volume for the database data, so it will persist when the container is destroyed/recreated.

The above config suggests Rails settings in config/database.yml like:

production:
  <<: *default
  adapter: postgresql
  host: db
  encoding: unicode
  database: project
  username: project
  password: secret

And you will have to add the Postgres gem to your Gemfile. Let's add Unicorn as well, since we'll need it later.

gem 'pg'
gem 'unicorn'

Web Frontend Container

The static assets that Rails will assemble for us need to be stored somewhere: the above Compose config creates a volume assets that will be populated by the application and served by the frontend server.

You will need to update the web server config so that most URLs are handled by your app, and the assets are served by the frontend server. That will be a something like:

location / {
    proxy_pass  http://app:8000;
    proxy_set_header X-Forwarded-Host localhost:8080;
}
location /assets {
    alias /assets;
}

Make sure your config file is copied to /etc/nginx/conf.d/default.conf by the Dockerfile-web.

Application Container

For production, we will copy our code into the image, not mount it externally.

The Docker command for the main application will need to do a few things: (1) wait for the database to be ready, (2) have Rails copy the assets into a single location where the frontend server can find then, (3) run the app with a more production-ready system.

FROM ruby:2.6
ENV RAILS_ENV=production
RUN apt-get update \
  && apt-get install -y nodejs \
  && apt-get clean \
  && rm -rf /var/lib/apt/lists/*
COPY wait.sh /wait.sh
RUN chmod +x /wait.sh
COPY mycode/Gemfile /tmp/
RUN cd /tmp && bundle install
COPY . /code
WORKDIR /code/mycode
RUN mkdir -p /code/mycode/shared/pids /code/mycode/shared/sockets /code/mycode/shared/log
CMD /wait.sh db 5432 \
  && ./bin/rake assets:precompile \
  && ./bin/rake db:migrate \
  && ./bin/rake db:seed \
  && unicorn -c config/unicorn.rb -E production -l 0.0.0.0:8000

This uses the wait.sh script script that will pause until a service is available: the Postgres database in this case.

My config/unicorn.rb is:

# from https://www.linode.com/docs/development/ror/use-unicorn-and-nginx-on-ubuntu-14-04/

# set path to the application
app_dir = File.expand_path("../..", __FILE__)
shared_dir = "#{app_dir}/shared"
working_directory app_dir

# Set unicorn options
worker_processes 2
preload_app true
timeout 30

# Path for the Unicorn socket
listen "#{shared_dir}/sockets/unicorn.sock", :backlog => 64

# Set path for logging
stderr_path "#{shared_dir}/log/unicorn.stderr.log"
stdout_path "#{shared_dir}/log/unicorn.stdout.log"

# Set proccess id path
pid "#{shared_dir}/pids/unicorn.pid"
Updated Mon Aug. 30 2021, 07:36 by ggbaker.