Deploy a Python Web App to Azure Container Apps with Docker and GitHub Actions

Deploy a Python Web App to Azure Container Apps with Docker and GitHub Actions EduTechBuddy.com © edutechbuddy.com
Deploy a Python Web App to Azure Container Apps with Docker and GitHub Actions © edutechbuddy.com
Learn how to build a simple Python web app, package it with Docker, deploy it to Azure Container Apps, and set up automated deployment using GitHub Actions. This step-by-step guide covers essential commands, troubleshooting, and practical workflow tips for beginners.

Getting a Python web application running in the cloud can trip up beginners, especially when Docker, Azure Container Apps, and GitHub Actions all come into play. This guide lays out the steps for building a minimal Python web server, packaging it in a container, deploying to Azure, and wiring up a CI/CD pipeline for automated updates. It also flags common mistakes and practical fixes that help new developers grasp both the workflow and the core ideas behind it.

Microsoft positions Azure Container Apps as a general-purpose, serverless container platform that runs containerized workloads without requiring users to manage the orchestration layer. The platform focuses on containerized app deployment instead of the usual virtual-machine setup, so a Docker image fits as the packaging boundary for a small Python HTTP service. Microsoft Learn training points to several configuration areas for container apps: application creation, environment variables, ingress, scaling, instance management, and security. In this setup, the Python process runs the app, Docker creates the immutable runtime, and Azure Container Apps handles hosting and scaling controls.

Setting Up the Project and Repository

Begin by making a project directory and initializing a Git repository. Run these commands in a Linux-style terminal:

mkdir -p ~/shipit-app && cd ~/shipit-app
git init -b main

This sets up the project folder and a Git repository with main as the default branch. Naming matters—if you follow a guided lab, match the expected repository name to avoid headaches later in deployment. Microsoft documentation points out that the generated pipeline runs on pushes to main by default, so keeping your branch naming in sync with workflow triggers is operationally important.

Writing a Minimal Python Web App

The sample app uses only Python's standard library, so you don't need extra dependencies. Save this code as app.py:

import os
from http.server import BaseHTTPRequestHandler, HTTPServer

MESSAGE = "Hello from ShipIt Weekend!"

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        body = f"\n\n# {MESSAGE}\n\nBuilt with Docker, shipped to Azure, automated with GitHub Actions.\n\n".encode()
        self.send_response(200)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

HTTPServer(("0.0.0.0", int(os.environ.get("PORT", "8080"))), Handler).serve_forever()

This server listens on port 8080 or the value of the PORT environment variable, responding to every request with a simple HTML page. Microsoft Learn training notes that ingress is a separate configuration area for Azure Container Apps, and binding to 0.0.0.0 with an environment-controlled port matches container deployment best practices. Binding only to localhost would block traffic from reaching the app through the container network. Environment variables are a managed configuration feature, so reading PORT from the environment instead of hard-coding every runtime setting is the right move.

Containerizing the App with Docker

Add a Dockerfile to your project directory:

FROM python:3.12-slim
WORKDIR /app
COPY app.py .
ENV PORT=8080
EXPOSE 8080
USER nobody
CMD ["python", "app.py"]

This setup uses a slim Python base image, copies the app into the container, sets and exposes the port, and runs the app as an unprivileged user for better security. Microsoft documentation draws a line between a code deployment, where source is uploaded and built remotely, and a container deployment, where a Dockerfile builds and deploys an image. Choosing Python doesn't automatically mean Docker is used, and picking Docker doesn't remove the need to define runtime startup behavior. The Dockerfile, listening port, startup command, registry image, and Azure ingress settings all need to line up with each other.

Building and Testing Locally

Build and run the container locally to check that everything works:

docker build -t shipit-app .
docker run -d --rm -p 8080:8080 --name shipit shipit-app
curl http://localhost:8080
docker stop shipit

The curl command should return the HTML heading from your app, showing the container is working as intended.

Pushing to GitHub

Add your files, commit, and push to a new GitHub repository. If you use SSH, the commands are:

git add app.py Dockerfile
git commit -m "feat: containerised web app"
git remote add origin git@github.com:rahimahisah17/shipit.git
git push -u origin main

Make sure the repository name matches what automated checks or deployment scripts expect. If names don't match, you could run into issues during credential verification or automated deployment.

Publishing the Docker Image to GitHub Container Registry

Set your GitHub username as an environment variable and add a label to the Docker image for repository linkage:

export GH_USER=rahimahisah17
echo "LABEL org.opencontainers.image.source=https://github.com/$GH_USER/shipit" >> Dockerfile
git commit -am "chore: link image to repository" && git push

Log in to GitHub Container Registry (GHCR) using a personal access token with write:packages scope:

echo YOUR_TOKEN | docker login ghcr.io -u $GH_USER --password-stdin

Build, tag, and push the image to GHCR:

docker build -t ghcr.io/$GH_USER/shipit-app:v1 .
docker push ghcr.io/$GH_USER/shipit-app:v1

After pushing, set the package to public in the GitHub UI so Azure can pull it without authentication.

Deploying to Azure Container Apps

Sign in to Azure and get your subscription ready:

az login --use-device-code
az extension add --name containerapp --upgrade
az provider register --namespace Microsoft.App --wait
az provider register --namespace Microsoft.OperationalInsights --wait

Create a resource group for your deployment:

az group create --name shipit-rg --location westeurope

Deploy the container image to Azure Container Apps:

az containerapp up \
  --name shipit-app \
  --resource-group shipit-rg \
  --location westeurope \
  --image ghcr.io/$GH_USER/shipit-app:v1 \
  --ingress external \
  --target-port 8080

This command creates the environment, workspace, and app. Deployment can take several minutes. When it's done, Azure gives you a public URL for your app. Microsoft Learn documentation points out that scaling and instance management are part of the platform's configuration, so app availability depends on both the Python server and correct ingress and replica settings.

Testing the Live Deployment

Get the app's public URL and test it:

export APP_URL=https://$(az containerapp show --name shipit-app --resource-group shipit-rg --query properties.configuration.ingress.fqdn -o tsv)
echo $APP_URL
curl $APP_URL

If you see your app's HTML response, the deployment worked.

Automating Deployment with GitHub Actions

To set up continuous deployment, create a service principal in Azure with contributor access limited to your resource group. Microsoft Learn documentation explains that the pipeline setup uses a service principal for CI/CD authentication, not an individual developer's interactive login. This method provides non-interactive credentials with permissions scoped to deployment resources, and repository secrets and variables are set up as part of the pipeline. Authentication material and environment-specific values should stay out of application source code and Dockerfiles.

export SUB_ID=$(az account show --query id -o tsv)
az ad sp create-for-rbac --name shipit-github --role contributor \
  --scopes /subscriptions/$SUB_ID/resourceGroups/shipit-rg --json-auth

Save the resulting JSON as a GitHub secret named AZURE_CREDENTIALS in your repository settings. Reviewing generated permissions, protecting the deployment branch, rotating credentials when needed, and avoiding printing secret values in workflow logs are recommended operational safeguards.

Next, add a GitHub Actions workflow in .github/workflows/deploy.yml to build, push, and deploy the image on every push to main:

name: deploy
on:
  push:
    branches: [main]
  workflow_dispatch:
permissions:
  contents: read
  packages: write
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Log in to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - name: Build and push the image
        run: |
          IMAGE=ghcr.io/${GITHUB_REPOSITORY_OWNER,,}/shipit-app:${GITHUB_SHA::7}
          docker build -t "$IMAGE" .
          docker push "$IMAGE"
          echo "IMAGE=$IMAGE" >> "$GITHUB_ENV"
      - name: Log in to Azure
        uses: azure/login@v2
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}
      - name: Deploy to Azure Container Apps
        run: az containerapp update --name shipit-app --resource-group shipit-rg --image "$IMAGE"

This workflow builds a new image for each commit, pushes it to GHCR, and updates the Azure Container App. Microsoft Learn documentation details that the azd pipeline config process detects the Git provider, creates a service principal for CI/CD authentication, sets up repository secrets and variables, and generates a workflow file such as .github/workflows/azure-dev.yml. The pipeline uses azd deploy to build the container, push it to Azure Container Registry, and create a new deployed version. The workflow needs consistent image naming, registry authentication, and a deployment reference that points Azure to the newly built image instead of an older tag. If you hit a permission_denied: write_package error, grant your repository write access to the package in the GitHub UI under Package settings.

Common Troubleshooting and Lessons Learned

  • If environment variables like GH_USER are unset in a new terminal, re-export them before running commands.
  • GHCR packages are private by default; set them to public so Azure can pull images.
  • Deployment to Azure Container Apps may take several minutes. Avoid interrupting the process to prevent incomplete resources.
  • Repository naming matters for automated credential checks. If a credential claim fails, verify that your repository name matches the expected value.

When finished, clean up by deleting the Azure resource group, the service principal, and any secrets or tokens you created. This avoids unnecessary charges and keeps your credentials secure.

Summary

This workflow shows how to build, containerize, and deploy a Python web app to Azure Container Apps, then automate updates with GitHub Actions. By following these steps, beginners get hands-on experience with Docker, cloud deployment, and CI/CD pipelines using real-world tools and workflows. For more on Azure Container Apps and automation, see Microsoft Learn's official documentation.

Camila Ferreira Editor EduTechBuddy.com
Co-Founder

Camila Ferreira

Camila Ferreira is the Co-Founder of EduTechBuddy, where her editorial focus is programming education and practical software development skills. Her primary subjects include Python programming, Git version control, and the fundamental concepts beginners need to understand when working with code.