Learn how to run Swift & Vapor inside a Docker container. Are you completely new to Docker? This article is just for you!


What the heck is Docker?

If you never heard about Docker or the containerization technology, you should read these quotes from wikipedia two or three times carefully and you'll get the basics:

Docker is a computer program that performs operating-system-level virtualization, also known as "containerization"
Docker is used to run software packages called "containers". Containers are isolated from each other and bundle their own tools, libraries and configuration files; they can communicate with each other through well-defined channels.
All containers are run by a single operating system kernel and are thus more lightweight than virtual machines.
Containers are created from "images" that specify their precise contents. Images are often created by combining and modifying standard images downloaded from public repositories.

It's pretty simple, but it's also a quite complicated technology. Docker is extremely useful if you don't want to spend hours to setup & configure your work environment. Also it helps the software deployment process, so patches, hotfixes and new code releases can be delivered more frequently.  That's why it's usually categorized as a DevOps tool. Guess what: you can use Swift right ahead through a single Docker container, you don't even need to install anything else on your computer. 🐳

Docker architecture in a nutshell

There is a nice get to know post about the Docker ecosystem, but if you want to get a detailed overview you should read the Docker glossary. In this tutorial I'm going to focus on images and containers. Maybe a little bit on the hub, engine & machines. 😅

Docker engine

Lightweight and powerful open source containerization technology combined with a work flow for building and containerizing your applications.

Docker image

Docker images are the basis of containers.

Docker container

A container is a runtime instance of a docker image.

Docker machine

A tool that lets you install Docker Engine on virtual hosts, and manage the hosts with docker-machine commands.

Docker hub

A centralized resource for working with Docker and its components.

So just a little clarification: Docker images can be created through Dockerfiles, these are the templates for running containers. Imagine them like "pre-built install disks" for your container environments. You'll get it through the samples. 💾


Docker cheatsheet for beginners

So you want to learn Docker commands, but you don't know where to start? Before I show you some real world examples here is a useful cheatsheet for the Docker CLI. Don't worry you don't have to remember any of these commands yet! Also...

feel free to skip this section, anyway you'll come back later... 😉

Docker machine commands

  • Create new: docker-machine create MACHINE
  • List all: docker-machine ls
  • Show env: docker-machine env default
  • Use: eval "$(docker-machine env default)"
  • Unset: docker-machine env -u
  • Unset: eval $(docker-machine env -u)

Docker image commands

  • Download: docker pull IMAGE[:TAG]
  • Build from local Dockerfile: docker build -t TAG .
  • Build with user and tag: docker build -t USER/IMAGE:TAG .
  • List: docker image ls or docker images
  • List all: docker image ls -a or docker images -a
  • Remove (image or tag): docker image rm IMAGE or docker rmi IMAGE
  • Remove all dangling (nameless): docker image prune
  • Remove all unused: docker image prune -a
  • Remove all: docker rmi $(docker images -aq)
  • Tag: docker tag IMAGE TAG
  • Save to file: docker save IMAGE > FILE
  • Load from file: docker load -i FILE

Docker container commands

  • Run from image: docker run IMAGE
  • Run with name: docker run --name NAME IMAGE
  • Map a port: docker run -p HOST:CONTAINER IMAGE
  • Map all ports: docker run -P IMAGE
  • Start in background: docker run -d IMAGE
  • Set hostname: docker run --hostname NAME IMAGE
  • Set domain: docker run --add-host HOSTNAME:IP IMAGE
  • Map local directory: docker run -v HOST:TARGET IMAGE
  • Change entrypoint: docker run -it --entrypoint NAME IMAGE
  • List running: docker ps or docker container ls
  • List all: docker ps -a or docker container ls -a
  • Stop: docker stop ID or docker container stop ID
  • Start: docker start ID
  • Stop all: docker stop $(docker ps -aq)
  • Kill (force stop): docker kill ID or docker container kill ID
  • Remove: docker rm ID or docker container rm ID
  • Remove running: docker rm -f ID
  • Remove all stopped: docker container prune
  • Remove all: docker rm $(docker ps -aq)
  • Rename: docker rename OLD NEW
  • Create image from container: docker commit ID
  • Show modified files: docker diff ID
  • Show mapped ports: docker port ID
  • Copy from container: docker cp ID:SOURCE TARGET
  • Copy to container docker cp TARGET ID:SOURCE
  • Show logs: docker logs ID
  • Show processes: docker top ID
  • Start shell: docker exec -it ID bash

Other useful Docker commands

  • Log in: docker login
  • Run compose file: docker-compose
  • Get info about image: docker inspect IMAGE
  • Show stats of running containers: docker stats
  • Show version: docker version

How to run Swift in a Docker container?

As I promised in the beginning let me show you how to run Swift under linux inside a Docker container. First of all, install Docker (fastest way is brew cask install docker), start the app itself (give it some permissions), and pull the "official" Swift Docker image from the cloud by using the docker pull swift command. 😎

