Deploy a FastAPI App to Azure Container Apps

Containerize a Python REST API with Docker and deploy it to Azure.

Introduction

30 Second Summary

You have built an app that works perfectly on your machine. Now someone asks you to put it on the internet. Suddenly you need to think about packaging, environments, ports, and cloud services.

In this project, you will build a Python API, package it into a Docker container, and deploy it live to Azure Container Apps where anyone can reach it with a public URL.

What You'll Build

A book catalog REST API built with FastAPI, containerized with Docker, and deployed to Azure Container Apps with a public HTTPS endpoint.

By the end of this project, you'll have:

  • A working FastAPI application with Pydantic data validation, multiple endpoints, and a health check.
  • A production-style Dockerfile using layer caching, a slim base image, and a non-root user.
  • A live, publicly accessible API running on Azure Container Apps with an HTTPS URL.
  • A secret mission to add environment-based configuration with Pydantic Settings and a Docker Compose workflow.

Are there any prerequisites?

This project assumes medium-level familiarity with Python, Docker, and Azure. You should have Python 3, Docker Desktop, and Homebrew (macOS) installed on your machine. The project walks you through installing the Azure CLI in Step 1.

Before We Start

Before We Start...

Set Up Azure CLI and Verify Your Tools

Before you can deploy anything to Azure, you need the Azure CLI installed on your machine.

The Azure CLI is a command-line tool that lets you create, manage, and deploy Azure resources directly from your terminal. In this step, you will install it, log in to your Azure account, and confirm that Python and Docker are ready to go.

In this step, get ready to:
  • Install the Azure CLI and log in to your Azure account.
  • Install the Container Apps CLI extension and register required Azure providers.
  • Verify that Python and Docker are installed and running.
Install Azure CLI and log in

The Azure CLI is installed through Homebrew on macOS. This requires macOS 13 or higher.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open it.
  • Install the Azure CLI by running this command:
brew update && brew install azure-cli

What does this command do?

  • brew update refreshes Homebrew's list of available packages so you get the latest version.
  • brew install azure-cli downloads and installs the Azure CLI tool on your machine.

This installation can take a few minutes. Once it finishes, you should see a success message from Homebrew.

  • Verify the installation by running:
az version

You should see JSON output showing the Azure CLI version (e.g. 2.85.0 or newer).

Command not found?

  • Make sure Homebrew is installed. Run brew --version to check. If Homebrew is not installed, visit brew.sh for installation instructions.
  • Try closing and reopening your terminal, then run az version again.
  • Help me troubleshoot my Azure CLI installation.
  • Log in to your Azure account by running:
az login

What happens when you run this?

Your default web browser opens an Azure sign-in page. Sign in with your Azure account credentials, then return to your terminal.

After a successful login, your terminal displays JSON output listing your Azure subscriptions.

If you have multiple Azure subscriptions, you need to set the one you want to use for this project.

  • Check which subscription is active by running:
az account show --query "{name:name, id:id}" --output table
  • If you need to switch subscriptions, set the correct one by running:
az account set --subscription your-subscription-id-here

Pro tip: one subscription is fine

If you only have one Azure subscription, it is already selected automatically. You can skip the az account set command and move on.

Install the Container Apps extension and register providers

Azure Container Apps requires a CLI extension and two resource providers to be registered on your subscription before you can deploy.

  • Install the Container Apps CLI extension by running:
az extension add --name containerapp --upgrade

What does this command do?

This installs the containerapp extension, which adds Container Apps-specific commands to your Azure CLI. The --upgrade flag ensures you get the latest version if you already had an older one installed.

  • Register the two required Azure resource providers by running these commands:
az provider register --namespace Microsoft.App
az provider register --namespace Microsoft.OperationalInsights

What are resource providers?

Azure organizes its services into resource providers. Before you can use a service, its provider must be registered on your subscription.

  • Microsoft.App enables Azure Container Apps itself.
  • Microsoft.OperationalInsights enables the log analytics workspace that Container Apps uses for logging and monitoring.

Provider registration runs in the background and can take a minute or two. You do not need to wait for it to finish before continuing.

Seeing a permission error?

  • You need at least Contributor-level access on the Azure subscription to register providers. If you are using a work or school subscription, ask your administrator to register the providers for you.
  • Confirm you are logged in to the correct subscription by running az account show.
  • Help me fix my Azure provider registration error.
Verify Python and Docker

You should already have Python and Docker Desktop installed. Let's confirm both are working before moving on.

  • Check your Python version by running:
python3 --version

You should see a version number like Python 3.13.x (or any Python 3.10+ version).

I see a version number

Python is ready. Next, let's check Docker.

Command not found

Python 3 is not installed or not in your system path.

  • Download and install Python from python.org.
  • After installing, close and reopen your terminal.
  • Run python3 --version again to confirm it works.
  • Make sure Docker Desktop is running before you continue.
  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Docker Desktop and press Enter to open it.
  • Wait for Docker Desktop to finish starting up (the whale icon in your menu bar or system tray stops animating).
  • Verify Docker is running by running this command:
docker info

You should see detailed output about your Docker installation, including the server version and operating system.

I see Docker info output

Docker is running and ready. You have all three tools set up.

Cannot connect to the Docker daemon

This error means Docker Desktop is not running yet.

  • Open Docker Desktop using the steps above and wait for it to fully start.
  • Run docker info again once the whale icon stops animating.

If Docker Desktop is not installed, download it from docker.com/products/docker-desktop.

Your environment is ready. You have the Azure CLI installed and authenticated, the Container Apps extension loaded, and both Python and Docker confirmed working. Next up, you will build the FastAPI application that you will eventually containerize and deploy.

Build the FastAPI Application

Your tools are installed and your Azure account is ready. Now you need an application to containerize and deploy.

In this step, you will build a small book catalog API using FastAPI. The API will support listing books, adding new books with input validation, retrieving a single book by ID, and reporting its own health status.

In this step, get ready to:
  • Set up a Python project with a virtual environment and pinned dependencies.
  • Write a FastAPI application with four endpoints and a Pydantic data model.
  • Run and test the API locally using the built-in Swagger UI.
Set up the project and install dependencies

Before writing any code, you need a dedicated project folder, a Python virtual environment to isolate your packages, and a requirements.txt file that lists your exact dependencies. Docker will use this file later to install the same packages inside your container.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir fastapi-books
cd fastapi-books
  • Confirm the folder exists by running:
ls

You should see the fastapi-books folder listed on your Desktop.

  • Create a Python virtual environment by running:
python3 -m venv .venv
  • Activate the virtual environment by running:
source .venv/bin/activate

What is a virtual environment?

A virtual environment is an isolated Python installation inside your project folder. Packages you install here stay separate from your system Python, so different projects can use different package versions without conflicts.

Seeing 'command not found' for python3?

Make sure Python 3 is installed. Run python3 --version to check. If it is missing, revisit Step 1 to install it.

help me fix Python not being found on my system

  • Install FastAPI and Uvicorn by running:
pip install fastapi==0.136.0 uvicorn==0.44.0

What are FastAPI and Uvicorn?

FastAPI is a Python web framework for building APIs. It gives you automatic request validation, interactive API docs, and high performance out of the box.

Uvicorn is the server that actually runs your FastAPI app. It listens for incoming HTTP requests and passes them to your application code.

  • Freeze your installed packages into a requirements.txt file by running:
pip freeze > requirements.txt

Why freeze dependencies?

The pip freeze command outputs every installed package with its exact version number. Redirecting this into requirements.txt creates a snapshot that Docker can replay to install the identical packages inside your container.

  • Create the app directory that will hold your application code by running:
mkdir app
  • Verify your project structure by running:
ls

You should see .venv, app, and requirements.txt in the output.

Missing .venv or requirements.txt?

Make sure you are inside the fastapi-books folder. Run pwd to check your current directory. It should end with fastapi-books.

help me figure out why my project files are missing

Write the FastAPI application

Your project structure is ready. Now you will write the API itself. The application lives in a single file, app/main.py, and defines four endpoints plus a Pydantic data model for input validation.

  • Create the file app/main.py by running:
touch app/main.py

You also need an __init__.py file so Python treats the app directory as a package.

  • Create the init file by running:
touch app/__init__.py
  • Confirm both files exist by running:
ls app/

You should see __init__.py and main.py in the output.

  • Open app/main.py in your text editor.

The application code is split into three chunks. Each chunk adds a distinct piece of functionality.

  • Add the imports, data model, and in-memory book store by copying the following code into app/main.py:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

# Define the shape of a book using Pydantic
class Book(BaseModel):
    title: str
    author: str
    year: int
    isbn: str | None = None  # ISBN is optional

# Create the FastAPI application instance
app = FastAPI(title="Book Catalog API")

# In-memory list of books (acts as a simple database)
books: list[dict] = []

What does this code do?

  • The Book class inherits from BaseModel. Pydantic uses this class to automatically validate incoming request data. If a request is missing title, author, or year, FastAPI returns a 422 error with a clear message.
  • The isbn field uses str | None = None to mark it as optional. Callers can include it or leave it out.
  • The books list stores book data in memory. It resets every time the server restarts.
  • Add the root and health endpoints below the existing code in app/main.py:

# Root endpoint - returns a welcome message
@app.get("/")
def root():
    return {"message": "Welcome to the Book Catalog API"}

# Health check endpoint - used by container orchestrators to verify the app is running
@app.get("/health")
def health_check():
    return {"status": "healthy"}

What does this code do?

  • The / endpoint returns a simple welcome message. It confirms the API is reachable.
  • The /health endpoint returns {"status": "healthy"}. Container orchestrators like Azure Container Apps use health endpoints to check whether your app is alive and ready to serve traffic.
  • Add the book endpoints below the health check in app/main.py:

# GET /books - return all books in the catalog
@app.get("/books")
def list_books():
    return books

# POST /books - add a new book with automatic validation
@app.post("/books", status_code=201)
def add_book(book: Book):
    # Convert the Pydantic model to a dict and assign an ID
    book_dict = book.model_dump()
    book_dict["id"] = len(books) + 1
    books.append(book_dict)
    return book_dict

# GET /books/{book_id} - retrieve a single book by its ID
@app.get("/books/{book_id}")
def get_book(book_id: int):
    # Search for the book in the list
    for book in books:
        if book["id"] == book_id:
            return book
    # If no book matches, return a 404 error
    raise HTTPException(status_code=404, detail="Book not found")

What does this code do?

  • GET /books returns the full list of books currently stored in memory.
  • POST /books accepts a JSON body matching the Book model. FastAPI validates the input automatically. The status_code=201 tells the client a new resource was created.
  • GET /books/{book_id} looks up a book by its numeric ID. If no match is found, HTTPException raises a 404 error with a clear detail message.
  • Save app/main.py.

Awesome, I've got everything!

Double-check you have saved app/main.py before moving on.

I'd like to double check the full code

Here is the complete app/main.py file for comparison:

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

# Define the shape of a book using Pydantic
class Book(BaseModel):
    title: str
    author: str
    year: int
    isbn: str | None = None  # ISBN is optional

# Create the FastAPI application instance
app = FastAPI(title="Book Catalog API")

# In-memory list of books (acts as a simple database)
books: list[dict] = []

# Root endpoint - returns a welcome message
@app.get("/")
def root():
    return {"message": "Welcome to the Book Catalog API"}

# Health check endpoint - used by container orchestrators to verify the app is running
@app.get("/health")
def health_check():
    return {"status": "healthy"}

# GET /books - return all books in the catalog
@app.get("/books")
def list_books():
    return books

# POST /books - add a new book with automatic validation
@app.post("/books", status_code=201)
def add_book(book: Book):
    # Convert the Pydantic model to a dict and assign an ID
    book_dict = book.model_dump()
    book_dict["id"] = len(books) + 1
    books.append(book_dict)
    return book_dict

# GET /books/{book_id} - retrieve a single book by its ID
@app.get("/books/{book_id}")
def get_book(book_id: int):
    # Search for the book in the list
    for book in books:
        if book["id"] == book_id:
            return book
    # If no book matches, return a 404 error
    raise HTTPException(status_code=404, detail="Book not found")
Run and test locally

Your API code is written. Now you will start the development server and test each endpoint using FastAPI's built-in interactive documentation.

  • Start the FastAPI development server by running:
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000

What does this command do?

  • app.main:app tells Uvicorn to look inside the app folder, open main.py, and find the variable named app.
  • --reload watches for file changes and restarts the server automatically. This is for development only.
  • --host 0.0.0.0 makes the server accessible on all network interfaces, not just localhost.
  • --port 8000 sets the port the server listens on.

You should see output ending with Uvicorn running on http://0.0.0.0:8000.

Seeing 'Address already in use'?

Another process is using port 8000. Stop it first, or use a different port by changing --port 8000 to --port 8001.

help me fix the 'Address already in use' error when starting uvicorn

  • Open your browser and go to http://localhost:8000/docs.

You should see the Swagger UI page. This interactive documentation is generated automatically by FastAPI from your endpoint definitions and Pydantic models.

What is Swagger UI?

Swagger UI is a web-based tool that lets you explore and test your API directly in the browser. Every endpoint you defined appears as an expandable section with a "Try it out" button. FastAPI generates this page automatically from your code.

  • Click on GET / to expand it.
  • Click Try it out, then click Execute.

You should see a response body of {"message": "Welcome to the Book Catalog API"}.

  • Test the health endpoint by expanding GET /health, clicking Try it out, then clicking Execute.

You should see {"status": "healthy"} in the response body.

  • Test adding a book by expanding POST /books, clicking Try it out, and replacing the request body with this JSON:
{
  "title": "The Pragmatic Programmer",
  "author": "David Thomas",
  "year": 1999
}
  • Click Execute.

You should see a 201 response with the book data plus an id field set to 1.

  • Test retrieving the book by expanding GET /books/1, clicking Try it out, entering 1 in the book_id field, and clicking Execute.

You should see the same book data returned.

  • Test the 404 error by trying GET /books/{book_id} with book_id set to 999.

You should see a 404 response with {"detail": "Book not found"}.

Swagger UI not loading?

Check that the Uvicorn server is still running in your terminal. If you see an error, read the traceback carefully. The most common cause is a syntax error in app/main.py.

help me debug why my FastAPI Swagger UI is not loading

  • Go back to your terminal and stop the server by pressing Ctrl+C.

Your FastAPI application is built, tested, and ready to be containerized. Next up, you will write a production-quality Dockerfile and run your API inside a Docker container.

Containerize with Docker

In the previous step, you built a working FastAPI application and tested it locally with uvicorn.

Running the app on your machine is great for development, but deploying it to the cloud means packaging it so it runs the same way everywhere. That is exactly what Docker does. In this step, you will write a production-quality Dockerfile, build a container image, and verify it works locally before deploying to Azure.

In this step, get ready to:
  • Write a production-optimized Dockerfile with layer caching and a non-root user.
  • Build and run your containerized API locally.
  • Verify that Docker layer caching speeds up rebuilds when only code changes.
Write the Dockerfile

A Dockerfile is a text file that tells Docker how to build your container image. Each line becomes a layer in the image, and the order of those layers matters for build speed.

  • Make sure you are in your fastapi-books project folder (the one containing your app/ directory and requirements.txt).
  • Create a file called Dockerfile in the project root (no file extension) and paste the following code into it:
# Use a slim Python base image for smaller size
FROM python:3.13-slim

# Prevent Python from writing .pyc files to disk
ENV PYTHONDONTWRITEBYTECODE=1

# Ensure Python output is sent straight to the terminal (no buffering)
ENV PYTHONUNBUFFERED=1

# Set the working directory inside the container
WORKDIR /app

# Copy only the requirements file first (enables layer caching)
COPY requirements.txt .

# Install dependencies in a separate layer
RUN pip install --no-cache-dir -r requirements.txt

# Copy the rest of the application code
COPY . .

# Create a non-root user for security
RUN adduser --disabled-password --gecos "" --no-create-home appuser

# Switch to the non-root user
USER appuser

# Document the port the app listens on
EXPOSE 8000

# Start the FastAPI app with uvicorn
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

What does this Dockerfile do?

  • FROM python:3.13-slim starts from a lightweight Debian-based Python image. The slim variant is much smaller than the full image but avoids the compatibility issues that Alpine can cause with some Python packages.
  • COPY requirements.txt . and RUN pip install happen before COPY . .. This is the layer caching trick. Docker caches each layer, so when you only change application code, Docker reuses the cached dependency layer instead of reinstalling everything.
  • RUN adduser creates a non-root user called appuser. Running containers as root is a security risk because a compromised container could affect the host system.
  • CMD uses the exec form (JSON array) so that uvicorn receives OS signals directly. This ensures the container shuts down gracefully.

Why slim instead of Alpine?

Alpine Linux uses musl libc, which can cause issues with Python packages that expect glibc. The python:3.13-slim image is Debian-based and avoids those compatibility headaches while still being much smaller than the full Python image.

Next, you need a .dockerignore file to keep unnecessary files out of the Docker build context.

  • Create a file called .dockerignore in the project root and paste the following:
__pycache__
*.pyc
.venv
.git
.gitignore
*.md
.env
.dockerignore
Dockerfile

What does .dockerignore do?

The .dockerignore file works like .gitignore. It tells Docker which files to exclude when copying your project into the image. This keeps the image smaller and prevents sensitive files (like .env) from leaking into the container.

  • Save both Dockerfile and .dockerignore.
  • Confirm both files exist by running:
ls Dockerfile .dockerignore
  • You should see both filenames printed in the output.

Don't see both files?

Make sure you created both files in the project root (the fastapi-books folder), not inside the app/ subdirectory. The Dockerfile has no file extension. If you accidentally named it Dockerfile.txt, rename it.

Help me troubleshoot my Dockerfile setup

Build and run the container

With the Dockerfile ready, you can build a Docker image and run it as a container to test your API in an isolated environment.

  • If you still have the uvicorn dev server running from the previous step, stop it by pressing Ctrl+C in that terminal.
  • Build your Docker image by running this command:
docker build -t fastapi-books .

What does this command do?

  • docker build reads your Dockerfile and executes each instruction to create a container image.
  • -t fastapi-books tags the image with the name fastapi-books so you can reference it easily later.
  • . tells Docker to use the current directory as the build context (the set of files Docker can access during the build).

The first build takes a minute or two because Docker downloads the base image and installs your Python dependencies. You will see output for each layer being built.

  • Once the build finishes, start the container by running:
docker run -d --name fastapi-books -p 8000:8000 fastapi-books

What does this command do?

  • -d runs the container in detached mode (in the background), so your terminal stays free.
  • --name fastapi-books gives the container a human-readable name instead of a random ID.
  • -p 8000:8000 maps port 8000 on your machine to port 8000 inside the container, so you can access the API from your browser.
  • Open your browser and go to http://localhost:8000/docs to see the Swagger UI.
  • Click on the GET /health endpoint, then click Try it out and Execute.
  • You should see {"status": "healthy"} in the response body.
  • Check the container logs by running:
docker logs fastapi-books
  • You should see uvicorn startup output and the GET request you just made to /health.

Can't reach localhost:8000?

  • Make sure Docker Desktop is running. If it is not, the docker run command fails silently in detached mode.
  • Check that no other process is using port 8000. If you forgot to stop the uvicorn dev server from the previous step, it will conflict with the container.
  • Run docker ps to confirm the container is actually running. If it is not listed, run docker logs fastapi-books to see the error.

Help me debug my Docker container not responding on localhost

Verify layer caching works

The whole point of the layer ordering in your Dockerfile is to make rebuilds fast when only your code changes. Let's prove it works.

  • Open app/main.py in your editor.
  • Find the root endpoint (the @app.get("/") function) and find the line that returns the welcome message.
  • Find this line:
return {"message": "Welcome to the Book Catalog API"}
  • Replace it with:
return {"message": "Welcome to the Book Catalog API v2"}
  • Save app/main.py.
  • Rebuild the image by running:
docker build -t fastapi-books .

Watch the build output carefully. You should see the word CACHED next to the dependency installation step. Docker skipped reinstalling your Python packages because requirements.txt did not change.

Why does this matter?

In a real project, installing dependencies can take minutes. Layer caching means that code-only changes rebuild in seconds. This is one of the most important Docker best practices to internalize.

Now clean up the running container so it does not conflict with future steps.

  • Stop and remove the container by running:
docker stop fastapi-books && docker rm fastapi-books
  • Confirm the container is gone by running:
docker ps -a --filter name=fastapi-books
  • You should see an empty list with only the column headers. The container has been removed, but the image is still available for the next step.

Seeing "No such container"?

If the docker stop command says the container does not exist, it may have already stopped on its own due to an error. Run docker ps -a to see all containers (including stopped ones) and use docker rm fastapi-books to remove it.

Help me clean up my Docker containers

Awesome, I've got everything!

Great. Double check you have saved your Dockerfile, .dockerignore, and the updated app/main.py.

I'd like to double check the full Dockerfile

Here is the complete Dockerfile for comparison.

# Use a slim Python base image for smaller size
FROM python:3.13-slim

# Prevent Python from writing .pyc files to disk
ENV PYTHONDONTWRITEBYTECODE=1

# Ensure Python output is sent straight to the terminal (no buffering)
ENV PYTHONUNBUFFERED=1

# Set the working directory inside the container
WORKDIR /app

# Copy only the requirements file first (enables layer caching)
COPY requirements.txt .

# Install dependencies in a separate layer
RUN pip install --no-cache-dir -r requirements.txt

# Copy the rest of the application code
COPY . .

# Create a non-root user for security
RUN adduser --disabled-password --gecos "" --no-create-home appuser

# Switch to the non-root user
USER appuser

# Document the port the app listens on
EXPOSE 8000

# Start the FastAPI app with uvicorn
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

Your API is containerized and proven to work locally. Next up, you will deploy this container image to Azure Container Apps and get a live public URL.

Deploy to Azure Container Apps

Your Docker container runs perfectly on your local machine. The next challenge is getting it running on the internet so anyone can reach it.

In this step, you will deploy your containerized API to Azure Container Apps using a single command that handles everything. It creates a container registry, builds your image in the cloud, and launches your app with a public URL.

In this step, get ready to:
  • Set shell variables for your Azure resource names.
  • Deploy your app to Azure Container Apps with one command.
  • Verify your live API is publicly accessible.
Set environment variables for Azure resources

Azure resources need names. Instead of typing these names repeatedly, you will store them in shell variables that the deployment command can reference.

  • Define your Azure resource names by running these commands in your terminal:
RESOURCE_GROUP="fastapi-books-rg"
LOCATION="eastus"
APP_NAME="fastapi-books-app"
ENVIRONMENT="fastapi-books-env"

What do these variables control?

  • RESOURCE_GROUP is the container for all your Azure resources. Deleting this group later removes everything at once.
  • LOCATION sets the Azure region. eastus is a common default with broad service availability.
  • APP_NAME is the name of your container app. It appears in your public URL.
  • ENVIRONMENT is the Container Apps environment that manages networking, logging, and scaling for your app.
  • Confirm the variables are set by running:
echo $RESOURCE_GROUP $LOCATION $APP_NAME $ENVIRONMENT

You should see fastapi-books-rg eastus fastapi-books-app fastapi-books-env printed in your terminal.

Seeing blank output?

Make sure you ran the variable assignment commands in the same terminal session. If you opened a new terminal window, the variables are lost and you need to run the assignments again.

help me debug why my shell variables are empty

Deploy with a single command

The az containerapp up command is a one-stop deployment tool. It reads your Dockerfile, builds the image in the cloud, creates all the necessary Azure resources, and launches your container app. No manual registry setup or image pushing required.

  • Make sure you are in your project root directory (the folder containing your Dockerfile).
  • Deploy your app to Azure by running this command:
az containerapp up \
  --name $APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --location $LOCATION \
  --environment $ENVIRONMENT \
  --source . \
  --ingress external \
  --target-port 8000

What does this command do?

This single command performs six steps behind the scenes:

  • Creates the resource group fastapi-books-rg in the eastus region.
  • Creates an Azure Container Registry to store your Docker image.
  • Builds your Docker image using the Dockerfile in the current directory.
  • Pushes the built image to the container registry.
  • Creates the Container Apps environment with a Log Analytics workspace.
  • Deploys your container app with external ingress on port 8000.

The --ingress external flag makes your app publicly accessible from the internet. The --target-port 8000 flag tells Azure which port your container listens on.

The --source . flag points to the current directory, where Azure finds your Dockerfile and builds the image.

This command takes a few minutes

The deployment process builds your image in the cloud and provisions multiple Azure resources. Expect it to take 3-5 minutes. You will see progress output in your terminal as each step completes.

When the command finishes, look for the FQDN (fully qualified domain name) in the output. It looks something like fastapi-books-app.happyvalley-abcd1234.eastus.azurecontainerapps.io.

Pro tip: save your URL

Copy the FQDN from the terminal output and save it somewhere handy. You will use this URL for the rest of the project to test your live API.

Seeing an error during deployment?

  • If you see a "missing parameters" error, make sure you have the latest Container Apps extension installed. Run az extension add --name containerapp --upgrade and try again.
  • If you see an authentication error, run az login to refresh your session.
  • If the build fails, make sure your Dockerfile has no syntax errors and that you are running the command from the directory containing your Dockerfile.

help me debug my az containerapp up deployment error

Verify the deployment

Your app is now live on Azure. Time to confirm everything is working by hitting the public URL.

  • Open your browser and navigate to your app's FQDN. You should see the welcome message from your root endpoint.
  • Append /health to the URL to check the health endpoint. You should see {"status": "healthy"}.
  • Append /docs to the URL to open the Swagger UI. This is the same interactive docs you used locally, now running on Azure.
  • Append /books to see the book list endpoint.

You now have a live FastAPI application running on Azure with a public HTTPS URL that anyone on the internet can access.

Why is my URL using HTTPS automatically?

Azure Container Apps provides automatic TLS termination for all external ingress. Your app receives plain HTTP traffic on port 8000 inside the container, but Azure handles the HTTPS certificate and encryption for incoming requests. No certificate setup needed on your end.

Seeing a 404 or blank page?

  • Double-check that you copied the full FQDN from the deployment output. The URL should end with .azurecontainerapps.io.
  • If the app is not responding, it may still be starting up. Wait 30 seconds and refresh.
  • Make sure --target-port 8000 matches the port your Uvicorn server listens on in your Dockerfile's CMD.

help me troubleshoot my Container Apps deployment that is not responding

Your API is live on the internet. Next up, you will exercise it with real requests and explore Azure's log streaming to see what happens inside your running container.

Explore and Test Your Live API

In the previous step, you deployed your containerized API to Azure Container Apps and got a public HTTPS URL. Now it is time to put that live API through its paces.

You will send real requests to every endpoint, intentionally trigger validation errors, and stream live logs from your running container. These are the same techniques you would use to verify and debug a production deployment.

In this step, get ready to:
  • Test all four endpoints on your live API using curl and the Swagger UI.
  • Stream real-time logs from your Azure container to watch requests arrive.
  • Check your app's status and understand how the Consumption plan scales to zero.
Test all endpoints on the live URL

Your API is live, but you have not exercised it with real data yet. Let's hit every endpoint to confirm that FastAPI's routing, Pydantic validation, and error handling all work correctly in the deployed environment.

First, grab your app's FQDN so you can use it in the commands below.

  • Retrieve your app's public URL by running this command:
az containerapp show \
  --name $APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --query properties.configuration.ingress.fqdn \
  --output tsv

What does this command do?

The --query flag uses a JMESPath expression to extract just the FQDN from the full JSON response. The --output tsv flag outputs the value as plain text, making it easy to copy.

You should see a domain like fastapi-books-app.happyhill-70162bb9.eastus.azurecontainerapps.io. Copy this value.

  • Test the health endpoint by running this command:
curl https://[[FQDN="your app's FQDN"]]/health

You should see {"status":"healthy"} in your terminal output.

  • Add a book to your live API by sending a POST request:
curl -X POST https://[[FQDN="your app's FQDN"]]/books \
  -H "Content-Type: application/json" \
  -d '{"title": "The Pragmatic Programmer", "author": "David Thomas", "year": 1999}'

What does this command do?

  • -X POST tells curl to send a POST request instead of the default GET.
  • -H "Content-Type: application/json" sets the request header so FastAPI knows the body contains JSON.
  • -d passes the JSON body with the book's title, author, and year.

You should see a JSON response containing the book you just added, with an assigned id field.

  • List all books to confirm your new book was saved:
curl https://[[FQDN="your app's FQDN"]]/books

You should see a JSON array that includes the book you just added.

  • Retrieve a single book by its ID:
curl https://[[FQDN="your app's FQDN"]]/books/1

You should see the JSON object for just that one book.

Now let's see what happens when you send invalid data. Pydantic validates every incoming request, and FastAPI returns a detailed error response when validation fails.

  • Send a POST request with missing required fields to trigger a validation error:
curl -X POST https://[[FQDN="your app's FQDN"]]/books \
  -H "Content-Type: application/json" \
  -d '{"title": "Incomplete Book"}'

You should see a 422 Unprocessable Entity response with details about which fields are missing (author and year). This is Pydantic validation working in production.

  • Try fetching a book that does not exist to see the 404 response:
curl https://[[FQDN="your app's FQDN"]]/books/999

You should see a {"detail":"Book not found"} response with a 404 status code.

Pro tip: use Swagger UI

You can also test all of these endpoints in your browser by visiting https://your-fqdn/docs. FastAPI's built-in Swagger UI lets you fill in request bodies and send requests without writing any curl commands.

Getting a connection error or timeout?

  • Make sure you are using https:// (not http://) in your URL. Azure Container Apps enforces HTTPS.
  • Your app may have scaled to zero while idle. The first request after scaling up takes a few seconds. Try the command again.
  • Double-check that you copied the full FQDN from the previous step, including the .azurecontainerapps.io suffix.

Still stuck? Help me debug why my Azure Container App is not responding to requests.

Stream live logs from Azure

When something goes wrong in production, logs are your first line of defense. Azure Container Apps lets you stream logs in real time from the CLI, just like tailing a local log file.

  • Start streaming your container's console logs by running this command:
az containerapp logs show \
  --name $APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --type console \
  --follow

What does this command do?

  • --type console streams the application's stdout and stderr output. This is where Uvicorn's request logs appear.
  • --follow keeps the stream open so new log lines appear as they happen, similar to tail -f on a local file.

You should see Uvicorn's startup message and any recent request logs in the output.

  • Open a second terminal window by pressing Cmd+N (macOS) or Ctrl+Shift+N (Windows) in your terminal app.
  • Send a few requests to your live API from the second terminal:
curl https://[[FQDN="your app's FQDN"]]/health
curl https://[[FQDN="your app's FQDN"]]/books
  • Switch back to your first terminal where the log stream is running.

You should see new log entries for each request you just made, including the HTTP method, path, and status code.

  • Press Ctrl+C to stop the log stream.

Not seeing any log output?

  • If your app scaled to zero, the log stream has no active container to connect to. Send a request to your FQDN first to wake the app up, then run the log command again.
  • Make sure the $APP_NAME and $RESOURCE_GROUP variables are still set in your terminal session. Run echo $APP_NAME to check.

Help me troubleshoot why az containerapp logs show is not returning any output.

Check your app's status and resource consumption

Azure Container Apps on the Consumption plan automatically scales your app to zero replicas when it is idle. This means you are only charged for the time your app is actively handling requests.

  • Check your app's FQDN and provisioning state by running this command:
az containerapp show \
  --name $APP_NAME \
  --resource-group $RESOURCE_GROUP \
  --query "{FQDN:properties.configuration.ingress.fqdn, ProvisioningState:properties.provisioningState}" \
  --output table

What does this command do?

The --query flag uses a JMESPath expression to extract just the FQDN and provisioning state from the full JSON response. The --output table flag formats the result as a readable table.

You should see your FQDN and a provisioning state of Succeeded.

  • Check how many replicas are currently running by listing them:
az containerapp replica list \
  --name $APP_NAME \
  --resource-group $RESOURCE_GROUP

If you have not sent a request recently, you may see an empty list. This is the scale-to-zero behavior in action.

What is scale to zero?

On the Consumption plan, Azure Container Apps shuts down all replicas of your app when no requests are coming in. When a new request arrives, Azure spins up a replica to handle it. This means you pay nothing while your app is idle.

The free tier includes 180,000 vCPU-seconds, 360,000 GiB-seconds, and 2 million requests per subscription per month. For a project like this, you will stay well within the free tier.

Seeing an error with the replica list command?

  • The az containerapp replica list command requires the Container Apps CLI extension. If you see an unrecognized command error, update the extension by running az extension add --name containerapp --upgrade.
  • If your app has scaled to zero, the list will be empty. That is expected behavior. Send a request to your FQDN to wake the app, then try the command again.

Help me troubleshoot the az containerapp replica list command.

Your API is live, tested, and observable. You have confirmed that every endpoint works, that validation catches bad input, and that you can monitor your app's logs and status from the CLI. Next up, you will clean up your Azure resources so you do not incur any unexpected charges.

Secret mission

Add Environment Configuration and Docker Compose

Your API works on Azure, but the configuration is hardcoded. In production, teams use environment variables to control app behavior across different environments (development, staging, production) without changing code. Can you add Pydantic Settings for environment-based configuration and a Docker Compose file for a smoother local workflow?

Before you go

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. Your Azure Container Apps app scales to zero when idle, but the Azure Container Registry incurs a small daily cost if left running.

Cost warning

The Container Apps Consumption plan has a generous free tier (180,000 vCPU-seconds, 360,000 GiB-seconds, and 2M requests per subscription per month). Your app scales to zero when idle, so Container Apps itself costs nothing while unused.

However, the Azure Container Registry (Basic SKU) costs approximately $0.167/day (~$5/month) if left running. Delete the resource group to avoid any charges.

Resources you used:

  • Azure Resource Group (contains your Container Registry, Container Apps environment, Container App, and Log Analytics workspace).
  • Azure Container Registry (Basic SKU, stores your Docker image).
  • Azure Container Apps environment and Container App.
  • Local Docker image (fastapi-books).
  • Local project directory with Python virtual environment.

Keep everything running

No action needed. Choose this if you are still actively building or want to keep your live API accessible at its public URL.

  • Your Container App stays live at its FQDN and scales to zero when idle.
  • The Azure Container Registry (Basic SKU) costs approximately $0.167/day (~$5/month) while it exists.
  • Your local Docker image and project files remain on your machine.

Pause - I'll come back to this later

Delete your Azure resources to stop all cloud costs, but keep your local project files and Docker image so you can redeploy later.

  • Delete the Azure resource group to remove all cloud resources:
az group delete --name $RESOURCE_GROUP --yes --no-wait
  • This removes the resource group, Container Registry, Container Apps environment, and your Container App.
  • Your local project directory, virtual environment, and Docker image are untouched. You can redeploy anytime by running az containerapp up again.

Delete - I don't want to use this again

Remove all project resources from both Azure and your local machine.

  • Delete the Azure resource group:
az group delete --name $RESOURCE_GROUP --yes --no-wait
  • Remove the local Docker image:
docker rmi fastapi-books
  • Optionally, delete your local project directory:
cd ~ && rm -rf fastapi-books
  • This fully removes all Azure resources, Docker images, and project files from your machine.

Mission accomplished

That's a Wrap!

Nice work! You just took a Python API from your local machine to a live HTTPS endpoint on Azure. That is the core container deployment workflow that engineering teams use every day.

Here's what you learned:

  • Built a FastAPI REST API with Pydantic models, input validation, and a health check endpoint.
  • Wrote a production-style Dockerfile with layer caching, a slim base image, and a non-root user for security.
  • Deployed to Azure Container Apps using az containerapp up with automatic image building, registry management, and live log streaming.
  • Added environment-based configuration with Pydantic Settings and a Docker Compose workflow for local multi-environment development.

Ready to quiz yourself?