Docker Survival Guide for Python Developers

What will we cover?

You need to create your first docker image to run your python module in a docker container? In this tutorial you will learn how to do that.

Most get a bit scared first time they need to create their first docker image – it seems so complex. But don’t worry, in this guide you will learn how to build your python docker image which you can use to run your python module in a docker container.

Step 1: What is Docker?

Docker enables developers to easy deploy their applications in containers to run in a specific environment decided by you.

That might be a bit difficult to understand at first.

Take our Python API – we already know, that it does not run with Python 3.8. But we want to be able to deploy it different places without the trouble of having correct Python, libraries, environment variables etc. This is what Docker can do for us.

You can package Python 3.10 (in our case) with the correct libraries in a Linux OS.

What about this Linux, you might wonder. I don’t run Linux.

Neither do I – but I can run it in my Docker Daemon.

This is the beauty of it, you can run it on any machine or server with a Docker Daemon.

This actually solves a big problem. Imagine you also had another API running, but it only worked with Python 3.8. Your new does not work with 3.8.

But each of these API’s are running in each their package with each their environment.

Therefore, you can have it running isolated in their own containers (as they are called) – also, they can call each other endpoints (API’s) and communicate with each other.

You suddenly understand the power of Docker.

Step 2: Docker terminology

There can seem to be a lot terminology with Docker.

The good news is, you only need to understand the high-level concepts of how it works – the precise concepts can ease the communication, but most people understand you if you call a Dockerfile the Docker image.

The terminology used is as follows.

  • Dockerfile. The commands to create a Docker image.
  • Docker images. The blueprints of the containers.
  • Docker container. Created from Docker images and run the actual application – this is the running code.
  • Docker Daemon. The background service running on the host that manages building, running and distributing Docker container.
  • Docker Client (CLI). The command line tool that allows users to interact with the daemon.
  • Docker Hub. A registry of Docker images.
  • Docker Desktop. An easy to install UI including Docker Daemon and Docker client.

I think we will learn by doing, but in short you need the following.

  • Install Docker Desktop.
  • Create Dockerfile.
  • Compose a Docker image.
  • Deploy the image to run as a container in Docker Daemon.
  • Then know how to shut them down and restart.

Don’t worry too much about understanding all this now. You will learn it in this tutorial.

Step 3: Install Docker Desktop

You can install Docker Desktop and it will automatically install all you need to manage all Docker tools need.

Go to Docker official download and get Docker Desktop for your OS (here).

Follow the installation instructions and you are ready to go.

  • Linux Notice. Please notice that if you use Linux there is no Docker Desktop you will need to use command lines when we use the Desktop. For the most part we only use to see the containers and see what is running. This can be done from a command line as well.

You can launch Docker Desktop, which will start the Docker Daemon in the background.

Step 4: Explore a Python project with Docker

The best way to learn is to try it on a real project. To do that, I suggest you clone the follow ing project (here)

If you want to understand the code in the project, I suggest you read this tutorial.

If you want to understand the logging read this one.

And if you want to understand the tox testing framework you should read this one.

Here we will explore the Dockerfile in the project.

FROM python:3.10-bullseye
COPY requirements.txt .
RUN pip install -r requirements.txt
RUN mkdir /src
COPY . /src
WORKDIR /src
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

On the first line we take the base image, which is the most recent Python 3.10 version, called python:3.10-bullseye.

  • If you at a later stage want to user other Python versions in a Docker image, you can find them here: https://hub.docker.com/_/python
  • Notice, that some are early alpha releases and it is not recommended to use them unless you know what you are doing.

 The base image is like the foundation of what you are running it on. This is a prebuild image and contains all the standard libraries for the Python 3.10 release.

  • It should be clear that whatever you will run from a container created by this image, cannot access anything except what is provided by this image and what we also add to it.

This makes the next line understandable.

COPY requirements.txt .

This will copy the requirements.txt file to the Docker image.

Notice that we need the requirement.txt to be in same folder as the Dockerfile where we will build the image.

RUN pip install -r requirements.txt