Packaging Swift code into an image

The first thing I'd like to teach you is how to create a custom Docker image & pack all your Swift source code into it. Just create a new Swift project swift package init --type=executable inside a folder and also make a new Dockerfile:

The FROM directive tells Docker to set our base image, which will be the previously pulled "official" Swift Docker image with some minor changes. Let's make those changes right ahead!  We're going to add a new WORKDIR that's called /app, and from now on we'll literally work inside that. The COPY command will copy our local files to the remote (working) directory, CMD will run the given command if you don't specify an external command e.g. run shell. 🐚

Please note that we could use the ADD instruction instead of COPY or the RUN instuction instead of CMD, but there are slight differneces (see the links).

Now build, tag & finally run the image. 🔨

docker build -t my-swift-image . # build the image
docker run --rm my-swift-image 	 # --rm = remove container after exit

Congratulations! You just made your first Docker image & used your first Docker container with Swift! But wait, do I have to re-build every time I change my code?

Editing Swift code inside a Docker container on-the-fly

The first option is that you execute a bash docker run -it my-swift-image bash and log in to your container so you'll be able to edit Swift source files inside of it & build the whole package by using swift build (or you can run swift test if you'd just like to test your app under linux). This method is a little bit inconvenient, because all the Swift files are copied during the image build process so if you would like to pull out changes from the container you have to manually copy everything, also you can't use your favorite editor inside a terminal window.

Second option is to run the original Swift image, instead of our custom one and attach a local directory to it. Imagine that my sources are under the current directory, so I can simply run docker run --rm -v $(pwd):/app -it swift in order to start a new container with the local content mapped to the remote app directory. Now I can use Xcode, Sublime Text or anything I want to make modifications, and execute my Swift code inside the terminal window by using swift run. 🏃

Server side Swift projects inside Docker

You can also run a server side Swift application through Docker. Let's create a new Vapor project vapor new my-project and a brand new Dockerfile:

Now we use the ADD & RUN instructions in order to build our Swift source files during the image build phase. Next we have to build our sources & we'll copy the build artifact into a better location, because we don't want to mess with the architecture name in the executable's build path.

Finally we have to EXPOSE our local 8080 port to the outside world, and set our main ENTRYPOINT to the newly created Run executable file. We also have to change the IP address that Vapor listens on. Since we are inside of a Docker container we have to use 0.0.0.0 instead of 127.0.0.1.

docker build -t vapor-image .
docker run --name vapor-server -p 8080:8080 vapor-image

Let's build our image & run it. You also have to publish your exposed port, in order to make things work. Now if you go to your browser and enter http://localhost:8080 you'll see that Vapor is running inside your container... It works! ... like magic! ⭐️

Creating Swift (micro)services using Docker compose

The docker-compose command can be used to start multiple docker containers at once. You can have separate containers for every single service, like your Swift application, or the database that you are going to use. You can deploy & start all of your microservices with just one command.  Let me show you how to do this. 🤓

In this example I'm going to connect our Vapor application (running inside a Docker container) to another one that's going to run a PostgreSQL database. In order to make our sample work, we have to switch from SQLite to pgsql inside our project.  

First, we have to add postgres as a dependency inside the Package.swift file:

Next we change the configure.swift file in order to set up the PostgreSQL driver instead of SQLite, also we're going to get the database connection details from the environment. Thankfully Vapor has a nice support to get env vars:

Only a slight change is needed inside the Models/Todo.swift file:

We also have to add one little thing inside the Run/main.swift file. If a SLEEP_LENGHT environment variable is present we'll wait before we start our Vapor server. Despite the docker-compose up command can manage dependencies, it won't wait for services to load completely inside the containers and the PostgreSQL database service needs just a little extra time to boot up. In a production environment you could solve this issue by using health checks, but for the demo sleep is fine enough. 😴

The last thing that we have to do is to create a docker-compose.yaml file. This contains all the services that we'd like to start with the base images / environment variables and other configurations. Just use the following content as a base:

Ready? Go! Fire up your terminal window and enter docker-compose up to start all of our services. Docker will first build the api service based on your local Dockerfile and name it as vapor-composer-image and pull the postgres image from the hub. Next it'll create a container for both services based on the images. Environment variables will be passed to the images (you can reach out to other containers by using the service names) and the api service will be exposed on port 8080. 🌍

Just visit http://localhost:8080 after everything is up and runnning. You can stop the services by entering the docker-compose down command in a new terminal tab or window. You can also "get into the containers" - if you want to run a special script - by executing docker exec -it <my-container> bash. So cool, isn't it? 😎

🐳 +🐘 +💧 = ❤️

I hope you enjoyed this comprehensive Docker tutorial. If you want to learn more about building & hosting cloud services, you should browse my collection of server side Swift articles. More new Vapor 3 related stuff is on it's way, I promise! 😅


External sources