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](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)
Top comments (0)