loading...

Setting up Rails with Docker and Caddyserver

dstull profile image Doug Stull ・6 min read

Goal

Show how to setup a basic implementation of Ruby on Rails with Docker, utilizing Caddyserver as the reverse proxy, tls, and load balancer.

For this tutorial, I will be using a simple demo Rails application, which you can find the source code for here. There is also a repo for the basic Caddyserver setup.

I'll break this down into 2 steps:

  • Create Caddyserver Docker image
  • Configure Rails and Caddyserver with Docker

Create Caddyserver Docker image

For this part we will go over the caddyfile, Dockerfile and docker-compose.yml files.

caddyfile

Note: all items below will reference the file linked above, and would be helpful if it were open in a separate window to reference.

Site/host declaration

https://localhost
  • for our simple demo, we'll just set this up for our local development. In production, this would be set to your dns name that is validated in your tls certificate

Proxy setup

proxy / http://rails:3000 http://rails2:3000
  • this will instruct Caddy to route all requests to our 2 containers running puma/rails, both listening on port 3000 for requests on the docker network where rails and rails2 are setup as aliases
transparent
  • according to the docs, this is shorthand for:
header_upstream Host {host}
header_upstream X-Real-IP {remote}
header_upstream X-Forwarded-For {remote}
header_upstream X-Forwarded-Port {server_port}
header_upstream X-Forwarded-Proto {scheme}
websocket
  • according to the docs, this is shorthand for:
header_upstream Connection {>Connection}
header_upstream Upgrade {>Upgrade}
policy round_robin
  • I changed this from the default of random to have a little more predictability, which helps when troubleshooting.
fail_timeout 30s
max_fails 1
try_duration 90s
health_check /stats?token=stats
health_check_interval 30s
health_check_port 9191
health_check_timeout 10s
  • I was confused/couldn't wrap my head around the official documentation on how this all worked together, so here is my attempt at explaining in a way that made sense to me...
    • fail_timeout:
      • try for X amount of time before declaring a request failed and moving on to try another backend, kicking in the try_duration.
    • try_duration:
      • after fail_timeout is reached for the first time, this will then pick up and will try and find another backend so that the request doesn't fail.
      • so if fail_timeout is 30s and try_duration is 90s, and both backends are down...it would be 2 minutes before you get a bad gateway (502) response from Caddy.
      • try_duration must be greater than fail_timeout + time to find backend and serve request(rails request part), else it will respond with 502 on the first response from a failed backend.
    • health_check:
      • hitting a puma control app to get status, therefore the port needs to be specifically set. For more insight into this setting, see the backend rails app settings in the puma config
errors stdout
header / {
  Strict-Transport-Security "max-age=31536000"
}
log / stdout "{combined} cache={cache_status}"
gzip
tls self_signed
  • Docker prefers logging to stdout, so we'll do that here, adding some formatting for cacheing, in case we turn it on in the future.
  • Caching is off for now due to this issue on the current Caddyserver image we are using.
  • gzip content to speed everything up. I thought there might be issues with this and Rails, but there wasn't and it provides great speed improvements in page load.
  • use a simple self signed cert for this demo

Dockerfile

In this file, I will be highlight a few items that might not be strait forward.

RUN apk add --no-cache \
    libcap \
    && \
    :

RUN setcap cap_net_bind_service=+ep /usr/sbin/caddy

  • install the alpine linux package libcap, which will enable us in the next line to grant binding privileges to the caddy binary.
  • this will in turn allow us to run a caddy container as an unprivileged user(non root) on a port below 1024(443).
VOLUME /tmp
  • if using the caddy cache module, this needs to be writeable for the cache to be written.
    • note: we are running the container as read only by default, therefore, any area that needs to have files written to it will need be declared as volumes.

docker-compose.yaml

In this file we are declaring some items that will help build and test out our app:

build locally

15:25 $ docker-compose build caddy_rails
Building caddy_rails
Step 1/10 : FROM jumanjiman/caddy:v0.11.0-20181002T1350-git-3d0ba71
 ---> 6b039a312afc
Step 2/10 : USER root
 ---> Using cache
 ---> ab346bccfd06
Step 3/10 : COPY src/caddyfile /etc/caddy/caddyfile
 ---> Using cache
 ---> 27bcf28474ae
Step 4/10 : COPY src/init.sh /usr/bin
 ---> Using cache
 ---> 047528cc4a11
Step 5/10 : COPY src/healthcheck /var/opt/healthcheck
 ---> Using cache
 ---> d15a10faa15c
Step 6/10 : RUN apk add --no-cache          libcap          &&      :
 ---> Using cache
 ---> e0a9d0b2b44e
Step 7/10 : RUN setcap cap_net_bind_service=+ep /usr/sbin/caddy
 ---> Using cache
 ---> d23679349861
Step 8/10 : VOLUME /tmp
 ---> Using cache
 ---> 2e94a84380e6
Step 9/10 : USER caddy
 ---> Using cache
 ---> b2d343318687
