/ rails

Dockerizing a rails app part III

The first two parts of this series illustrated how to take a regular rails application stack and dockerize it for development and testing, this article will look at deploying to production.

Given the choice I would use docker in production also but sometimes that option isn't available and deploys need to be managed using legacy tools. In the rails world that often means capistrano.

SSH agent forwarding

Capistrano works by taking advantage of a native feature of ssh called agent forwarding.

When you run a deploy task on your local machine capistrano uses your private key to authenticate with the remote server you are deploying to.

The remote server then tries to clone the latest version of your app's code from github or wherever you choose to host its source - this is where ssh-agent forwarding comes in.

Github will create and send a key challenge back to the remote server which will forward the challenge back to your local machine. Your answer to the challenge will be forwarded via the remote server back to github. You can read a detailed description of how it all works here.

The problem

When you run a capistrano task it will expect your private key to be loaded into a locally running ssh-agent which it then uses to communicate securely with other hosts. You can read more about how ssh authentication works in Capistrano here

Our newly dockerized app isn't running on our localhost any more but rather inside a container host, essentially a different machine.

Therefore in order to use capistrano, we need to find a way to pass our development machine's private key to the appliation's docker host at runtime.

A solution

I use a very simple docker-compose file to run deployment tasks (but you could just as easily use a script):

# docker-compose.deploy.yml

version: "3.3"
services:
  cap:
    image: seocahill/my_app:0.0.1
    volumes:
      - .:/app
      - ~/.ssh:/root/.ssh
    entrypoint: ash /app/script/entrypoint.sh
    command: ["staging", "git:check"]

The cap service instantiates a container based on the current stable app image and mounts the application's code and the local machine's ssh directory into it.

In order to run ssh commands in alpine linux the openssh library must be installed as a prerequisite (we added it as a buildtime dependency in the Dockerfile in part I).

The entrypoint command executes a simple script which starts the ssh agent service, adds the local-machine's private key from the mounted volume and then finally executes the cap binary while passing in any extra arguments that are present when the service is run.

#! /bin/ash

eval "$(ssh-agent -s)"
ssh-add
bin/cap "$@"

The command command specifies default arguments for the entrypoint script. This particular task checks to see if ssh-agent forwarding is properly setup on the staging server.

Deploying

Run the default command to check if your setup is working:

docker-compose -f docker-compose.deploy.yml run --rm cap

To deploy to production run:

docker-compose -f docker-compose.deploy.yml run --rm cap production deploy

Password and security

Each time the capistrano service is run it creates a container host and then destroys it after (the --rm flag removes the container post-run).

This is good because you don't want private ssh key details persisting accidentally (except on your local dev machine of course!).

The bad news is that you'll have to enter your password every time you run capistrano as the ssh-add command requires it.

If you are deploying infrequently (as I do) then this likely isn't a big deal but there are alternatives if it does bother you.

For example you can run an instance of the app image in an interactive terminal:

# open a tty
docker run --rm -ti \
    -v `pwd`:/app \
    -v `$HOME`/.ssh:/root/.ssh \
    seocahill/my_app:0.0.1 \
    ash
    
# execute the setup script
script/entrypoint.sh

# run capistrano commands as normal
bin/cap staging deploy --dry-run

If your local development machine is linux based you can mount your ssh-agent socket directly into the app container like so:

docker run -rm -ti \
    -v $(dirname $SSH_AUTH_SOCK) \
    -e SSH_AUTH_SOCK=$SSH_AUTH_SOCK 
    seocahill/my_app:0.0.1 \
    ash

That won't work for us mac people but there are many workarounds listed on this open issue.

As you can see from the length of the thread referenced above ssh-agent forwarding in docker is a highly requested feature so hopefully it will be supported natively soon.

Wrap up

I hope you enjoyed this series on how to dockerize a rails app, thanks for reading!