Archived copy of an article by Omer Sen, originally published on LinkedIn on 2025-11-11.
Original: https://www.linkedin.com/pulse/k3d-docker-sample-app-helm-chart-omer-sen-tq3ne/ · ← back to faruk.net

K3D in A Docker Sample App and Helm Chart

A production-ready Wikipedia-like API service with FastAPI, PostgreSQL, Prometheus, and Grafana, packaged as a Helm chart for Kubernetes and containerized using k3d with Docker-outside-of-Docker (DooD) approach.

Features

Architecture

The solution consists of:

Prerequisites

Installing Required Tools

If you don't have Docker, kubectl, k3d, or Helm installed, you can use the provided bootstrap script:

# Run the bootstrap script (requires sudo)
./bootstrap.sh

The bootstrap script will:

Note: After Docker installation, you may need to log out and log back in (or run newgrp docker) for the docker group changes to take effect.

Quick Start

Option 1: Run Complete Cluster in Docker (Recommended)

This runs the entire Kubernetes cluster inside a Docker container using k3d:

# Build the complete cluster image
docker build -t wiki-cluster .

# Run the cluster (requires privileged mode and host network for port access)
# Mount docker.sock to use host's Docker daemon (Docker-outside-of-Docker)
# Use -d to run in detached mode, --name to give it a name
docker run --privileged --network host -v /var/run/docker.sock:/var/run/docker.sock -d --name wiki-cluster-container wiki-cluster

The setup script will:

Note:

Option 2: Build and Deploy Manually

Step 1: Build the FastAPI Service Image

cd wiki-service
docker build -t wiki-service:latest .
cd ..

Step 2: Deploy Using Helm Chart

If you have a Kubernetes cluster available:

# Load the image into your cluster (if using k3d)
k3d image import wiki-service:latest

# Install the Helm chart
cd wiki-chart
helm install wiki-release . --wait
cd ..

Or customize the image name:

# Edit values.yaml to set fastapi.image_name
# Then install
helm install wiki-release . --wait

Accessing Services

Once the cluster is running, access services through the ingress at http://localhost:8080 or http://<your-machine-ip>:8080:

FastAPI Endpoints

Grafana Dashboard

The dashboard displays:

Dashboard Preview:

The dashboard shows real-time metrics for user and post creation rates, updated every 5 seconds.

API Endpoints

POST /users

Create a new user.

Request body:

{
  "name": "John Doe"
}

Response:

{
  "id": 1,
  "name": "John Doe",
  "created_time": "2025-11-04T10:30:00"
}

POST /posts

Create a new post under a given user.

Request body:

{
  "user_id": 1,
  "content": "This is my first post!"
}

Response:

{
  "post_id": 1,
  "content": "This is my first post!",
  "user_id": 1,
  "created_time": "2025-11-04T10:35:00"
}

GET /user/{id}

Fetch a user by ID.

Response:

{
  "id": 1,
  "name": "John Doe",
  "created_time": "2025-11-04T10:30:00"
}

GET /posts/{id}

Fetch a post by ID.

Response:

{
  "post_id": 1,
  "content": "This is my first post!",
  "user_id": 1,
  "created_time": "2025-11-04T10:35:00"
}

GET /metrics

Prometheus metrics endpoint for monitoring.

Exposed Metrics:

Response: Prometheus text-based exposition format

Example Usage with curl

Create a user:

curl -X POST "http://localhost:8080/users" \
  -H "Content-Type: application/json" \
  -d '{"name": "John Doe"}'

Create a post:

curl -X POST "http://localhost:8080/posts" \
  -H "Content-Type: application/json" \
  -d '{"user_id": 1, "content": "Hello, World!"}'

Get a user:

curl "http://localhost:8080/user/1"

Get a post:

curl "http://localhost:8080/posts/1"

Get Prometheus metrics:

curl "http://localhost:8080/metrics"

Local Development (Without Docker)

For local development without Docker/Kubernetes:

pip install -r requirements.txt
uvicorn app.main:app --reload

The API will be available at http://localhost:8000

Note: Local development uses SQLite by default. For PostgreSQL, set the DATABASE_URL environment variable.

Configuration

Environment Variables

Helm Chart Configuration

Edit wiki-chart/values.yaml to customize:

Example:

fastapi:
  image_name: wiki-service
  image_tag: latest
  replicas: 1

postgresql:
  database: wiki_db
  username: wiki_user
  password: wiki_password

grafana:
  admin_user: admin
  admin_password: admin

dataGenerator:
  enabled: true
  userCount: 10
  postsPerUser: 5
  cronjob:
    enabled: true              # Set to true for CronJob, false for one-time Job
    schedule: "*/5 * * * *"     # Cron schedule (every 5 minutes in this example)

Cron Schedule Examples:

Resource Constraints

The entire cluster is configured to use at most:

Project Structure

.
├── app/                    # FastAPI application source
│   ├── __init__.py
│   ├── main.py            # FastAPI application and endpoints
│   ├── models.py          # SQLAlchemy models
│   ├── schemas.py         # Pydantic schemas
│   ├── database.py        # Database configuration
│   └── metrics.py         # Prometheus metrics
├── wiki-service/          # FastAPI service Docker setup
│   ├── Dockerfile
│   ├── requirements.txt
│   ├── pyproject.toml
│   └── app/               # Application code (copied from root)
├── wiki-chart/            # Helm chart
│   ├── Chart.yaml
│   ├── values.yaml
│   └── templates/
│       ├── fastapi-deployment.yaml
│       ├── postgresql-deployment.yaml
│       ├── prometheus-deployment.yaml
│       ├── prometheus-configmap.yaml
│       ├── grafana-deployment.yaml
│       ├── grafana-configmap.yaml
│       ├── ingress.yaml
│       ├── data-generator-job.yaml
│       └── data-generator-cronjob.yaml
├── Dockerfile             # Top-level Dockerfile for k3d cluster
├── bootstrap.sh           # Script to install required tools (Docker, kubectl, k3d, Helm)
├── setup-cluster.sh       # Cluster setup script
└── README.md             # This file

