This is a shortened version of a talk I gave to University of Warwick Computing Society.

Introduction

What is Docker?

From the official Docker webpage:

“[Docker lets you] package applications as portable container images to run in any environment consistently from on-premises Kubernetes to AWS ECS, Azure ACI, Google GKE and more”

Docker is a tool which lets you bundle your application up into a self-contained image which can run in many environments with exactly the same results.

Why should I use Docker?

Portability

Docker guarantees your code behaves in the same way no matter the environment. This means it completely eliminates the “it works on my machine” excuse!

Gives you freedom

Because Docker images describe the entire filesystem, you can install things to the execution environment without affecting your development environment. When you delete a container, all changes are removed. This means you can try out whatever you want without worrying about having to remove packages from your local machine.

Saves you time

I find working with a microservices-based architecture to involve lots of time spent launching different services. Docker-Compose (a tool to help manage docker) can build and start all your services with only one command!

When not to use Docker

The advantages of Docker make it sound like containers are a better version of virtual machines. But, Docker does not come with the security guarantees of VMs. If you truly want to isolate your software, VMs are best.

How to install Docker

Best place to look is Docker’s official website.

On Linux it’s a simple process, just run a convenience installer script. This will curl down everything and install it for you.

On Windows/macOS you need to install Docker Desktop. This not only enables Docker on your device but it also gives you a cool application that gives you a GUI for managing your containers!

Containers? Images?

Time for some terminology, I’ve mentioned both these terms before so let’s define them:

A Docker image is a combination of a file system and parameters. They encode everything you need to run the application. Crucially, they are stateless. Without configuration, data does not persist between “runs” of the same image. This is an important thing to consider if you are using a database in an image. We’ll go through how to persist data in demo 3.

A Docker container is the result of running an image. When you execute an image, it creates something called a container. The container is a process which you can interact with.

A helpful analogy is to think of an image as a class definition and containers as instances of the class. You are able to create multiple, identical containers from one image.

Dockerfiles

To build an image, we describe its creation in something called a Dockerfile. This is a step-by-step guide on how to create the environment you desire. It is a plain-text file containing special Docker commands.

Dockerizing a Flask app

For this short example, I will be using the Hello World example from the official Flask tutorial: [https://flask.palletsprojects.com/en/2.0.x/quickstart/#a-minimal-application](see here)

from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello_world():
		return "<p>Hello, World!</p>"

Before we can create a container for our Flask app, we need to build a image. To do this, we must create a Dockerfile. We name this Dockerfile and place it in our project’s root folder, note: the name has no file extension.

FROM python:3.7.5-alpine


RUN mkdir /app
WORKDIR /app


COPY requirements.txt requirements.txt


RUN pip install -r requirements.txt


COPY . .


CMD flask run --host=0.0.0.0 --port=5000

Docker will read this file from top to bottom so let’s work through the commands in that order:

FROM python:3.7.5-alpine This is probably the most complex command in the file. Here we are defining our base image. The base image is nothing but another Docker image. Using the FROM command lets us inherit the image specified. Here we are loading in an environment designed for Python, so we don’t need to install any Python related files. We can just start running the commands needed for our Python program. The -alpine flag specifies a Linux environment. Alpine is a very lightweight distribution of Linux, so our image will be a Linux environment. However, we are able to specify any of the operating systems specified with this image. [https://hub.docker.com/_/python](Click here to learn more about our base image).

RUN mkdir /app

The RUN command allows us to run any valid command in the base image’s operating system in our image. As we inherited from the Alpine Linux distribution, we can use RUN to execute any Linux commands. Here we are creating a new directory called /app. This is standard practice for Docker images - all of our application’s files are stored under ‘/app’.

WORKDIR /app

This commands functions somewhat like cd. This changes our work directory to the directory specified. This means all of our later commands are executed in this directory. So for our Dockerfile, all our commands will be executed in our /app directory.

COPY requirements.txt requirements.txt

COPY has the general structure: COPY <src> <dest> and will copy the source file or directories to the destination location given. In our Dockerfile, this will mean requirements.txt is copied to /WORKDIR/requirements.txt. As we have set WORKDIR to /app, our requirements.txt file will be in /app/requirements.txt as we set our WORKDIR to /app in the previous command.

RUN pip install -r requirements.txt

Next, we use a the RUN command for a second time. Now, we use it to install our application’s dependencies. This is a standard Python command and is what we would run on our local machine. By prepending RUN, we can run this on our Docker image.

COPY . .

Now the COPY command again. By specifying . as the source and destination, all of our project’s folder will be copied into the image. Again, all of our files will be copied into the /app directory.

CMD flask run --host=0.0.0.0 --port=5000

This command defines the entry point for our container. When the image is run as a container the CMD command is what will be executed. This is another standard Flask command, exposing the server on 0.0.0.0:5000. The port is important as we will be forwarding it when we run our container.

Docker Commands

Once we have defined our Dockerfile, we are ready to build it into an image. To do this, we run the command:

docker image build -t myapp .

docker image build tells Docker we want to build an image from a Dockerfile. The -t flag is tags images. If we didn’t tag our image, we would have to refer to it by its id - with tags, we can refer to the tag instead of the id. Here, we give the image the tag myapp. The final argument is the path to the Dockerfle. Using . means the Dockerfile is in the current directory.

The ouput of this command is a Docker image. This can now be ran as a container.

We run our image using another console command:

docker container run -it -p 5000:5000 -e FLASK_APP=app.py --rm --name myapp myapp

This is a long command so let’s break it down:

The first part of the command: docker container run tells Docker we want to run an image. We then have a long list of flags:

  • -it: this flag gives us more interactivity with the container and lets us stop it using control-c
  • -p 5000:5000: this flag binds a port from the Docker host to the container. For our app, requests to port 5000 will be forwarded to port 5000 on our container. We have already told Flask to run on port 5000 in our Dockerfile so this allows us to access our app from outside our container.
  • -e FLASK_APP=app.py: The -e flag sets an environment variable. Here, we define the environment variable FLASK_APP to be app.py. This is a standard Flask practice, but the flag is the same format for any language.
  • --rm: this tells the Docker to delete our container when we it stops running. Without this, the container would remain stopped on our system until we tell Docker to delete it. Using this flag helps reduce the number of unused containers.
  • --name myapp: this functions in a similar way to tagging images. Specifying --name allows us to refer to this container using its name, rather than using it’s ID. This helps simplify later commands.
  • myapp: the final command specifies which image we’d like to use in our container. Here we use the tag we defined in our build command.

Everything works!

After running that final command, we should see the container running in our terminal. When we navigate to localhost:5000 we should see Hello world!! This means our app is working perfectly.


Thanks for working through this tutorial, stay tuned for more Docker content