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 thevapor 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.