Docker Deploys¶
Using Docker to deploy your Vapor app has several benefits:
- Your dockerized app can be spun up reliably using the same commands on any platform with a Docker Daemon -- namely, Linux (CentOS, Debian, Fedora, Ubuntu), macOS, and Windows.
- You can use docker-compose or Kubernetes manifests to orchestrate multiple services needed for a full deployment (e.g. Redis, Postgres, nginx, etc.).
- It is easy to test your app's ability to scale horizontally, even locally on your development machine.
This guide will stop short of explaining how to get your dockerized app onto a server. The simplest deploy would involve installing Docker on your server and running the same commands you would run on your development machine to spin up your application.
More complicated and robust deployments are usually different depending on your hosting solution; many popular solutions like AWS have builtin support for Kubernetes and custom database solutions which make it difficult to write best practices in a way that applies to all deployments.
Nevertheless, using Docker to spin your entire server stack up locally for testing purposes is incredibly valuable for both big and small serverside apps. Additionally, the concepts described in this guide apply in broad strokes to all Docker deployments.
Set Up¶
You will need to set your developer environment up to run Docker and gain a basic understanding of the resource files that configure Docker stacks.
Install Docker¶
You will need to install Docker for your developer environment. You can find information for any platform in the Supported Platforms section of the Docker Engine Overview. If you are on Mac OS, you can jump straight to the Docker for Mac install page.
Generate Template¶
We suggest using the Vapor template as a starting place. If you already have an App, build the template as described below into a new folder as a point of reference while dockerizing your existing app -- you can copy key resources from the template to your app and tweak them slightly as a jumping off point.
- Install or build the Vapor Toolbox (macOS, Linux).
- Create a new Vapor App with
vapor new my-dockerized-app
and walk through the prompts to enable or disable relevant features. Your answers to these prompts will affect how the Docker resource files are generated.
Docker Resources¶
It is worthwhile, whether now or in the near future, to familiarize yourself with the Docker Overview. The overview will explain some key terminology that this guide uses.
The template Vapor App has two key Docker-specific resources: A Dockerfile and a docker-compose file.
Dockerfile¶
A Dockerfile tells Docker how to build an image of your dockerized app. That image contains both your app's executable and all dependencies needed to run it. The full reference is worth keeping open when you work on customizing your Dockerfile.
The Dockerfile generated for your Vapor app has two stages. The first stage builds your app and sets up a holding area containing the result. The second stage sets up the basics of a secure runtime environment, transfers everything in the holding area to where it will live in the final image, and sets a default entrypoint and command that will run your app in production mode on the default port (8080). This configuration can be overridden when the image is used.
Docker Compose File¶
A Docker Compose file defines the way Docker should build out multiple services in relation to each other. The Docker Compose file in the Vapor App template provides the necessary functionality to deploy your app, but if you want to learn more you should consult the full reference which has details on all of the available options.
Note
If you ultimately plan to use Kubernetes to orchestrate your app, the Docker Compose file is not directly relevant. However, Kubernetes manifest files are similar conceptually and there are even projects out there aimed at porting Docker Compose files to Kubernetes manifests.
The Docker Compose file in your new Vapor App will define services for running your app, running migrations or reverting them, and running a database as your app's persistence layer. The exact definitions will vary depending on which database you chose to use when you ran vapor new
.
Note that your Docker Compose file has some shared environment variables near the top. (You may have a different set of default variables depending on whether or not you're using Fluent, and which Fluent driver is in use if you are.)
x-shared_environment: &shared_environment
LOG_LEVEL: ${LOG_LEVEL:-debug}
DATABASE_HOST: db
DATABASE_NAME: vapor_database
DATABASE_USERNAME: vapor_username
DATABASE_PASSWORD: vapor_password
You will see these pulled into multiple services below with the <<: *shared_environment
YAML reference syntax.
The DATABASE_HOST
, DATABASE_NAME
, DATABASE_USERNAME
, and DATABASE_PASSWORD
variables are hard coded in this example whereas the LOG_LEVEL
will take its value from the environment running the service or fall back to 'debug'
if that variable is unset.
Note
Hard-coding the username and password is acceptable for local development, but you should store these variables in a secrets file for production deployment. One way to handle this in production is to export the secrets file to the environment that is running your deploy and use lines like the following in your Docker Compose file:
DATABASE_USERNAME: ${DATABASE_USERNAME}
This passes the environment variable through to the containers as-defined by the host.
Other things to take note of:
- Service dependencies are defined by
depends_on
arrays. - Service ports are exposed to the system running the services with
ports
arrays (formatted as<host_port>:<service_port>
). - The
DATABASE_HOST
is defined asdb
. This means your app will access the database athttp://db:5432
. That works because Docker is going to spin up a network in use by your services and the internal DNS on that network will route the namedb
to the service named'db'
. - The
CMD
directive in the Dockerfile is overridden in some services with thecommand
array. Note that what is specified bycommand
is run against theENTRYPOINT
in the Dockerfile. - In Swarm Mode (more on this below) services will by default be given 1 instance, but the
migrate
andrevert
services are defined as havingdeploy
replicas: 0
so they do not start up by default when running a Swarm.
Building¶
The Docker Compose file tells Docker how to build your app (by using the Dockerfile in the current directory) and what to name the resulting image (my-dockerized-app:latest
). The latter is actually the combination of a name (my-dockerized-app
) and a tag (latest
) where tags are used to version Docker images.
To build a Docker image for your app, run
docker compose build
from the root directory of your app's project (the folder containing docker-compose.yml
).
You'll see that your app and its dependencies must be built again even if you had previously built them on your development machine. They are being built in the Linux build environment Docker is using so the build artifacts from your development machine are not reusable.
When it is done, you will find your app's image when running
docker image ls
Running¶
Your stack of services can be run directly from the Docker Compose file or you can use an orchestration layer like Swarm Mode or Kubernetes.
Standalone¶
The simplest way to run your app is to start it as a standalone container. Docker will use the depends_on
arrays to make sure any dependant services are also started.
First, execute:
docker compose up app
and notice that both the app
and db
services are started.
Your app is listening on port 8080 and, as defined by the Docker Compose file, it is made accessible on your development machine at http://localhost:8080.
This port mapping distinction is very important because you can run any number of services on the same ports if they are all running in their own containers and they each expose different ports to the host machine.
Visit http://localhost:8080
and you will see It works!
but visit http://localhost:8080/todos
and you will get:
{"error":true,"reason":"Something went wrong."}
Take a peak at the logs output in the terminal where you ran docker compose up app
and you will see:
[ ERROR ] relation "todos" does not exist
Of course! We need to run migrations on the database. Press Ctrl+C
to bring your app down. We are going to start the app up again but this time with:
docker compose up --detach app
Now your app is going to start up "detached" (in the background). You can verify this by running:
docker container ls
where you will see both the database and your app running in containers. You can even check on the logs by running:
docker logs <container_id>
To run migrations, execute:
docker compose run migrate
After migrations run, you can visit http://localhost:8080/todos
again and you will get an empty list of todos instead of an error message.
Log Levels¶
Recall above that the LOG_LEVEL
environment variable in the Docker Compose file will be inherited from the environment where the service is started if available.
You can bring your services up with
LOG_LEVEL=trace docker-compose up app
to get trace
level logging (the most granular). You can use this environment variable to set the logging to any available level.
All Service Logs¶
If you explicitly specify your database service when you bring containers up then you will see logs for both your database and your app.
docker-compose up app db
Bringing Standalone Containers Down¶
Now that you've got containers running "detached" from your host shell, you need to tell them to shut down somehow. It's worth knowing that any running container can be asked to shut down with
docker container stop <container_id>
but the easiest way to bring these particular containers down is
docker-compose down
Wiping The Database¶
The Docker Compose file defines a db_data
volume to persist your database between runs. There are a couple of ways to reset your database.
You can remove the db_data
volume at the same time as bringing your containers down with
docker-compose down --volumes
You can see any volumes currently persisting data with docker volume ls
. Note that the volume name will generally have a prefix of my-dockerized-app_
or test_
depending on whether you were running in Swarm Mode or not.
You can remove these volumes one at a time with e.g.
docker volume rm my-dockerized-app_db_data
You can also clean up all volumes with
docker volume prune
Just be careful you don't accidentally prune a volume with data you wanted to keep around!
Docker will not let you remove volumes that are currently in use by running or stopped containers. You can get a list of running containers with docker container ls
and you can see stopped containers as well with docker container ls -a
.
Swarm Mode¶
Swarm Mode is an easy interface to use when you've got a Docker Compose file handy and you want to test how your app scales horizontally. You can read all about Swarm Mode in the pages rooted at the overview.
The first thing we need is a manager node for our Swarm. Run
docker swarm init
Next we will use our Docker Compose file to bring up a stack named 'test'
containing our services
docker stack deploy -c docker-compose.yml test
We can see how our services are doing with
docker service ls
You should expect to see 1/1
replicas for your app
and db
services and 0/0
replicas for your migrate
and revert
services.
We need to use a different command to run migrations in Swarm mode.
docker service scale --detach test_migrate=1
Note
We have just asked a short-lived service to scale to 1 replica. It will successfully scale up, run, and then exit. However, that will leave it with 0/1
replicas running. This is no big deal until we want to run migrations again, but we cannot tell it to "scale up to 1 replica" if that is already where it is at. A quirk of this setup is that the next time we want to run migrations within the same Swarm runtime, we need to first scale the service down to 0
and then back up to 1
.
The payoff for our trouble in the context of this short guide is that now we can scale our app to whatever we want in order to test how well it handles database contention, crashes, and more.
If you want to run 5 instances of your app concurrently, execute
docker service scale test_app=5
In addition to watching docker scale your app up, you can see that 5 replicas are indeed running by again checking docker service ls
.
You can view (and follow) the logs for your app with
docker service logs -f test_app
Bringing Swarm Services Down¶
When you want to bring your services down in Swarm Mode, you do so by removing the stack you created earlier.
docker stack rm test
Production Deploys¶
As noted at the top, this guide will not go into great detail about deploying your dockerized app to production because the topic is large and varies greatly depending on the hosting service (AWS, Azure, etc.), tooling (Terraform, Ansible, etc.), and orchestration (Docker Swarm, Kubernetes, etc.).
However, the techniques you learn to run your dockerized app locally on your development machine are largely transferable to production environments. A server instance set up to run the docker daemon will accept all the same commands.
Copy your project files to your server, SSH into the server, and run a docker-compose
or docker stack deploy
command to get things running remotely.
Alternatively, set your local DOCKER_HOST
environment variable to point at your server and run the docker
commands locally on your machine. It is important to note that with this approach, you do not need to copy any of your project files to the server but you do need to host your docker image somewhere your server can pull it from.