Rails on Docker: How to Run Your Rails App on Docker Swarm
Written by Chris • 27 January 2017
Recently, I’ve been using with the latest version of Docker 1.13 and Swarm for deploying several Rails apps.
Previously, I’d used a combination of Docker Compose and systemd to manage containerised Rails apps. This approach worked, but always felt a little cobbled-together. There were too many moving parts, and sometimes containers would mysteriously shutdown, needing manually restart.
Whilst systemd kept things running relatively well, it didn’t always play very well with Docker Compose. The setup also made seamless deployments difficult without some manual load-balancing configuration.
Switching to Swarm
Docker Swarm, which became part of the core Docker engine in 1.12, adds built-in orchestration to resolve these issues and more. Rather than declaring the containers to run, Swarm instead allows you to define services and a desired state for your containers.
The difference is subtle but important. A host running as a Docker Swarm manager is told what state you need, and then figures out what needs to be done to achieve that state—and keep it stable.
As its name suggests, Swarm also allows your containers to be distributed across any number of hosts (nodes), although for my initial testing I’ve kept everything on a single node. There is a reason for this which I will discuss at the end of this tutorial…
From Containers to Services
A Docker service
declaration is similar to that of a container – in fact, the docker service create
command shares many similar to docker run
, allowing you to specify networks, mount volumes, set environment variables, and so on.
With Docker 1.13, Swarm shares more of these options, including the option to use files to load environment variables, and even use docker-compose.yml
files to declare your services.
In this tutorial, though, we’ll keep things simple by using the standard docker service
commands to spin up and run a simple Rails application.
Creating a Swarm
Let’s start by making your Docker host into a Swarm manager.
$ docker swarm init # --advertise-addr={your_ip_address}
When using Docker for Mac, I’ve not needed to specify the --advertise-addr
. If Docker complains, though, just add it as above.
You can use your own Docker image, but to keep things simple I’ve published a basic Rails app to Docker Hub. The image exposes port 3000, and requires a PostgreSQL database. Let’s start by creating a network into which we can launch our services:
$ docker network create my_net --attachable --driver overlay
Specifying the overlay
driver will allow this network to be used by our Swarm. Using the --attachable
flag will allow us to connect containers to the network that aren’t running in our Swarm. This is handy for single-run containers, such as those used when running rails db:migrate
.
Creating a Service
A Service is a description of the desired state of your containers. Each Service specifies the image you’d like to use, along with configuration data such as environment variables, published ports, mounted volumes, and so on. Once created, Swarm will then create the containers necessary to satisfy the Service description.
Let’s start by creating a database service into which Swarm will launch PostgreSQL:
$ docker service create --replicas 1 --env POSTGRES_PASSWORD=secret --name db --network my_net postgres:9.5
Here we’re declaring a service called db
into which we want to create a container using the standard postgres:9.5
image. We’ll only want one instance of our database container running (for now), which we declare using the --replicas
option.
We can check the state of our service by running docker service ls
:
$ docker service ls
ID NAME MODE REPLICAS IMAGE
abc123def567 db replicated 1/1 postgres:9.5
The response shows that our db
service is running 1 container using the postgres:9.5
. You can confirm this with a call to docker ps
to inspect the container itself:
$ docker ps
CONTAINER ID IMAGE COMMAND ...
987fed654cba postgres@sha256:31ad0... "/docker-entrypoin..." ...
Next, let’s create a service for our Rails app. Here I’ll use a basic Rails application image that I’ve published to Docker Hub.
The app is a very simple Rails scaffold for a blog-post style model. I’ve also tweaked the config/database.yml
file as shown below so that we can use environment variables to configure database connection details:
# config/database.yml
default: &default
adapter: postgresql
encoding: unicode
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
host: <%= ENV.fetch('DB_HOST') %>
username: <%= ENV.fetch('DB_USER') %>
password: <%= ENV.fetch('DB_PASSWORD') %>
development:
<
test:
<
production:
<true}
web.1.abc123abc123@moby | ...
web.1.abc123abc123@moby | Processing by Rails::WelcomeController#index as HTML
web.1.abc123abc123@moby | Parameters: {"internal"=>true}
web.1.abc123abc123@moby | ...
web.2.def321ghi567@moby | Processing by Rails::WelcomeController#index as HTML
web.2.def321ghi567@moby | Parameters: {"internal"=>true}
web.2.def321ghi567@moby | Rendering /usr/local/bundle/gems/railties-
web.2.def321ghi567@moby | ...
web.1.abc123abc123@moby | Processing by Rails::WelcomeController#index as HTML
web.1.abc123abc123@moby | Parameters: {"internal"=>true}
web.1.abc123abc123@moby | ...
Notice that the requests are balanced between web.1 and web.2. You can scale the service back to 1 container with:
$ docker service scale web=1
Keep Things Running
One of the greatest advantages I’ve found using Swarm is that it works to keeps things running. If, for example, one of our web containers crashes, Swarm will automatically try to recreate the container and bring things back to our desired state.
Let’s try this out now by dropping one of our web containers. Start by scaling the web
service to 2 containers, so that our website won’t be interrupted:
$ docker service scale web=2
$ docker ps
CONTAINER ID IMAGE COMMAND ...
987fed654cba postgres@sha256:31ad0... "/docker-entrypoin..." ...
fed987cba654 cblunt/rails-basic-app... "/bin/sh -c 'bundle..." ...
ghi012jkl345 cblunt/rails-basic-app... "/bin/sh -c 'bundle..." ...
Next, lets drop one of the containers in our web
service. Note you’ll need to run the commands in quite quick succession to see the results of removing the container:
$ docker kill ghi012jkl345
$ docker service ls
ID NAME MODE REPLICAS IMAGE
dztp3e7lxhof web replicated 1/2 cblunt/rails-basic-app
j7tj40djnma8 db replicated 1/1 postgres:9.5
$ docker ps
CONTAINER ID IMAGE COMMAND ...
987fed654cba postgres@sha256:31ad0... "/docker-entrypoin..." ...
fed987cba654 cblunt/rails-basic-app... "/bin/sh -c 'bundle..." ...
Swarm notices that our web service is only running 1 of 2 desired replicas. Within seconds, it will seek to rectify this by creating a new container. After a few seconds, try running the service ls
and ps
commands gain to see that this has happened:
$ docker service ls
ID NAME MODE REPLICAS IMAGE
dztp3e7lxhof web replicated 2/2 cblunt/rails-basic-app
j7tj40djnma8 db replicated 1/1 postgres:9.5
$ docker ps
CONTAINER ID IMAGE COMMAND ...
987fed654cba postgres@sha256:31ad0... "/docker-entrypoin..." ...
fed987cba654 cblunt/rails-basic-app... "/bin/sh -c 'bundle..." ...
ghi012jkl345 cblunt/rails-basic-app... "/bin/sh -c 'bundle..." ...
You can also use the docker service ps
to view the current state of the service, and on which node(s) the containers are running.
$ docker service ps web
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE
tsgjsi26dmo4 web.1 cblunt/rails... moby Running Running...
lflmm9czvlsj web.2 cblunt/rails... moby Running Running...
y1xmh2b7dohf \_ web.2 cblunt/rails... moby Shutdown Failed...
xq8q2s2hag2s \_ web.2 cblunt/rails... moby Shutdown Failed...
Our app is now resilient to errors and container shutdowns, as Swarm will do what it can to keep things running as desired. There are of course options you can pass to docker service create
to instruct Swarm how often to retry a service, and how long to wait between restarts. Check out docker service create --help
for details.
Shutting Down Swarm
Once you’re finished with your swarm, you can very quickly shut everything down by “leaving” the swarm. As we have only used a single node in this tutorial, leaving the Swarm will remove its services and any containers:
$ docker swarm leave --force
Next Steps
This has been a very quick overview of getting a Rails app running on Docker Swarm. Of course, you can use this as the basis to add additional services to your app, such as background workers, and use a reverse proxy (such as nginx) to expose your web app on standard ports, use SSL, etc .
One thing I’ve not covered here is volumes, which can be used to persist state between your containers. For example, you’ll probably want to store your database’s data in a mounted volume, so that data isn’t lost between container restarts.
At the moment, Docker doesn’t natively support moving volumes across nodes, which is why I’ve been experimenting with single-node Swarms. In a multi-node Swarm, your postgres
container could be created on one node, whilst its data sits on another. Moving data between nodes is not currently supported.
A workaround for this is to restrict a service to specific nodes using the --constraint
in docker service create
. For example:
$ docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS
abc123abc123abc123abc123 * alpha Ready Active ...
bdf823sdg132khd923nmg934 * beta Ready Active ...
$ docker service create --constraint 'node.id == abc123abc123abc123abc123' --name db postgres...
There are alternative options as well, such as Flocker, but the good news is that distributed storage—volumes which can move around with your containers—look like they’ll be gaining native support in a future version of Docker. Until then, constraints are probably the easiest way to handle this.
I hope you’ve found this tutorial useful, and it helps you use Swarm to orchestrate your Rails apps. Let me know by tweeting to @cblunt, and subscribe to get updates and further tutorials about working with Rails on Docker.