Vapor: Simple automatic deployment using Docker and GitLab CI
This is a little guide for how to deploy a non-essential API built in Vapor using Docker and GitLab CI on a Linux server.
This guide shows how to prepare the project for deployment using Docker, having GitLab CI package up a new version whenever I push, and then have the server pick-up the changes and redeploy automatically.
We’ll be using Vapor 4, Docker, GitLab CI and the GitLab Container Registry, and watchtower.
Please note that this has some important caveats. We’re not addressing HTTPS support and there’s a brief downtime during updates. If those bother you, you’ll have some additional work to do or you might consider using Heroku instead.
Basic setup
My API built in Vapor is ready to be deployed. However, each deployment cycle is slow as the project is linking GEOS, which is a sizeable third-party library. So a built can take several minutes, and I don’t want to wait for that myself everytime I commit a change to the repo.
Therefore a key requirement is that when I push a change to the repo, tests should run, the new API should be built and then automatically deployed to the server.
Enable the GitLab Registry
Firstly, we’ll enable the container registry on our GitLab repository. This is where the latest built Docker container for our API will be hosted. The server will then point at that and always get the latest container from it. GitLab CI will update it, whenever it processed a new commit.
This is easy: Go to “Packages” > “Container Registry” for your repo, and follow the steps to enable it. That’s it.
Configure GitLab CI
Next, we’ll enable GitLab CI. GitLab CI will run certain actions on every commit.
Vapor apps are already configured to work with Docker. That means we can go with the Docker template for GitLab CI. This will also already include the necessary snippets to push to the registry that we configured in the previous step.
Adding this will add a .gitlab-ci
file to your repo, which will trigger the first build.
To confirm, check the container registry page for your repo, and the first image should appear there.
Run on the server
On your server, make sure you have Docker installed (e.g., follow these instructions for Debian).
As we’ll be pulling the Docker images from our registry on GitLab, you’ll first need to register with that. Run the following command on your server:
docker login registry.gitlab.com
Now you can try running your docker image from the registry. Grab the path for that from your repo’s container registry page and then run:
docker run -p 80:8080 <YOUR_PATH_GOES_HERE>
And this should now run already on your server and is accessible on port 80, i.e., the default HTTP port.
However, when do push an update to your git repo, this won’t be picked up until you restart docker.
Automatically reload on new builds
There are different ways for automatically deploying new Docker images. To do it properly, you’ll likely want to use Kubernetes. However, my API isn’t critical, so something simple that just restarts the Docker container will do — just be aware that this leads to a brief downtime.
A great fit for this is watchtower, which is running as a separate Docker container that monitors your other Docker containers and then restarts them when their image changes. That’s exactly what we need.
Firstly, stop the container as started above and make sure it’s running as a daemon by providing the -d
option:
docker run -d -p 80:8080 <YOUR_PATH_GOES_HERE>
Then start watchtower, providing the path to your .docker/config.json
which is required by watchtower to monitor our private registry:
docker run -d \
--name watchtower \
-v /var/run/docker.sock:/var/run/docker.sock \
-v <PATH_TO_HOME_DIR>/.docker/config.json:/config.json \
containrrr/watchtower
And that’s it. Now, whenever you push to your repo, GitLab CI will rebuild the Docker image, push it to the registry, watchtower will pick it up (at an interval of every 5 minutes by default), and your changes to the API are then live.
Maintenance
Useful commands for checking if things are working okay:
- See if the Docker containers are running:
docker ps
- Get logs using
docker logs <CONTAINER_ID>
where that container ID comes fromps
Alternative: Heroku
Rather than going this route, Heroku is a good alternative and has great built-in support in Vapor. You’ll get HTTPS out-of-the-box and zero-downtime updates, and there’s no server to manage.
NOTE: Pay attention when setting it up, to add this snippet to your configure.swift
// dynamic port allocation
if let port = Environment.get("PORT").flatMap(Int.init) {
app.server.configuration.port = port
}
Otherwise your Heroku app will go from “starting” to “crashed” without any indication why.