Cleanup and Destroy

Option 1: Stop Docker Container (Recommended)

If you ran the cluster using the Docker container approach:

# Find the running container
docker ps | grep wiki-cluster-container

# Stop the container (cluster will still exist)
docker stop wiki-cluster-container

# Remove the container
docker rm wiki-cluster-container

Option 2: Clean Up k3d Cluster Manually

If you deployed manually using k3d:

# Delete the k3d cluster
k3d cluster delete wiki-cluster

# Remove the wiki-service image (optional)
docker rmi wiki-service:latest

Option 3: Uninstall Helm Chart

If you want to keep the cluster but remove the application:

# Uninstall the Helm chart (run inside container)
docker exec wiki-cluster-container sh -c 'export KUBECONFIG=$(k3d kubeconfig write wiki-cluster 2>/dev/null); helm uninstall wiki-release'

# Or if container is stopped, delete cluster and recreate
k3d cluster delete wiki-cluster

Complete Cleanup

To remove everything (cluster, images, and containers):

# Stop and remove Docker container
docker stop wiki-cluster-container 2>/dev/null || true
docker rm wiki-cluster-container 2>/dev/null || true

# Delete k3d cluster
k3d cluster delete wiki-cluster 2>/dev/null || true

# Remove Docker images
docker rmi wiki-cluster:latest 2>/dev/null || true
docker rmi wiki-service:latest 2>/dev/null || true

# Clean up any dangling containers/images
docker container prune -f
docker image prune -f

Viewing Logs and Debugging

# View container logs
docker logs wiki-cluster-container

# Follow logs in real-time
docker logs -f wiki-cluster-container

# Execute commands inside container
docker exec -it wiki-cluster-container sh

# Run kubectl commands inside container
docker exec wiki-cluster-container sh -c 'export KUBECONFIG=$(k3d kubeconfig write wiki-cluster 2>/dev/null); kubectl get pods -A'

Note: Traefik ingress is part of k3d and doesn't need separate cleanup. It will be removed when you delete the k3d cluster.

Troubleshooting

Docker Connection Issues

If the cluster doesn't start:

Port 8080 Already in Use

If you see "port is already allocated" or "Bind for 0.0.0.0:8080 failed" error:

# Check what's using port 8080
sudo lsof -i :8080
# or
sudo netstat -tulpn | grep :8080

# Stop the service using port 8080, or use a different port
# To use a different port, modify the k3d cluster creation in setup-cluster.sh:
# Change --port "8080:80@loadbalancer" to --port "8081:80@loadbalancer"
# Then rebuild and run the container

Pods Not Starting

Check pod status (run kubectl commands inside the container):

# View all pods
docker exec wiki-cluster-container sh -c 'export KUBECONFIG=$(k3d kubeconfig write wiki-cluster 2>/dev/null); kubectl get pods -A'

# Check specific pod logs
docker exec wiki-cluster-container sh -c 'export KUBECONFIG=$(k3d kubeconfig write wiki-cluster 2>/dev/null); kubectl logs <pod-name>'

# Describe pod for troubleshooting
docker exec wiki-cluster-container sh -c 'export KUBECONFIG=$(k3d kubeconfig write wiki-cluster 2>/dev/null); kubectl describe pod <pod-name>'

Important: Always run kubectl commands inside the wiki-cluster-container to avoid conflicts with other Kubernetes clusters on your machine.

Ingress Not Working

Ensure Traefik is running (k3d default):

# Run kubectl commands inside the container
docker exec wiki-cluster-container sh -c 'export KUBECONFIG=$(k3d kubeconfig write wiki-cluster 2>/dev/null); kubectl get pods -n kube-system | grep traefik'

Check ingress resources:

docker exec wiki-cluster-container sh -c 'export KUBECONFIG=$(k3d kubeconfig write wiki-cluster 2>/dev/null); kubectl get ingress -A'

Database Connection Issues

Verify PostgreSQL is running and FastAPI has correct DATABASE_URL:

# Check FastAPI logs
docker exec wiki-cluster-container sh -c 'export KUBECONFIG=$(k3d kubeconfig write wiki-cluster 2>/dev/null); kubectl logs deployment/fastapi'

# Check PostgreSQL status
docker exec wiki-cluster-container sh -c 'export KUBECONFIG=$(k3d kubeconfig write wiki-cluster 2>/dev/null); kubectl get pods -l app=postgresql'

Prometheus Metrics

The service exposes Prometheus metrics at /metrics endpoint. These metrics can be scraped by a Prometheus server for monitoring and alerting.

Available Metrics:

Example Prometheus Configuration:

scrape_configs:
  - job_name: "user_post_api"
    static_configs:
      - targets: ["fastapi:8000"]
    metrics_path: "/metrics"

Database

The service uses PostgreSQL in production (via Helm chart) and SQLite for local development. The database connection is configured via the DATABASE_URL environment variable.

To switch databases: