In the modern world of cloud-native development, speed and efficiency are paramount. Developers love FastAPI for its incredible performance and developer-friendly (Python-based) API development. DevOps engineers love Docker for its containerization standard and K3s for its lightweight, fully-compliant Kubernetes distribution. Combining these three technologies creates a powerful, scalable, and resource-efficient stack for modern applications. This guide provides a comprehensive, step-by-step walkthrough to Deploy FastAPI Docker K3s, taking you from a simple Python script to a fully orchestrated application running in a Kubernetes cluster.
Whether you’re a DevOps engineer, a backend developer, or an MLOps practitioner looking to serve models, this tutorial will equip you with the practical skills to containerize and deploy your FastAPI applications like a pro. We’ll cover everything from writing an optimized Dockerfile to configuring Kubernetes manifests for Deployment, Service, and Ingress.
Table of Contents
- 1 Why This Stack? The Power of FastAPI, Docker, and K3s
- 2 Prerequisites: What You’ll Need
- 3 Step 1: Creating a Simple FastAPI Application
- 4 Step 2: Containerizing FastAPI with Docker
- 5 Step 3: Setting Up Your K3s Cluster
- 6 Step 4: Preparing Your Image for the K3s Cluster
- 7 Step 5: Writing the Kubernetes Manifests to Deploy FastAPI Docker K3s
- 8 Step 6: Verifying the Deployment
- 9 Advanced Considerations and Best Practices
- 10 Frequently Asked Questions
- 11 Conclusion
Why This Stack? The Power of FastAPI, Docker, and K3s
Before we dive into the “how,” let’s briefly understand the “why.” This isn’t just a random assortment of technologies; it’s a stack where each component complements the others perfectly.
FastAPI: High-Performance Python
FastAPI is a modern, high-performance web framework for building APIs with Python 3.7+ based on standard Python type hints. Its key advantages include:
- Speed: It’s one of the fastest Python frameworks available, on par with NodeJS and Go, thanks to Starlette (for the web parts) and Pydantic (for the data parts).
- Async Support: Built from the ground up with
async
/await
, making it ideal for I/O-bound operations. - Developer Experience: Automatic interactive API documentation (via Swagger UI and ReDoc) and type-checking drastically reduce development and debugging time.
- Popularity: It’s seen massive adoption, especially in the MLOps community for serving machine learning models efficiently.
Docker: The Container Standard
Docker revolutionized software development by standardizing “containers.” A container packages an application and all its dependencies (libraries, system tools, code) into a single, isolated unit. This means:
- Consistency: An application runs the same way on a developer’s laptop as it does in a production environment. No more “it works on my machine” problems.
- Portability: Docker containers can run on any system that has the Docker runtime, from a local machine to any cloud provider.
- Isolation: Containers run in isolated processes, ensuring they don’t interfere with each other or the host system.
K3s: Lightweight, Certified Kubernetes
K3s, a project from Rancher (now part of SUSE), is a “lightweight Kubernetes.” It’s a fully CNCF-certified Kubernetes distribution that strips out legacy, alpha, and non-default features, packaging everything into a single binary less than 100MB. This makes it perfect for:
- Edge Computing & IoT: Its small footprint is ideal for resource-constrained devices.
- Development & Testing: It provides a full-featured Kubernetes environment on your local machine in seconds, without the resource-heavy requirements of a full K8s cluster.
- CI/CD Pipelines: Spin up and tear down test environments quickly.
K3s includes everything you need out-of-the-box, including a container runtime (containerd
), a storage provider, and an ingress controller (Traefik), which simplifies setup enormously.
Prerequisites: What You’ll Need
To follow this tutorial, you’ll need the following tools installed on your local machine (Linux, macOS, or WSL2 on Windows):
- Python 3.7+ and
pip
: To create the FastAPI application. - Docker: To build and manage your container images. You can get it from the Docker website.
- K3s: For our Kubernetes cluster. We’ll install this together.
kubectl
: The Kubernetes command-line tool. It’s often installed automatically with K3s, but it’s good to have.- A text editor: Visual Studio Code or any editor of your choice.
Step 1: Creating a Simple FastAPI Application
First, let’s create our application. Make a new project directory and create two files: requirements.txt
and main.py
.
mkdir fastapi-k3s-project
cd fastapi-k3s-project
Create requirements.txt
. We need fastapi
and uvicorn
, which will act as our ASGI server.
# requirements.txt
fastapi==0.104.1
uvicorn[standard]==0.23.2
Next, create main.py
. We’ll add three simple endpoints: a root (/
), a dynamic path (/items/{item_id}
), and a /health
endpoint, which is a best practice for Kubernetes probes.
# main.py
from fastapi import FastAPI
import os
app = FastAPI()
# Get an environment variable, with a default
APP_VERSION = os.getenv("APP_VERSION", "0.0.1")
@app.get("/")
def read_root():
"""Returns a simple hello world message."""
return {"Hello": "World", "version": APP_VERSION}
@app.get("/items/{item_id}")
def read_item(item_id: int, q: str | None = None):
"""Returns an item ID and an optional query parameter."""
return {"item_id": item_id, "q": q}
@app.get("/health")
def health_check():
"""Simple health check endpoint for Kubernetes probes."""
return {"status": "ok"}
if __name__ == "__main__":
import uvicorn
# This is only for local debugging (running `python main.py`)
uvicorn.run(app, host="0.0.0.0", port=8000)
You can test this locally by first installing the requirements and then running the app:
pip install -r requirements.txt
python main.py
# Or using uvicorn directly
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
You should now be able to access http://127.0.0.1:8000 in your browser and see {"Hello":"World","version":"0.0.1"}
. Also, check the interactive docs at http://127.0.0.1:8000/docs.
Step 2: Containerizing FastAPI with Docker
Now, let’s “dockerize” this application. We will write a Dockerfile
that packages our app into a portable container image.
Writing the Dockerfile
We’ll use a multi-stage build. This is a best practice that results in smaller, more secure production images.
- Stage 1 (Builder): We use a full Python image to install our dependencies into a dedicated directory.
- Stage 2 (Final): We use a slim Python image, create a non-root user for security, and copy *only* the installed dependencies from the builder stage and our application code.
Create a file named Dockerfile
in your project directory:
# Stage 1: The Builder Stage
# We use a full Python image to build our dependencies
FROM python:3.10-slim as builder
# Set the working directory
WORKDIR /usr/src/app
# Install build dependencies for some Python packages
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc && \
rm -rf /var/lib/apt/lists/*
# Set up a virtual environment
ENV VIRTUAL_ENV=/opt/venv
RUN python3 -m venv $VIRTUAL_ENV
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
# Copy requirements and install them into the venv
# We copy requirements.txt first to leverage Docker layer caching
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Stage 2: The Final Stage
# We use a slim image for a smaller footprint
FROM python:3.10-slim
# Set working directory
WORKDIR /app
# Create a non-root user and group for security
RUN groupadd -r appuser && useradd -r -g appuser appuser
# Copy the virtual environment from the builder stage
COPY --from=builder /opt/venv /opt/venv
# Copy the application code
COPY main.py .
# Grant ownership to our non-root user
RUN chown -R appuser:appuser /app
USER appuser
# Make the venv's Python the default
ENV PATH="/opt/venv/bin:$PATH"
# Expose the port the app runs on
EXPOSE 8000
# The command to run the application using uvicorn
# We run as the appuser
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
This Dockerfile
is optimized for production. It separates dependency installation from code (for caching), runs as a non-root user (for security), and uses a slim base image (for size).
Building and Testing the Docker Image Locally
Now, let’s build the image. Open your terminal in the project directory and run:
# The -t flag tags the image with a name (fastapi-app) and version (latest)
docker build -t fastapi-app:latest .
Once built, you can run it locally to confirm it works:
# -d: run detached
# -p 8000:8000: map host port 8000 to container port 8000
# --name my-fastapi-container: give the container a name
docker run -d -p 8000:8000 --name my-fastapi-container fastapi-app:latest
Test it again by visiting http://127.0.0.1:8000. You should see the same JSON response. Don’t forget to stop and remove the container:
docker stop my-fastapi-container
docker rm my-fastapi-container
Step 3: Setting Up Your K3s Cluster
K3s is famously easy to install. For a local development setup on Linux or macOS, you can just run their installer script.
Installing K3s
The official install script from k3s.io
is the simplest method:
curl -sfL https://get.k3s.io | sh -
This command will download and run the K3s server. After a minute, you’ll have a single-node Kubernetes cluster running.
Note for Docker Desktop users: If you have Docker Desktop, it comes with its own Kubernetes cluster. You can enable that *or* use K3s. K3s is often preferred for being lighter and including extras like Traefik by default. If you use K3s, make sure your kubectl
context is set correctly.
Configuring kubectl
for K3s
The K3s installer creates a kubeconfig
file at /etc/rancher/k3s/k3s.yaml
. Your kubectl
command needs to use this file. You have two options:
- Set the
KUBECONFIG
environment variable (temporary):export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
# You'll also need sudo to read this file
sudo chmod 644 /etc/rancher/k3s/k3s.yaml - Merge it with your existing config (recommended):
# Make sure your default config directory exists
mkdir -p ~/.kube
# Copy the K3s config to a new file
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/k3s-config
sudo chown $(id -u):$(id -g) ~/.kube/k3s-config
# Set KUBECONFIG to point to both your default and new config
export KUBECONFIG=~/.kube/config:~/.kube/k3s-config
# Set the context to k3s
kubectl config use-context default
Verify that kubectl
is connected to your K3s cluster:
kubectl get nodes
# OUTPUT:
# NAME STATUS ROLES AGE VERSION
# [hostname] Ready control-plane,master 2m v1.27.5+k3s1
You can also see the pods K3s runs by default, including Traefik (the ingress controller):
kubectl get pods -n kube-system
# You'll see pods like coredns-..., traefik-..., metrics-server-...
Step 4: Preparing Your Image for the K3s Cluster
This is a critical step that confuses many beginners. Your K3s cluster (even on the same machine) runs its own container runtime (containerd
) and does not automatically see the images in your local Docker daemon.
You have two main options:
Option 1: Using a Public/Private Registry (Recommended)
This is the “production” way. You push your image to a container registry like Docker Hub, GitHub Container Registry (GHCR), or a private one like Harbor.
# 1. Tag your image with your registry username
docker tag fastapi-app:latest yourusername/fastapi-app:latest
# 2. Log in to your registry
docker login
# 3. Push the image
docker push yourusername/fastapi-app:latest
Then, in your Kubernetes manifests, you would use image: yourusername/fastapi-app:latest
.
Option 2: Importing the Image Directly into K3s (For Local Dev)
K3s provides a simple way to “sideload” an image from your local Docker daemon directly into the K3s internal containerd
image store. This is fantastic for local development as it avoids the push/pull cycle.
# Save the image from docker to a tarball, and pipe it to the k3s image import command
docker save fastapi-app:latest | sudo k3s ctr image import -
You should see an output like unpacking docker.io/library/fastapi-app:latest...done
. Now your K3s cluster can find the fastapi-app:latest
image locally.
We will proceed with this tutorial assuming you’ve used Option 2.
Step 5: Writing the Kubernetes Manifests to Deploy FastAPI Docker K3s
It’s time to define our application’s desired state in Kubernetes using YAML manifests. We’ll create three files:
deployment.yaml
: Tells Kubernetes *what* to run (our image) and *how* (e.g., 2 replicas).service.yaml
: Creates an internal network “name” and load balancer for our pods.ingress.yaml
: Exposes our service to the outside world via a hostname (using K3s’s built-in Traefik).
Let’s create a new directory for our manifests:
mkdir manifests
cd manifests
Creating the Deployment (deployment.yaml
)
This file defines a Deployment, which manages a ReplicaSet, which in turn ensures that a specified number of Pods are running. We’ll also add the liveness and readiness probes we planned for.
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: fastapi-deployment
labels:
app: fastapi
spec:
replicas: 2 # Run 2 pods for high availability
selector:
matchLabels:
app: fastapi # This must match the pod template's labels
template:
metadata:
labels:
app: fastapi # Pods will be labeled 'app: fastapi'
spec:
containers:
- name: fastapi-container
image: fastapi-app:latest # The image we built/imported
imagePullPolicy: IfNotPresent # Crucial for locally imported images
ports:
- containerPort: 8000 # The port our app runs on
# --- Liveness and Readiness Probes ---
readinessProbe:
httpGet:
path: /health # The endpoint we created
port: 8000
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 15
periodSeconds: 20
# --- Environment Variables ---
env:
- name: APP_VERSION
value: "1.0.0-k3s" # Pass an env var to the app
Key points:
replicas: 2
: We ask Kubernetes to run two copies of our pod.selector
: The Deployment finds which pods to manage by matching labels (app: fastapi
).imagePullPolicy: IfNotPresent
: This tells K3s to *not* try to pull the image from a remote registry if it already exists locally. This is essential for our Option 2 import.- Probes: The
readinessProbe
checks if the app is ready to accept traffic. ThelivenessProbe
checks if the app is still healthy; if not, K8s will restart it. Both point to our/health
endpoint. env
: We’re passing theAPP_VERSION
environment variable, which our Python code will pick up.
Creating the Service (service.yaml
)
This file defines a Service, which provides a stable, internal IP address and DNS name for our pods. Other services in the cluster can reach our app at fastapi-service.default.svc.cluster.local
.
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: fastapi-service
spec:
type: ClusterIP # Expose the service on an internal-only IP
selector:
app: fastapi # This MUST match the labels of the pods (from the Deployment)
ports:
- protocol: TCP
port: 80 # The port the Service will listen on
targetPort: 8000 # The port on the pod that traffic will be forwarded to
Key points:
type: ClusterIP
: This service is only reachable from *within* the K3s cluster.selector: app: fastapi
: This is how the Service knows which pods to send traffic to. It forwards traffic to any pod with theapp: fastapi
label.port: 80
: We’re abstracting our app’s port. Internally, other pods can just talk tohttp://fastapi-service:80
, and the service will route it to a pod on port8000
.
Creating the Ingress (ingress.yaml
)
This is the final piece. An Ingress tells the ingress controller (Traefik, in K3s) how to route external traffic to internal services. We’ll set it up to route traffic from a specific hostname and path to our fastapi-service
.
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: fastapi-ingress
annotations:
# We can add Traefik-specific annotations here if needed
traefik.ingress.kubernetes.io/router.entrypoints: web
spec:
rules:
- host: fastapi.example.com # The hostname we'll use
http:
paths:
- path: / # Route all traffic from the root path
pathType: Prefix
backend:
service:
name: fastapi-service # The name of our Service
port:
number: 80 # The port our Service is listening on
Key points:
host: fastapi.example.com
: We’re telling Traefik to only apply this rule if the incoming HTTP request has thisHost
header.path: /
: We’re routing all traffic (/
and anything under it).backend.service
: This tells Traefik where to send the traffic: to ourfastapi-service
on port80
.
Applying the Manifests
Now that our three manifests are ready, we can apply them all at once. From inside the manifests
directory, run:
kubectl apply -f .
# OUTPUT:
# deployment.apps/fastapi-deployment created
# service/fastapi-service created
# ingress.networking.k8s.io/fastapi-ingress created
Step 6: Verifying the Deployment
Our application is now deploying! Let’s watch it happen.
Checking Pods, Services, and Ingress
First, check the status of your Deployment and Pods:
kubectl get deployment fastapi-deployment
# NAME READY UP-TO-DATE AVAILABLE AGE
# fastapi-deployment 2/2 2 2 30s
kubectl get pods -l app=fastapi
# NAME READY STATUS RESTARTS AGE
# fastapi-deployment-6c...-abcde 1/1 Running 0 30s
# fastapi-deployment-6c...-fghij 1/1 Running 0 30s
You should see READY 2/2
for the deployment and two pods in the Running
state. If they are stuck in Pending
or ImagePullBackOff
, it means there was a problem with the image (e.g., K3s couldn’t find fastapi-app:latest
).
Next, check the Service and Ingress:
kubectl get service fastapi-service
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# fastapi-service ClusterIP 10.43.123.456 <none> 80/TCP 1m
kubectl get ingress fastapi-ingress
# NAME CLASS HOSTS ADDRESS PORTS AGE
# fastapi-ingress traefik fastapi.example.com 192.168.1.100 80 1m
The ADDRESS
on your Ingress will be the IP of your K3s node. This is the IP we need to use.
Accessing Your FastAPI Application
We told Traefik to route based on the host fastapi.example.com
. Your computer doesn’t know what that is. We need to tell it to map that hostname to your K3s node’s IP address (the ADDRESS
from the kubectl get ingress
command). We do this by editing your /etc/hosts
file.
- Get your node’s IP (if
kubectl get ingress
didn’t show it, get it fromkubectl get nodes -o wide
). Let’s assume it’s192.168.1.100
. - Edit your
/etc/hosts
file (you’ll needsudo
):sudo nano /etc/hosts
- Add this line to the bottom of the file:
192.168.1.100 fastapi.example.com
Now, you can test your application using curl
or your browser!
# Test the root endpoint
curl http://fastapi.example.com/
# OUTPUT:
# {"Hello":"World","version":"1.0.0-k3s"}
# Test the items endpoint
curl http://fastapi.example.com/items/42?q=test
# OUTPUT:
# {"item_id":42,"q":"test"}
# Test the health check
curl http://fastapi.example.com/health
# OUTPUT:
# {"status":"ok"}
Success! You are now running a high-performance FastAPI application, packaged by Docker, and orchestrated by a K3s Kubernetes cluster. Notice that the version returned is 1.0.0-k3s
, which confirms our environment variable from the deployment.yaml
was successfully passed to the application.
Advanced Considerations and Best Practices
You’ve got the basics down. Here are the next steps to move this setup toward a true production-grade system.
Managing Configuration with ConfigMaps and Secrets
We hard-coded APP_VERSION
in our deployment.yaml
. For real configuration, you should use ConfigMaps (for non-sensitive data) and Secrets (for sensitive data like API keys or database passwords). You can then mount these as environment variables or files into your pod.
Persistent Storage with PersistentVolumes
Our app is stateless. If your app needs to store data (e.g., user uploads, a database), you’ll need PersistentVolumes (PVs) and PersistentVolumeClaims (PVCs). K3s has a built-in local path provisioner that makes this easy to start with.
Scaling Your FastAPI Application
Need to handle more traffic? Scaling is as simple as:
# Scale from 2 to 5 replicas
kubectl scale deployment fastapi-deployment --replicas=5
Kubernetes will automatically roll out 3 new pods. You can also set up a HorizontalPodAutoscaler (HPA) to automatically scale your deployment based on CPU or memory usage.
CI/CD Pipeline
The next logical step is to automate this entire process. A CI/CD pipeline (using tools like GitHub Actions, GitLab CI, or Jenkins) would:
- Run tests on your Python code.
- Build and tag the Docker image with a unique tag (e.g., the Git commit SHA).
- Push the image to your container registry.
- Update your
deployment.yaml
to use the new image tag. - Apply the new manifest to your cluster (
kubectl apply -f ...
), triggering a rolling update.
Frequently Asked Questions
Q: K3s vs. “full” K8s (like GKE, EKS, or kubeadm
)?
A: K3s is 100% K8s-compliant. Any manifest that works on K3s will work on a full cluster. K3s is just lighter, faster to install, and has sensible defaults (like Traefik) included, making it ideal for development, edge, and many production workloads.
Q: Why not just use Docker Compose?
A: Docker Compose is excellent for single-host deployments. However, it lacks the features of Kubernetes, such as:
- Self-healing: K8s will restart pods if they crash.
- Rolling updates: K8s updates pods one by one with zero downtime.
- Advanced networking: K8s provides a sophisticated service discovery and ingress layer.
- Scalability: K8s can scale your app across multiple servers (nodes).
K3s gives you all this power in a lightweight package.
Q: How should I run Uvicorn in production? With Gunicorn?
A: While uvicorn
can run on its own, it’s a common practice to use gunicorn
as a process manager to run multiple uvicorn
workers. This is a robust setup for production. You would change your Dockerfile
‘s CMD
to something like:
CMD ["gunicorn", "-k", "uvicorn.workers.UvicornWorker", "-w", "4", "-b", "0.0.0.0:8000", "main:app"]
.
The number of workers (-w 4
) is usually set based on the available CPU cores.
Q: How do I manage database connections from my FastAPI app in K3s?
A: You would typically deploy your database (e.g., PostgreSQL) as its own Deployment and Service within the K3s cluster. Then, your FastAPI application would connect to it using its internal K8s Service name (e.g., postgres-service
). Database credentials should *always* be stored in K8s Secrets.

Conclusion
Congratulations! You have successfully mastered a powerful, modern stack. You’ve learned how to build a performant FastAPI application, create an optimized multi-stage Docker image, and deploy it on a lightweight K3s Kubernetes cluster. You’ve seen how to use Deployments for self-healing, Services for internal networking, and Ingress for external access.
The ability to Deploy FastAPI Docker K3s is an incredibly valuable skill that bridges the gap between development and operations. This stack provides the speed of Python async, the portability of containers, and the power of Kubernetes orchestration, all in a developer-friendly and resource-efficient package. From here, you are well-equipped to build and scale robust, cloud-native applications. Thank you for reading the DevopsRoles page!