In May 2021, over 80,000 developers participated in StackOverFlow Annual Developer survey. Python traded places with SQL to become the third most popular language. Docker is a containerization tool used for spinning up isolated, reproducible application environments. It is a popular development tool for Python developers. The tutorials and articles here will teach you how to include Docker to your development workflow and use it to deploy applications locally and to the cloud.
If you're a Python developer and want to get started with Docker, I highly recommend you to first start with the basics of Docker. Docker Labs is one of the most popular resource that was developed by Collabnix community members. You can complete the Docker101 track before you start with the below instructions.
We will be leveraging Docker Desktop for most of the tutorials and examples below. Follow the below steps to install Docker Desktop in your local laptop.
Installing Docker Desktop
Let us quickly install Docker Desktop. You can refer this link to download Docker Desktop for Mac. Once you install Docker Desktop, go to Preference tab as shown in the image to make the necessary changes based on your system availability(optional).
You can use Homebrew to install Python in your system
brew install python
Create the app.py file with the following content:
from flask import Flask app = Flask(__name__) @app.route("/") def hello(): return "Hello World!" if __name__ == "__main__": app.run(host='0.0.0.0') # Why you should run it at 0.0.0.0 # https://stackoverflow.com/questions/30323224/deploying-a-minimal-flask-app-in-docker-server-connection-issues
Now that we have our server, let’s set about writing our Dockerfile and constructing the container in where our newly born Python application will live.
Create a Dockerfile with following content:
FROM python:3.8-alpine RUN mkdir /app ADD . /app WORKDIR /app RUN pip install -r requirements.txt CMD ["python", "app.py"]
Now that we have defined everything we need for our Python application to run in our Dockerfile we can now build an image using this file. In order to do that, we’ll need to run the following command:
$ docker build -t my-python-app . Sending build context to Docker daemon 5.12kB Step 1/6 : FROM python:3.8-alpine ---> d4953956cf1e Step 2/6 : RUN mkdir /app ---> Using cache ---> be346f9ff24f Step 3/6 : ADD . /app ---> eb420da7413c Step 4/6 : WORKDIR /app ---> Running in d623a88e4a00 Removing intermediate container d623a88e4a00 ---> ffc439c5bec5 Removing intermediate container 15805f4f7685 ---> 31828faf8ae4 Step 5/6 : CMD ["python", "app.py"] ---> Running in 9d54463b7e84 Removing intermediate container 9d54463b7e84 ---> 3f9244a1a240 Successfully built 3f9244a1a240 Successfully tagged my-python-app:latest
We can now verify that our image exists on our machine by typing docker images:
$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE my-python-app latest 3f9244a1a240 2 minutes ago 355MB$ docker images
In order to run this newly created image, we can use the docker run command and pass in the ports we want to map to and the image we wish to run.
$ docker run -p 8000:5000 -it my-python-app
-p 8000:5000- This exposes our application which is running on port 8081 within our container on http://localhost:8000 on our local machine.
-it- This flag specifies that we want to run this image in interactive mode with a tty for this container process.
my-python-app- This is the name of the image that we want to run in a container.
Awesome, if we open up http://localhost:8000 within our browser, we should see that our application is successfully responding with
You’ll notice that if we
ctrl-c this within the terminal, it will kill the container. If we want to have it run permanently in the background, you can replace -it with
-d to run this container in detached mode.
In order to view the list of containers running in the background you can use docker ps which should output something like this:
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 70fcc9195865 my-python-app "python app.py" 5 seconds ago Up 3 seconds 0.0.0.0:8080->8081/tcp silly_swirles
Dockerize Multi-Container Python App Using Compose
Learn how to containerize Python Application Using Docker Compose
The app fetches the quote of the day from a public API hosted at http://quotes.rest then it caches the result in Redis. For subsequent API calls, the app will return the result from Redis cache instead of fetching it from the public API
create following file structure :
python-docker-compose ↳ app.py
from flask import Flask from datetime import datetime import requests import redis import os from dotenv import load_dotenv import json load_dotenv() # take environment variables from .env. app = Flask("app") def get_quote_from_api(): API_URL = "http://quotes.rest/qod.json" resp = requests.get(API_URL) if resp.status_code == 200: try: quote_resp = resp.json()["contents"]["quotes"]["quote"] return quote_resp except (KeyError, IndexError) as e: print (e) return None else: return None @app.route("/") def index(): return "Welcome! Please hit the `/qod` API to get the quote of the day." @app.route("/qod") def quote_of_the_day(): # get today's date in string date = datetime.now().strftime("%Y-%m-%d") quote = redis_client.get("date") if not quote: quote = get_quote_from_api() return "Quote of the day: " + quote if __name__ == '__main__': # Connect to redis client redis_host = os.environ.get("REDIS_HOST", "localhost") redis_port = os.environ.get("REDIS_PORT", 6379) redis_password = os.environ.get("REDIS_PASSWORD", None) redis_client = redis.StrictRedis(host=redis_host, port=redis_port, password=redis_password) # Run the app app.run(port=8080, host="0.0.0.0")
git clone https://github.com/docker-community-leaders/dockercommunity/ cd /content/en/examples/Python/python-docker-compose pip install -r requirements.txt python app.py
On a different terminal run
$ curl http://localhost:8080/qod The free soul is rare, but you know it when you see it - basically because you feel good, very good, when you are near or with them.
# Dockerfile References: https://docs.docker.com/engine/reference/builder/ # Start from python:3.8-alpine base image FROM python:3.8-alpine # The latest alpine images don't have some tools like (`git` and `bash`). # Adding git, bash and openssh to the image RUN apk update && apk upgrade && \ apk add --no-cache bash git openssh # Make dir app RUN mkdir /app WORKDIR /app COPY requirements.txt requirements.txt RUN pip install -r requirements.txt # Copy the source from the current directory to the Working Directory inside the container COPY . . COPY . . # Expose port 8080 to the outside world EXPOSE 8080 # Run the executable CMD ["python", "app.py"]
Our application consists of two services -
- App service that contains the API to display the “quote of the day”.
- Redis which is used by the app to cache the “quote of the day”.
Let’s define both the services in a
# Docker Compose file Reference (https://docs.docker.com/compose/compose-file/) version: '3' # Define services services: # App Service app: # Configuration for building the docker image for the service build: context: . # Use an image built from the specified dockerfile in the current directory. dockerfile: Dockerfile ports: - "8080:8080" # Forward the exposed port 8080 on the container to port 8080 on the host machine restart: unless-stopped depends_on: - redis # This service depends on redis. Start that first. environment: # Pass environment variables to the service REDIS_HOST: redis REDIS_PORT: 6379 networks: # Networks to join (Services on the same network can communicate with each other using their name) - backend # Redis Service redis: image: "redis:alpine" # Use a public Redis image to build the redis service restart: unless-stopped networks: - backend networks: backend:
$ docker-compose up Starting python-docker-compose_redis_1 ... done Starting python-docker-compose_app_1 ... done Attaching to python-docker-compose_redis_1, python-docker-compose_app_1 redis_1 | 1:C 02 Feb 2019 12:32:45.791 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo redis_1 | 1:C 02 Feb 2019 12:32:45.791 # Redis version=5.0.3, bits=64, commit=00000000, modified=0, pid=1, just started redis_1 | 1:C 02 Feb 2019 12:32:45.791 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf redis_1 | 1:M 02 Feb 2019 12:32:45.792 * Running mode=standalone, port=6379. redis_1 | 1:M 02 Feb 2019 12:32:45.792 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128. redis_1 | 1:M 02 Feb 2019 12:32:45.792 # Server initialized redis_1 | 1:M 02 Feb 2019 12:32:45.792 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled. redis_1 | 1:M 02 Feb 2019 12:32:45.793 * DB loaded from disk: 0.000 seconds redis_1 | 1:M 02 Feb 2019 12:32:45.793 * Ready to accept connections app_1 | 2019/02/02 12:32:46 Starting Server
The docker-compose up command starts all the services defined in the
docker-compose.yml file. You can interact with the python service using curl -
$ curl http://localhost:8080/qod A show of confidence raises the bar
$ docker-compose down