The next line will run a command: pip install -r requirements.txt.

Remember what that did?

Yes, you are right, it installs all the requirements.

Notice, that the RUN will execute this command inside the Docker image, and hence, the installation is inside the Docker image.

RUN mkdir /src

This will create a folder /src inside the Docker image.

COPY . /src

This will copy all the files to the Docker image.

Now you might wonder about one thing?

  • Why first copy requirements.txt and install it and then copy everything thing afterwards?

This has something to do with how Docker images are created. And, yes, you could do it, but I will tell why the approach in the Dockerfile might be good.

Docker images are built in layers. And when you re-build an image, it will only build from the layer there has been changes.

Each line in the Dockerfile represents a layer.

Now consider this.

  • Imagine you already have a built image of the Dockerfiler
  • Then you make a change to app/main.py file.
  • What happens?

This change does not affect the requirements.txt file. Hence the installation of all the required libraries will not be executed again. It will start from the COPY . /src command.

You see, it will save time the next time you build your image.

WORKDIR /src

This will set the working folder (or directory). This means, when you execute a command, it will run from this folder.

EXPOSE 8000

This will inform Docker that the container will listen to that specified network port.

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

Finally, the CMD is the command.

This can seem a bit confusing. But it takes the list of arguments and runs the command uvicorn with all the arguments.

uvicorn app.main:app –host 0.0.0.0 –port 8000

You might wonder.

  • Why don’t we run python server.py?
    • We could, but we will use the uvicorn instead here.

Now we have some basic understanding of the Dockerfile, we are ready to build the Docker image.

Step 5: Build and run the Docker image

Before you build your first Docker image, you need to ensure that the Docker Desktop is running.

This is needed to have the Docker daemon running.

Now run the command: docker build -t fruit-api .

Notice the space and dot at the end of the command.

Then it will start making a lot of stuff. You need to wait until it finishes.

[+] Building 16.3s (12/12) FINISHED                                                                                                                                                                                     
 => [internal] load build definition from Dockerfile                                                                                                                                                               0.0s
 => => transferring dockerfile: 257B                                                                                                                                                                               0.0s
 => [internal] load .dockerignore                                                                                                                                                                                  0.0s
 => => transferring context: 2B                                                                                                                                                                                    0.0s
 => [internal] load metadata for docker.io/library/python:3.10-bullseye                                                                                                                                            2.5s
 => [auth] library/python:pull token for registry-1.docker.io                                                                                                                                                      0.0s
 => [1/6] FROM docker.io/library/python:3.10-bullseye@sha256:8846e26238945dc748151dc3e0f4e9a00bfd09ad2884edcc6e739f085ad9db3b                                                                                      0.8s
 => => resolve docker.io/library/python:3.10-bullseye@sha256:8846e26238945dc748151dc3e0f4e9a00bfd09ad2884edcc6e739f085ad9db3b                                                                                      0.0s
 => => sha256:cee26ee24819941bed15229baeb8e437866a7864432a6b15d1bfbd5da70cee09 2.22kB / 2.22kB                                                                                                                     0.0s
 => => sha256:ace6d68f6bea726cb6f2aa5a02cc799df1902deef9a94b362fc6a23ff8a69efc 3.04MB / 3.04MB                                                                                                                     0.4s
 => => sha256:8846e26238945dc748151dc3e0f4e9a00bfd09ad2884edcc6e739f085ad9db3b 1.86kB / 1.86kB                                                                                                                     0.0s
 => => sha256:b62e4294564cd2bdf0693cbd786338d15734a6fbee280faa976c23f3ae60f636 8.53kB / 8.53kB                                                                                                                     0.0s
 => => extracting sha256:ace6d68f6bea726cb6f2aa5a02cc799df1902deef9a94b362fc6a23ff8a69efc                                                                                                                          0.3s
 => [internal] load build context                                                                                                                                                                                  6.4s
 => => transferring context: 46.05MB                                                                                                                                                                               5.3s
 => [2/6] COPY requirements.txt .                                                                                                                                                                                  0.2s
 => [3/6] RUN pip install -r requirements.txt                                                                                                                                                                      6.3s
 => [4/6] RUN mkdir /src                                                                                                                                                                                           0.2s 
 => [5/6] COPY . /src                                                                                                                                                                                              0.3s 
 => [6/6] WORKDIR /src                                                                                                                                                                                             0.0s 
 => exporting to image                                                                                                                                                                                             0.4s 
 => => exporting layers                                                                                                                                                                                            0.3s 
 => => writing image sha256:8842602312874f7a1c289a2f6a47e5b341033ee4a447ed73381c06df1d44421e                                                                                                                       0.0s 
 => => naming to docker.io/library/fruit-api                                                                                                                                                                       0.0s

