How to Containerize Python Web Applications

Updated on July 25, 2024
How to Containerize Python Web Applications header image

Introduction

Containerizing Python web applications encapsulates the application structure and dependencies into a single package to ensure consistent and reproducible deployments across different environments. Deploying containerized Python web applications is a fundamental approach to modern software development that involves scaling and automation when deploying to platforms such as Kubernetes.

You can build Python applications using different development frameworks. Each platform may require a slightly different approach to containerize and ship the application image. Popular Python frameworks include:

  • Flask: A lightweight, flexible web framework that provides essential tools for building web applications. It's minimalistic and offers direct tools that allow developers to add libraries based on the specific project needs.
  • Django: A high-level full-stack web framework that provides a complete set of tools and libraries for building robust web applications.
  • FastAPI: A modern, high-performance web framework for building APIs with Python. It leverages type hints to enable auto-generated interactive API documentation (using Swagger UI) and provides high performance using Python's type system.
  • Pyramid: A flexible and scalable framework that allows developers to choose the components they need for their applications.
  • Tornado: An asynchronous networking library and web framework primarily known for its high performance and ability to handle many concurrent connections.

This article explains how to containerize Python web applications for deployment in production environments such as Kubernetes clusters.

Prerequisites

Before you begin:

Create a Python Web Application

To create a consistent Python web application image that you can use with different frameworks such as Flask or Django, structure your code to separate the frontend HTTP server interface logic from the main application logic. Follow the steps below to create a basic Python application using app.py as the main application file while using a server.py file to include the basic HTTP server logic.

  1. Install the Python virtual environment and the PIP package manager.

    console
    $ sudo apt install python3-venv python3-pip -y
    
  2. Create a new project directory to store the application files.

    console
    $ mkdir python-app
    
  3. Switch to the directory.

    console
    $ cd python-app
    
  4. Create a new Python virtual environment to separate the application dependencies from other system packages.

    console
    $ python3 -m venv python-env
    
  5. Activate the virtual environment.

    console
    $ source python-env/bin/activate
    
  6. Create the main application file app.py using a text editor such as Nano.

    console
    (python-env)$ nano app.py
    
  7. Add the following code to the file.

    python
    def hello_world():
     print("Hello, World!")
    
    if __name__ == "__main__":
     hello_world()
    

    Save and close the file.

    The above code creates a basic Python web application that displays a Hello, World! prompt. Within the application:

    • The hello_world function prints the Hello, World! string when called.
    • if __name__ == "__main__":: Checks if the script runs as the main program. This ensures that certain code elements run when the script starts with the main thread. If the script is the main program, it calls the hello_world function.
  8. Create a new server.py file to set up a basic HTTP application using the Python http.server and socketserver modules.

    console
    (python-env)$ nano server.py
    
  9. Add the following code to the file.

    python
    from http.server import SimpleHTTPRequestHandler
    from socketserver import TCPServer
    from threading import Thread
    
    class HelloWorldHandler(SimpleHTTPRequestHandler):
     def do_GET(self):
         # Handle GET requests
         self.send_response(200)
         self.send_header("Content-type", "text/plain")
         self.end_headers()
         self.wfile.write(b"Hello, World!")
    
    def start_server():
     # Set up the HTTP server with custom handler
     server_address = ("", 8000)
     httpd = TCPServer(server_address, HelloWorldHandler)
    
     print("Server is running at http://localhost:8000")
    
     try:
         # Serve indefinitely
         httpd.serve_forever()
     except KeyboardInterrupt:
         # Handle keyboard interrupt to gracefully shut down the server
         print("Shutting down the server.")
         httpd.shutdown()
    
    if __name__ == "__main__":
     # Start the server in a separate thread
     server_thread = Thread(target=start_server)
     server_thread.start()
    
     # Run the main application
     from app import hello_world
     hello_world()
    
     # Wait for the server thread to finish
     server_thread.join()
    

    Save and close the file.

    The above application code responds to incoming GET requests with a Hello, World! prompt. Within the code:

    • HelloWorldHandler: Defines a custom handler class that inherits the SimpleHTTPRequestHandler HTTP module function to handle GET requests.
    • start_server: Creates a TCPServer instance running on the host port 8000. In addition, it captures KeyboardInterrupt exceptions Ctrl + C interrupts to gracefully shut down the server.
    • (if __name__ == "__main__":): Starts the HTTP server in a separate thread using the Thread(target=start_server) function. Then, it imports and calls a hello_world function from the app module. server_thread.join() waits for the server thread to complete before closing the connection.
  10. Start the Python application as a background task in your server session.

    console
    (python-env)$ python3 server.py &
    

    Output:

    Server is running at http://localhost:8000
  11. Send a GET request to the host port 8000 to verify access to the application.

    console
    (python-env)$ curl -X GET -H "Content-Type: application/json" http://localhost:8000
    

    Output:

    Hello, World!

    When the above GET request fails, verify that no conflicting applications run on the defined host port 8000.

  12. View the background application Job ID.

    console
    $ jobs
    

    Output:

    [1] python3 server.py &
  13. Stop the application by Job ID. For example 1.

    console
    (python-env)$ kill %1
    
  14. Deactivate your virtual environment.

    console
    (python-env)$ deactivate
    

Containerize the Python Web Application

  1. Create a new Dockerfile Dockerfile to set up your Python application container image.

    console
    $ nano Dockerfile
    
  2. Add the following contents to the file.

    Dockerfile
    FROM python:3.8-slim
    WORKDIR /app
    
    COPY . /app
    
    CMD ["python3", "./server.py"]
    

    Save and close the file.

    The above Dockerfile configuration defines the Python container directory and run-time structure. Within the configuration:

    • FROM python:3.8-slim: Uses the official Python 3.8 image as the base image to run the container.
    • WORKDIR /app: Sets the working directory to /app inside the container.
    • COPY . /app: Copies data from your project directory to the container.
    • CMD ["python3", "./server.py"]: Runs the server.py script when the container starts.
  3. Build the Docker image with all directory files.

    console
    $ docker build -t python-app .
    
  4. View the local Docker images to verify that the application is available.

    console
    $ docker images
    

    Output:

    REPOSITORY                         TAG         IMAGE ID       CREATED          SIZE
    python-app                               latest             86ff1e8041fe   19 seconds ago   180MB

Push the Python Application Image to the Vultr Container Registry

To store and use the Python application image on a platform such as Kubernetes, push the image to the Vultr Container Registry to ship it to your target environments as described in the steps below.

  1. Log in to your Vultr Container Registry. Replace pythonregistry, vcr-user, vcr-password with your actual registry details.

    console
    $ docker login https://sjc.vultrcr.com/pythonregistry -u vcr-user -p vcr-password
    
  2. Tag the local Docker image with your target Vultr Container Registry repository.

    console
    $ docker tag python-app:latest sjc.vultrcr.com/pythonregistry/python-app:latest
    
  3. View the list of available Docker images and verify that the registry image is available.

    console
    $ docker images
    

    Output:

    sjc.vultrcr.com/pythonregistry/python-app    latest             5594b65d7cd5   3 minutes ago   159MB
    python-app                                   latest             01acb92c461c   3 minutes ago   159MB
    python                                       3.8-slim           067655fb1c09   2 months ago    128MB
    
  4. Push your Python application image to the Vultr Container Registry.

    console
    $ docker push sjc.vultrcr.com/pythonregistry/python-app:latest
    

    Output:

    The push refers to repository [sjc.vultrcr.com/pythonregistry/python-app]
    1.2e1e26d6f: Pushed
    1.df21a05: Pushed
    b081d565ce24: Pushed
    e67a1acfc698: Pushed
    1.8551e70: Pushed
    da5d55102092: Pushed
    fb1bd2fc5282: Pushed
    latest: digest: sha256:4a523aab91202e4d79d9456672571ae855a7227590d2dd63de11498bf64f098c size: 1788
  5. Start a new Docker container using your registry container image to test access to the application.

    console
    $ docker run -dit -p 8000:8000 sjc.vultrcr.com/pythonregistry/python-app:latest
    
  6. View the list of running Docker containers to verify that your application container is available.

    console
    $ docker ps
    

    Output:

    CONTAINER ID   IMAGE               COMMAND                 CREATED         STATUS         PORTS                                       NAMES
    cfaf3ab6bfac   python-app:latest   "python3 ./server.py"   8 seconds ago   Up 7 seconds   0.0.0.0:8000->8000/tcp, :::8000->8000/tcp   jovial_brattain
  7. Send a new GET request to the host port 8000 to verify access to the application container.

    $ curl -X GET -H "Content-Type: application/json" http://127.0.0.1:8000

    Output:

    Hello, World!

Deploy the Python Application in a Kubernetes Cluster

To securely deploy your containerized Python application in a production environment such as a Kubernetes cluster, set up access to your Vultr Container Registry and install the application image as a new Deployment resource. When deployed, your application container listens for incoming connections on port 8000 as defined in the container configuration. Follow the steps below to deploy and use your Python application image in a Vultr Kubernetes Engine (VKE) cluster.

  1. Access your Vultr Container Registry control panel.

  2. Navigate to the Docker/Kubernetes tab, and click Generate Kubernetes YAML to generate a new registry Secret resource configuration.

    Generate a Vultr Container Registry Secret File

  3. Create a new Secret resource file secret.yaml.

    console
    $ nano secret.yaml
    
  4. Add your generated Vultr Container Registry YAML configuration to the file similar to the one below:

    apiVersion: v1
    kind: Secret
    metadata:
      name: vultr-cr-credentials
    data:
      .dockerconfigjson: example-json
    type: kubernetes.io/dockerconfigjson

    Save and close the file.

  5. Deploy the Secret to your cluster.

    console
    $ kubectl apply -f secret.yaml
    
  6. View the cluster resources to verify that your registry credentials are ready to use.

    console
    $ kubectl get secret
    

    Output:

    NAME                   TYPE                             DATA   AGE
    vultr-cr-credentials   kubernetes.io/dockerconfigjson   1      29m
  7. Create a new Deployment resource file deployment.yaml to describe how to run your application in a pod.

    console
    $ nano deployment.yaml
    
  8. Add the following configurations to the file. Replace sjc.vultrcr.com/pythonregistry/python-app:latest with your actual Vultr Container Registry repository.

    yaml
    kind: Deployment
    apiVersion: apps/v1
    metadata:
     name: python-app
    spec:
     replicas: 3
     selector:
       matchLabels:
         app: python-app
     template:
       metadata:
         labels:
           app: python-app
       spec:
         containers:
         - name: python-app
           image: sjc.vultrcr.com/pythonregistry/python-app:latest
           ports:
           - containerPort: 8000
         imagePullSecrets:
         - name: vultr-cr-credentials
    

    Save and close the file.

  9. Apply the deployment to your cluster.

    console
    $ kubectl apply -f deployment.yaml
    
  10. View the cluster deployments to verify that the new resource is available.

    console
    $ kubectl get deployment
    

    Output:

    NAME         READY   UP-TO-DATE   AVAILABLE   AGE
    python-app   3/3     3            3           21s
  11. View the cluster pods to verify all pods associated with your deployment.

    console
    $ kubectl get pods
    

    Output:

    NAME                          READY   STATUS    RESTARTS   AGE
    python-app-6cb76cfc8d-5wjbf   1/1     Running   0          35s
    python-app-6cb76cfc8d-tphrx   1/1     Running   0          35s
    python-app-6cb76cfc8d-txk5p   1/1     Running   0          35s

Securely Expose the Python Application for External Access

To Expose your Python application for external access using your Vultr Kubernetes Engine (VKE) cluster, install an Ingress controller to map your application service to a domain name. The controller securely routes all application requests from your domain name to the backend Python application service. Follow the steps below to expose the application in your VKE cluster.

  1. Add the NGINX ingress repository to your Helm sources.

    console
    $ helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
    
  2. Update the Helm repository index.

    console
    $ helm repo update
    
  3. Install the NGINX Ingress Controller to your cluster using Helm.

    console
    $ helm install nginx-ingress ingress-nginx/ingress-nginx --set controller.publishService.enabled=true
    
  4. Wait for at least 3 minutes for the Nginx Ingress Controller to install all necessary CRDs. Then, view the controller services to verify the assigned load balancer IP address.

    console
    $ kubectl get service --namespace default nginx-ingress-ingress-nginx-controller
    

    Output:

    NAME                                     TYPE           CLUSTER-IP     EXTERNAL-IP     PORT(S)                      AGE
    nginx-ingress-ingress-nginx-controller   LoadBalancer   10.98.94.149   192.0.2.100   80:32671/TCP,443:32325/TCP   2m58s

    Keep note of the Ingress controller EXTERNAL-IP value to externally access your cluster services.

  5. Set Up a new domain A record pointing to the Ingress controller public IP address. For example, app.example.com.

  6. Create a new Python Service resource file service.yaml

    console
    $ nano service.yaml
    
  7. Add the following contents to the file.

    yaml
    apiVersion: v1
    kind: Service
    metadata:
     name: python-app
    spec:
     selector:
       app: python-app
     ports:
       - protocol: TCP
         port: 80
         targetPort: 8000
    

    Save and close the file.

    The above Service configuration forwards all network requests on the HTTP port 80 to your Python application port 8000.

  8. Apply the Service to your cluster.

    console
    $ kubectl apply -f service.yaml
    
  9. Create a new Ingress resource file ingress.yaml to expose the Python application service.

    console
    $ nano ingress.yaml
    
  10. Add the following contents to the file. Replace app.example.com with your actual domain,

    yaml
    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      name: nginx-ingress
      namespace: default
      annotations:
        kubernetes.io/ingress.class: nginx
    spec:
      rules:
      - host: app.example.com
        http:
          paths:
          - backend:
              service:
                name: python-app
                port:
                  number: 80
            path: /
            pathType: Prefix
    

    Save and close the file.

    The above Ingress configuration forwards all root requests from app.example.com to your Python application service on port 80.

  11. Apply the Ingress resource to your cluster.

    console
    $ kubectl apply -f ingress.yaml
    
  12. View the cluster Ingress objects and verify that the new resource is available.

    console
    $ kubectl get ingress
    

    Output:

    NAME            CLASS   HOSTS              ADDRESS         PORTS   AGE
    nginx-ingress   nginx   app.example.com   192.0.2.100   80      32s
  13. Access your domain using a web browser such as Firefox to test access to the application.

    Python App Dashboard

Conclusion

You have containerized a Python application and stored it in a Vultr Container Registry repository for deployment in a VKE cluster. Shipping Python web applications as container images enables the efficient management and upgrade of application components for deployment in multiple environments.