Develop a Python Flask Application with Docker and Deploy it to AWS – Part 3 Bring it together with docker compose
Jan 19, 2019Recap
Up until this point we covered:
Now we will use docker-compose to do all these things, but do them even better and with style.
Get the Code
Clone the github repo to see the full code.
Turbo Charge your Docker Dev with Docker-Compose
What is Docker Compose?
Now, I'm going to discuss an incredibly powerful method of connecting and building our docker containers using docker-compose. Docker compose is simply a layer on top of docker that allows us to compose our containers functionality into services, along with additional features such as easily connecting our services and allowing them to talk to one another, declaring dependencies, such as don't bring up my web app until the database is up, and adding configuration parameters such as environmental variables, ports, and file mounts.
If you haven't already, install docker-compose by following the official instructions here. If you installed Docker Desktop for Mac or Windows there is no need for this, it's there already.
What amazing things can I do with docker-compose?
Because this is my blog and I CAN, I'm going to spend a bit of time discussing just how amazing using docker is to deploy your applications. You can use docker-compose to mix and match any number of technologies, from python or node applications, to databases, spark clusters, celery job queues, messaging systems, etc, all without needing to install or configure any of these technologies on your local computer. As a result deployment becomes a breeze (most of the time!), whether you are deploying to a remote server on digital ocean, an EC2 instance on AWS, or another cloud platform such as Google Cloud or Azure. No matter the destination the underlying concepts and technologies remain the same!
Enough talk! CODE!
For this particular application we will 'only' be deploying a single docker service. In later posts I hope to go over how to connect multiple services together.
version: '3' | |
services: | |
flask-app-server: | |
build: | |
context: flask_app | |
dockerfile: Dockerfile | |
ports: | |
- "5000:5000" | |
volumes: | |
- ./flask_app:/home/flask/flask_app:Z |
Docker Compose - Version
This is important. You MUST declare a version, and it must be have quotation marks around it. As of now the newest version is version 3. If you're trying to connect to any other APIs, such as deploying docker-compose directly on AWS, make sure that you are using a compliant version.
Docker Compose - Services
Now, this is where we can really start to build out our application. Each docker container becomes a service. Generally, each container encapsulates a very specific function. That could be a database, a web server, serve a tensorflow model, etc. The point is that your container has one domain, and it does that one thing very well.
In this instance our only service is our flask app. Notice how everything we did previously, building the container with a specific build context, exposing ports, and mounting directories, we declare right in the service. This is amazing because I hate remembering stuff, and I love having a single point of truth for my entire application.
Here we are creating a volume from our host computer to our container for development purposes, but sometimes you will simply want named volumes to persist data.
Docker Compose - Networks and Volumes
I'm not covering it here, but you can also specify networks and volumes. If you hate networking (like me!!!), you very rarely need to worry about this as docker-compose takes care of it for you, and creates a network for your services.
If you want to get more in depth with networking I suggest the official docs.
If you want to persist data, such as saving a database even when restarting the containers, you can use volumes. We are not persisting any data here, but it is a super handy feature I hope to go over later. Check out the official docs here.
Bring up our Flask App with Docker-Compose
This is where the magic happens! Once you have your application all ready, and you have your docker-compose.yml file ready, you can bring it all up with a single command.
That's it! It's not particularly impressive, because we only have 1 service, but imagine if we had 5, or 10. Instead of having to remember all the ports and commands and run each individually, we simply tell docker to remember it all for us.
Run the same curl command we did in the last post to ensure it is up and working correctly.
Also notice the -d flag, telling docker-compose to run this as a daemon in the background.
It should just spit '{"hello":"world"}' right back at you. If you want to go really crazy, make some changes to the flask app, write your own endpoints, and try it again.
Docker Compose Candy
Like I said earlier, docker-compose is a layer on top of docker. Everything you can do with docker, you can do with docker-compose.
As a quick aside, running all these commands is dependent upon staying in the same working directory, or its sub directories, as the docker-compose file
.
Docker-Compose: List
To see all current services running as a part of this docker-compose instance, run docker-compose images.
You will notice that we don't have to specify a tag for our services, it is already taken care of as ${current_working_directory}_${service_name}. In this example the current working directory is deploy-a-python-flask-app-to-aws, and our service name is flask-app-server. So our container is named deploy-a-python-flask-app-to-aws_flask-app-server. You will also notice a _N appended to each container. We can use docker to deploy multiple instances of the same container, but don't worry about that for now.
Try running docker images, and see what you get.
Docker-Compose: Run
Just as we ran an interactive shell in docker in Part 1, we can do that with docker-compose.
Now, this can be a bit tricky to work with, because we have to remember the super handy way docker-compose names our containers for us as ${current_working_directory}_${service_name}. When we want to run a command interactively using docker-compose, we only use the ${service_name}, because it already knows about the current working directory. If we want to run our command directly using docker, which normally we don't, we would use the full container name, ${current_working_directory}_${service_name}.
Run docker-compose exec flask-app-server-bash
Alternately, drop into the shell using just plain docker.
Docker-Compose: Restart
If, for some reason, you need to restart the server (you think your job queue is stuck, for instance), you can restart the entire docker-compose instance.
Docker-compose: Stop
If you want to stop your docker-compose instance, perhaps to shut off your computer or work on some other project, just run:
Docker-Compose: RM
CAUTION - DO NOT USE THESE COMMANDS UNLESS YOU ACTUALLY WANT TO REMOVE THE INSTANCE!
Wrap Up
Hopefully, now you have a good idea of: