Vai al contenuto

Docker Deploys

Using Docker to deploy your Vapor app has several benefits:

  1. 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.
  2. You can use docker-compose or Kubernetes manifests to orchestrate multiple services needed for a full deployment (e.g. Redis, Postgres, nginx, etc.).
  3. 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.

  1. Install or build the Vapor Toolbox (macOS, Linux).
  2. 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 as db. This means your app will access the database at http://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 name db to the service named 'db'.
  • The CMD directive in the Dockerfile is overridden in some services with the command array. Note that what is specified by command is run against the ENTRYPOINT in the Dockerfile.
  • In Swarm Mode (more on this below) services will by default be given 1 instance, but the migrate and revert services are defined as having deploy 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.