It tells you to use docker scan. This is beyond the scope of what we will cover and requires you create a login.

The image is created from the official Python image and the files in your project.

Now where is the image?

Look in Docker Dashboard (Docker Desktop) and click images and search for the fruit-api.

Now there is your image.

Can you access it?

It is not running. Remember, you need to launch it in a Docker container.

  • Run the command: docker run -dp 8000:8000 fruit-api
    • The -d will detach it from the command line, this is convenient as it will occupy the terminal until you finish the container.
    • The p part (of -dp) will publish the port 8000:8000 on host from container.

The name (beautiful_diffie) will most likely be different in your case. This is simply a random name.

If you click on it you will see the output.


To call it, you can run make_order.py file.

Then you should see it in the log in Docker Desktop.

Step 6: Stopping your Docker container

When you want to shut down your Docker container – that is, you don’t want to have the service running on port 8000, what do you do?

If you hover the mouse over you will see the following options.

It has a stop button (and some more).

If you click the stop button, it will stop.

  • Don’t believe me? Try to stop it and run make_order.py and it will fail.

Then you can start it again with the RUN button (the STOP will change to a RUN button).

  • Try it and run make_order.py and see it works again.

Step 7: How to make changes to your API running in Docker

Right now, you have built your Docker image and you can run it in a Docker container.

If you make changes to the code in PyCharm, those changes will not be reflected if you run the container we have available.

To make changes you need to build a new image and run that image as a container.

Let’s try that.

Make the following change in app/routers/order.py

import logging
from http import HTTPStatus
from typing import Dict
from fastapi import APIRouter
logger = logging.getLogger(__file__)
router = APIRouter(tags=['income'])

@router.post('/order', status_code=HTTPStatus.OK)
async def order_call(order: str) -> Dict[str, str]:
    logger.info(f'Incoming order received: {order}')
    return {'Order Received': order}

Let’s make this simple change to line 14.  Change it to return {‘Order received’: order}

If you start the Docker container, you will see it is the old version running.

To be clear, you can modify make order a bit to print the response.

As you see – it returns the old response we had.

What to do?

  1. Stop the container.
    1. You can do that as we saw with the STOP button in Docker Dashboard.
    1. Or run the command: docker stop eloquent_hawking
      1. Of course, you need to change the name after stop to fit with the name it gave your instance of the container.
  2. Delete the container.
    1. This can also be done from Docker Dashboard (use the DELETE button – hiding under the symbol).
  3. Or run the command: docker container rm eloquent_hawking
    1. Again, you need to change it to your containers name.
  4. Build the image again.
    1. Run docker build -t fruit-api .
    1. This is done in the terminal.
    1. Notice, that it will be faster, as it does not need to build all layers.
  5. Run the image
    1. Run the command: docker run -dp 8000:8000 fruit-api
    1. This could also be done from Docker Dashboard.
      1. Go to images
      1. Find the image
      1. Run it and remember to publish the port.

Now make an order (run make_order.py)

And it prints our changes

Want to learn more?

Get my book that will teach you everything a modern Python cloud developer needs to master.

Learn how to create REST API microservices that generate metrics that allow you to monitor the health of the service.

What does all that mean? 

Don’t wait, get my book and master it and become one of the sought after Python developers and get your dream job.

Leave a Reply

%d bloggers like this: