Deploying a Vapor app on Digital Ocean

This is a follow up to my previous blog post about Using Vapor with Docker, which described how to run a Vapor app in a Docker container. We will pick up from there and deploy that container in a Digital Ocean droplet.

Vapor is a Swift framework for web and backend development which supports both OSX and Linux. It is a great way to get started with server side swift and this (and the previous post) aim to help you get started running Vapor in a Docker container - and therefore to get it running in any place that supports Docker containers. I’ve chosen Digital Ocean as an example here but it equally applies with minimal changes to any hosting service that docker-machine supports or in fact to any hosting service that supports Docker.

Create Access Token

The first thing we need to do is obtain an access token which will be our ‘key’ to interact with Digital Ocean through its API.

Go to the API section in your Digital Ocean account and generate a new access token. We will need this token in the next step to create a droplet from the command line on our local machine.

Create Digital Ocean Droplet

With the token to hand for authorisation, run the following command to create a new droplet on Digital Ocean. Note that creating a droplet will incur costs and that you can choose which instance size you prefer by specifying --digitalocean-size. The 512mb option is cheap and sufficient for our purposes of testing:

docker-machine create \
  -d digitalocean \
  --digitalocean-size "512mb" \
  --digitalocean-access-token <your token> \
  dobox

Another useful parameter is --digitalocean-region <region code>, where region code can be nyc1 for New York City or lon1 for London for instance. Query the Digital Ocean regions API for a full list.

The last argument in the docker-machine create command is the name we want to give our droplet, dobox in this example. We will refer to the droplet by that name again in the next step.

Connect to Docker Daemon

When you run docker as is out of the box it will connect to your local Docker daemon and run containers on your local machine. However, it is very easy to change which host docker is talking to. By running the following command we will set it up to connect to our droplet dobox:

eval $(docker-machine env dobox)

This will set a few environment variables that docker uses to connect (inspect them by running printenv | grep DOCKER), directing any docker commands subsequently run to dobox rather than your local machine.

Connect to the Droplet

You can also use docker-machine to interact with the droplet directly (as opposed to the Docker daemon running on it), for example to connect to it via ssh:

docker-machine ssh dobox

When we used docker-machine create to set up the droplet it configured all the required ssh keys and it uses them here to connect securely without us having to deal with the details of key management.

In case you need to access the keys, you can find everything related to your machine in ~/.docker/machine/machines/dobox.

Add a Swap File

Compiling on the target box can take some extra memory and instead of paying a bigger box sometimes it’s good enough to add some swap space to get through a memory bottleneck. We’ll do this here to avoid trouble down the line.

Connect to the droplet via ssh as shown above:

docker-machine ssh dobox

and run

dd if=/dev/zero of=/swapfile bs=1M count=2048
chmod 0600 /swapfile
mkswap /swapfile
swapon /swapfile
echo "/swapfile none swap sw 0 0" >> /etc/fstab

The following steps assume you have done this, as the link step of building the Vapor app may require more than 512MB of RAM. See below for an alternative way of building the app where this step is not necessary.

Building the Image

Now that we have a Docker machine up and running in a droplet ready to receive our container we need to build the image file from which we will spawn that container.

The image that gets build by the vapor docker build command (see the previous blog post for details) is based on a Dockerfile that is only suited for local development purposes for a few of reasons:

  • The Dockerfile expects the sources to be volume mapped to /vapor by the vapor docker run command. This makes the image only work in conjunction with your local project directory, i.e. it cannot be relocated.
  • It is set up to run the build as part of the image CMD , which will delay start up unnecessarily. For deployment we only really need to build once, not on every startup.
  • There is no notion of versioning. What gets built and run is that you have in your local project directory at run time, which is good for development, less so for deployment/production.
  • Finally, the app is run as root , which should be avoided even when an application is containerised.

For these reasons we update the Dockerfile which is installed by vapor docker init by replacing everything below and including the # vapor specific part with the following:

# vapor specific part

# fix for /usr/lib/swift/CoreFoundation not being world readable in 05-09 (and possible others)
RUN chmod -R o+r /usr/lib/swift/CoreFoundation

# set up user
ENV USERNAME vapor
RUN adduser --disabled-password ${USERNAME}

WORKDIR /vapor
RUN chown -R ${USERNAME}:${USERNAME} /vapor

# Specify repository and revision via --build-args
# e.g. --build-arg REPO=vapor-example --build-arg REVISION=b389e2a
# REVISION can be a tag or branch
ARG REPO
ARG REVISION
ENV REPO ${REPO}
ENV REVISION ${REVISION}

USER ${USERNAME}
RUN git clone https://github.com/qutheory/${REPO}.git .
RUN git checkout ${REVISION}
RUN swift build

EXPOSE 8080

CMD .build/debug/App

This Dockerfile fixes the issues listed above and comes with parameters REPO and REVISION which we will supply at image build time with the git revision of our app we want to check out and build.

For example, let’s choose revision d2d49dd of the vapor-example repository and build the image:

docker build \
  --rm -t myapp_image \
  --build-arg SWIFT_VERSION=DEVELOPMENT-SNAPSHOT-2016-06-06-a \
  --build-arg REPO=vapor-example \
  --build-arg REVISION=d2d49dd \
  .

So what this means is that we’re effectively creating a Docker image without any local references, purely by checking out a repository and building the project for a specific revision (or branch/tag - anything that can be checked out).

Run Image

Now that we’ve built the image on our droplet we can launch it, which is as simple as running:

docker run --rm -it -p 8080:8080 myapp_image

The parameters -it make your container run in the foreground (interactively) and connect it to your terminal. --rm ensures the container gets removed when it’s stopped, while -p 8080:8080 maps the container’s port 8080 to the same port on the droplet.

This means we can then connect to the container via port 8080 on the droplet as follows:

open http://$(docker-machine ip dobox):8080

The command docker-machine ip is a simple way to get the IP address of your droplet without having to look it up in the Digital Ocean management console.

Enter Image (for debugging)

In case you want to enter a container based on your image without running the app, you can override the entry point and start bash for example:

docker run --rm -it --entrypoint bash myapp_image

This can be useful in case you want to start the app with different parameters for instance or if you need to inspect the contents of the container.

Building elsewhere

As mentioned above you do not need to build the image on the droplet, for instance if you want to avoid having to set up swap space.

Instead you can build locally and then push /pull the resulting image to the droplet via the Docker Hub registry. Note that you need to have a Docker Hub account for this (or access to another registry that supports Docker’s push and pull).

If you choose this option, make sure you build without being connected to your Digital Ocean target machine. The easiest way to ensure that is by opening a new terminal and then running the same build command as above:

docker build ...

followed by

docker push <dockerhub id>/myapp_image

where <dockerhub id> is your Docker Hub user id. This will then push the image you have just built up to Docker Hub. (Take note that depending on your settings this image will be publicly accessible for others to pull.)

Next connect to your droplet again and pull the image:

eval $(docker-machine env dobox)
docker pull <dockerhub id>/myapp_image

And we can run it as before:

docker run --rm -it -p 8080:8080 myapp_image

The only difference is that in this case the image gets to the Docker instance on the droplet by actually building it on the droplet, whereas now we build it locally and copy it there via the Docker Hub registry.

Conclusion

Docker is a great way to get started with server side swift on Linux and to work with Swift on Linux in general. It gives you easy access to an isolated environment with swift installed (even various different environments with different versions) and immediately opens up a plethora of deployment options.

In case of questions or comments please feel free to get in touch via the links below and/or follow me on Twitter.