Setting up Rails with Docker and Caddyserver
Tags: rails, caddyserver, docker, puma
Published: 2019-06-26 20:05:05

---
title: Setting up Rails with Docker and Caddyserver
published: true
description: Configure Rails to work with Docker, utilizing Caddyserver as a reverse proxy and load balancer
tags: rails, caddyserver, docker, puma
---
# Goal
Show how to setup a basic implementation of Ruby on Rails with Docker, utilizing [Caddyserver](https://caddyserver.com/) as the reverse proxy, [tls](https://en.wikipedia.org/wiki/Transport_Layer_Security), and [load balancer](https://en.wikipedia.org/wiki/Load_balancing_(computing)).

For this tutorial, I will be using a simple demo Rails application, which you can find the source code for [here](https://github.com/dstull/docker-rails/tree/caddy).  There is also a [repo](https://github.com/dstull/caddy_rails) 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](https://github.com/dstull/caddy_rails/blob/master/src/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](https://caddyserver.com/docs/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](https://docs.docker.com/compose/compose-file/#aliases)

```
transparent
```
- according to the [docs](https://caddyserver.com/docs/proxy), 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](https://caddyserver.com/docs/proxy), this is shorthand for:
```
header_upstream Connection {>Connection}
header_upstream Upgrade {>Upgrade}
```
- in rails we would need this if using [Action Cable](https://guides.rubyonrails.org/action_cable_overview.html)

```
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](https://caddyserver.com/docs/proxy) 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](https://github.com/dstull/docker-rails/blob/caddy/config/puma.rb#L60)
 
```
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](https://github.com/nicolasazrak/caddy-cache/issues/18) 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](https://github.com/dstull/caddy_rails/blob/master/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](https://caddyserver.com/docs/http.cache), this needs to be writeable for the cache to be written.
   - note: we are running the container as [read only](https://nickjanetakis.com/blog/docker-tip-55-creating-read-only-containers) by default, therefore, any area that needs to have files written to it will need be declared as [volumes](https://docs.docker.com/storage/volumes/).

## [docker-compose.yaml](https://github.com/dstull/caddy_rails/blob/master/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](https://github.com/dstull/docker-rails/blob/caddy/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](https://tandrepires.wordpress.com/2016/11/15/the-importance-of-pid-1-in-containers/)

## [docker-compose.yml](https://github.com/dstull/docker-rails/blob/caddy/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](https://github.com/dstull/caddy_rails/blob/master/src/caddyfile#L2)
- 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](https://cloud.docker.com/repository/docker/hammer098/caddy_rails), 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.

![](https://thepracticaldev.s3.amazonaws.com/i/9kk0w5fp20a2e7gzcdl9.png)