Step 10/10 : ENTRYPOINT ["/usr/bin/init.sh"]
 ---> Using cache
 ---> 8b6a28c0ab20

Successfully built 8b6a28c0ab20
Successfully tagged caddy_rails:latest

Run locally

15:25 $ docker-compose up -d
Creating network "caddy_rails_default" with the default driver
Creating caddy_rails_caddy_rails_1 ... done
15:26 $ docker ps
CONTAINER ID        IMAGE               COMMAND              CREATED             STATUS              PORTS                  NAMES
13e9cf1dd292        caddy_rails         "/usr/bin/init.sh"   4 seconds ago       Up 4 seconds        0.0.0.0:443->443/tcp   caddy_rails_caddy_rails_1

Configure Rails and Caddyserver with Docker

In this section, I am going to focus on the setup required for Rails to work with our Caddy setup, glossing over more specific Rails/Docker items at times.

Dockerfile

ENTRYPOINT ["/web/script/entrypoint"]
CMD ["puma", "-C", "config/puma.rb"]
  • set the entrypoint script, which will merely exec the command passed and allow it to take over PID 1

docker-compose.yml

In this file, we setup our local build and runtime environments.

We define:

  • 2 Rails instances so that caddy can load balance across rails and rails2. We declare aliases on our networks for these instances, which then enables caddy to reference them as backends in the caddyfile
  • Basic Healthcheck settings for docker to determine the container health status as seen from docker ps
    healthcheck:
      test: ["CMD", "curl", "http://localhost:3000"]
      interval: 10s
      timeout: 10s
      retries: 20
  • I have configured the caddy_rails example above to automatically build images on update of master branch on dockerhub, and reference it now in the file.
image: hammer098/caddy_rails:latest
  • On Caddy, expost port 443 to the underlying host
    ports:
      - 443:443
  • Finish setting up our dependency chain so that when we start everything with a docker-compose up -d caddy, it will start in sequence of rails -> rails2 -> caddy
    depends_on:
      - rails2

Build

15:54 $ sdlc/build
Building rails
Step 1/11 : FROM hammer098/ruby_24
 ---> ba1ba3ccbaca
Step 2/11 : WORKDIR /web
 ---> Using cache
 ---> 1d49ff0018bb
Step 3/11 : COPY .ruby-version /web/.ruby-version
 ---> Using cache
 ---> f187344f9c6c
Step 4/11 : COPY Gemfile /web/Gemfile
 ---> Using cache
 ---> 5592d6874d91
Step 5/11 : COPY Gemfile.lock /web/Gemfile.lock
 ---> Using cache
 ---> 96ae9333a90e
Step 6/11 : RUN /bin/bash -l -c "bundle install"
 ---> Using cache
 ---> c593b6c4f41e
Step 7/11 : COPY . /web
 ---> 7ab2c7c6ac96
Step 8/11 : ENV TEMP /web/tmp
 ---> Running in 610cb512b3d1
Removing intermediate container 610cb512b3d1
 ---> ba400993482d
Step 9/11 : VOLUME /web/tmp
 ---> Running in aa2e71bc9236
Removing intermediate container aa2e71bc9236
 ---> 4489c90a69bb
Step 10/11 : ENTRYPOINT ["/web/script/entrypoint"]
 ---> Running in 9bb7c471c455
Removing intermediate container 9bb7c471c455
 ---> 602ff0fd2660
Step 11/11 : CMD ["puma", "-C", "config/puma.rb"]
 ---> Running in 99f2a4c59410
Removing intermediate container 99f2a4c59410
 ---> cc6672106b95
Successfully built cc6672106b95
Successfully tagged rails:latest

real    0m2.346s
user    0m0.328s
sys 0m0.098s


REPOSITORY              TAG                                 IMAGE ID            CREATED                  SIZE
rails                   latest                              cc6672106b95        Less than a second ago   1.13GB

Run

15:54 $ sdlc/run

Starting application container(s)

Creating network "docker-rails_railsnet" with driver "bridge"
Creating docker-rails_rails_1 ... done
Creating docker-rails_rails2_1 ... done
Creating docker-rails_caddy_1  ... done

15:55 $ docker ps
CONTAINER ID        IMAGE                          COMMAND                  CREATED             STATUS                            PORTS                  NAMES
a85a08b577c1        hammer098/caddy_rails:latest   "/usr/bin/init.sh"       5 seconds ago       Up 3 seconds                      0.0.0.0:443->443/tcp   docker-rails_caddy_1
20e44a42434a        rails                          "/web/script/entrypo…"   7 seconds ago       Up 4 seconds (health: starting)                          docker-rails_rails2_1
f07ac602d1d4        rails                          "/web/script/entrypo…"   18 seconds ago      Up 16 seconds (healthy)                                  docker-rails_rails_1

Open Browser to see Rails running through Caddy

open to https://localhost and accept the certificate warning.

Posted on by:

Discussion

markdown guide