惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

人人都是产品经理
人人都是产品经理
博客园_首页
IT之家
IT之家
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Vercel News
Vercel News
美团技术团队
D
Docker
WordPress大学
WordPress大学
T
Tailwind CSS Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
The Cloudflare Blog
Y
Y Combinator Blog
F
Fortinet All Blogs
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
G
Google Developers Blog
爱范儿
爱范儿
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
月光博客
月光博客
MongoDB | Blog
MongoDB | Blog
S
SegmentFault 最新的问题
GbyAI
GbyAI
Hugging Face - Blog
Hugging Face - Blog
Microsoft Azure Blog
Microsoft Azure Blog
A
About on SuperTechFans

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Zero to Kubernetes Part 4: FastAPI Backend + PostgreSQL i...
jesus manriq · 2026-05-20 · via DEV Community

jesus manrique

K8s Terraform ArgoCD — Header

Series: Zero to Kubernetes — Part 1 · Part 2 · Part 3 · Part 4 · Part 5


We now have a cluster with ingress, TLS, and GitOps. Time to put real workloads on it: a PostgreSQL database managed by an operator, and a FastAPI backend that connects to it, runs migrations on startup, and exposes a REST API with health checks.

This is the part where the tutorial stops being "look how pretty the empty cluster is" and starts being "this actually works in production."


Components of This Part

┌─────────────────────────────────────────┐
│          api.yourdomain.com              │
│                │                         │
│    ┌───────────▼───────────┐             │
│    │   Ingress             │             │
│    │   (nginx + TLS)       │             │
│    └───────────┬───────────┘             │
│                │                         │
│    ┌───────────▼───────────┐             │
│    │   Backend Service     │             │
│    │   (ClusterIP)         │             │
│    └───────────┬───────────┘             │
│                │                         │
│    ┌───────────▼───────────┐             │
│    │   Backend Deployment  │             │
│    │   2 replicas          │             │
│    │   FastAPI + Uvicorn   │             │
│    └───────────┬───────────┘             │
│                │                         │
│    ┌───────────▼───────────┐             │
│    │   PostgreSQL          │             │
│    │   (CloudNativePG)     │             │
│    │   1 instance          │             │
│    │   PVC 10Gi            │             │
│    └───────────────────────┘             │
└─────────────────────────────────────────┘

Enter fullscreen mode Exit fullscreen mode


Step 1: CloudNativePG — PostgreSQL as a Native Kubernetes Resource

CloudNativePG (CNPG) is an operator that manages PostgreSQL on Kubernetes without manual StatefulSets. You install it and create a Cluster resource. The operator handles replication, backups, failover, and updates.

resource "helm_release" "cnpg" {
  name       = "cloudnative-pg"
  namespace  = kubernetes_namespace.infrastructure.metadata[0].name
  repository = "https://cloudnative-pg.github.io/charts"
  chart      = "cloudnative-pg"
  version    = "0.22.0"
}

Enter fullscreen mode Exit fullscreen mode

Then define the database as code in our GitOps repo:

gitops/apps/database/postgresql-cluster.yaml:

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: guayoyo-db
  namespace: production
spec:
  instances: 1
  imageName: ghcr.io/cloudnative-pg/postgresql:16.4

  bootstrap:
    initdb:
      database: guayoyo
      owner: app
      secret:
        name: db-credentials

  storage:
    size: 10Gi
    storageClass: local-path

  resources:
    requests:
      memory: "256Mi"
      cpu: "250m"
    limits:
      memory: "512Mi"
      cpu: "500m"

  monitoring:
    enablePodMonitor: false

Enter fullscreen mode Exit fullscreen mode

gitops/apps/database/db-credentials-secret.yaml:

apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
  namespace: production
type: kubernetes.io/basic-auth
stringData:
  username: app
  password: "ChangeThisInProductionWithSealedSecrets"

Enter fullscreen mode Exit fullscreen mode

⚠️ In real production: Never put passwords in plain text in git. Use SealedSecrets (encrypts the entire Secret), ExternalSecrets (syncs from Vault), or SOPS + Age.


Step 2: Backend Code (FastAPI)

main.py:

from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import asyncpg
import os
import time
from contextlib import asynccontextmanager

DB_HOST = os.getenv("DB_HOST", "guayoyo-db-rw")
DB_PORT = os.getenv("DB_PORT", "5432")
DB_NAME = os.getenv("DB_NAME", "guayoyo")
DB_USER = os.getenv("DB_USER", "app")
DB_PASSWORD = os.getenv("DB_PASSWORD", "")

pool = None

@asynccontextmanager
async def lifespan(app: FastAPI):
    global pool
    max_retries = 10
    for attempt in range(max_retries):
        try:
            pool = await asyncpg.create_pool(
                host=DB_HOST, port=DB_PORT, database=DB_NAME,
                user=DB_USER, password=DB_PASSWORD,
                min_size=2, max_size=10
            )
            async with pool.acquire() as conn:
                await conn.execute("""
                    CREATE TABLE IF NOT EXISTS items (
                        id SERIAL PRIMARY KEY,
                        name TEXT NOT NULL,
                        description TEXT,
                        created_at TIMESTAMPTZ DEFAULT NOW()
                    )
                """)
            break
        except Exception as e:
            if attempt == max_retries - 1:
                raise
            time.sleep(3)
    yield
    await pool.close()

app = FastAPI(title="Guayoyo API", lifespan=lifespan)

app.add_middleware(
    CORSMiddleware,
    allow_origins=[
        "https://admin.yourdomain.com",
        "https://app.yourdomain.com",
    ],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

class ItemCreate(BaseModel):
    name: str
    description: str = ""

class Item(ItemCreate):
    id: int
    created_at: str

@app.get("/health")
async def health():
    try:
        async with pool.acquire() as conn:
            await conn.fetchval("SELECT 1")
        return {"status": "ok", "db": "connected", "timestamp": time.time()}
    except Exception as e:
        return {"status": "degraded", "db": str(e)}

@app.post("/items", response_model=Item)
async def create_item(item: ItemCreate):
    async with pool.acquire() as conn:
        row = await conn.fetchrow(
            "INSERT INTO items (name, description) VALUES ($1, $2) RETURNING *",
            item.name, item.description
        )
    return Item(id=row["id"], name=row["name"],
               description=row["description"] or "",
               created_at=str(row["created_at"]))

@app.get("/items")
async def list_items():
    async with pool.acquire() as conn:
        rows = await conn.fetch("SELECT * FROM items ORDER BY created_at DESC")
    return [{"id": r["id"], "name": r["name"],
             "description": r["description"] or "",
             "created_at": str(r["created_at"])} for r in rows]

@app.get("/items/{item_id}")
async def get_item(item_id: int):
    async with pool.acquire() as conn:
        row = await conn.fetchrow("SELECT * FROM items WHERE id = $1", item_id)
    if not row:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"id": row["id"], "name": row["name"],
            "description": row["description"] or "",
            "created_at": str(row["created_at"])}

@app.delete("/items/{item_id}")
async def delete_item(item_id: int):
    async with pool.acquire() as conn:
        result = await conn.execute("DELETE FROM items WHERE id = $1", item_id)
    if result == "DELETE 0":
        raise HTTPException(status_code=404, detail="Item not found")
    return {"status": "deleted"}

Enter fullscreen mode Exit fullscreen mode

Dockerfile:

FROM python:3.12-slim AS builder
WORKDIR /app
RUN pip install --no-cache-dir poetry
COPY pyproject.toml poetry.lock ./
RUN poetry config virtualenvs.create false && poetry install --no-dev

FROM python:3.12-slim
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY . .
EXPOSE 8000
HEALTHCHECK --interval=10s --timeout=5s --retries=3 \
  CMD curl -f http://localhost:8000/health || exit 1
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Enter fullscreen mode Exit fullscreen mode

Build and push:

docker build -t ghcr.io/your-org/backend:v1.0.0 .
docker push ghcr.io/your-org/backend:v1.0.0

Enter fullscreen mode Exit fullscreen mode


Step 3: Deploy to Kubernetes via ArgoCD

gitops/apps/backend/deployment.yaml (key sections):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend
  namespace: production
spec:
  replicas: 2
  selector:
    matchLabels:
      app: backend
  template:
    metadata:
      labels:
        app: backend
    spec:
      serviceAccountName: apps-sa
      containers:
        - name: backend
          image: ghcr.io/your-org/backend:v1.0.0
          ports:
            - containerPort: 8000
          env:
            - name: DB_HOST
              value: guayoyo-db-rw.production.svc.cluster.local
            - name: DB_PORT
              value: "5432"
            - name: DB_NAME
              value: guayoyo
            - name: DB_USER
              valueFrom:
                secretKeyRef:
                  name: db-credentials
                  key: username
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: db-credentials
                  key: password
          resources:
            requests:
              memory: "128Mi"
              cpu: "100m"
            limits:
              memory: "256Mi"
              cpu: "500m"
          livenessProbe:
            httpGet:
              path: /health
              port: 8000
            initialDelaySeconds: 10
            periodSeconds: 15
          readinessProbe:
            httpGet:
              path: /health
              port: 8000
            initialDelaySeconds: 5
            periodSeconds: 10

Enter fullscreen mode Exit fullscreen mode

Key points:

  • guayoyo-db-rw.production.svc.cluster.local — internal DNS that CloudNativePG creates automatically. -rw = read-write (primary). If you had read replicas, they'd be at -ro.
  • secretKeyRef injects username/password from the db-credentials Secret without exposing them in the Deployment
  • livenessProbe and readinessProbe — Kubernetes uses these to know if the pod is alive and if it should receive traffic

gitops/apps/backend/ingress.yaml:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: backend
  namespace: production
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-production
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - api.yourdomain.com
      secretName: api-tls
  rules:
    - host: api.yourdomain.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: backend
                port:
                  number: 80

Enter fullscreen mode Exit fullscreen mode


Step 4: End-to-End Verification

# Verify ArgoCD synced everything
argocd app list
# backend  Healthy  Synced
# database Healthy  Synced

# Test the API
curl https://api.yourdomain.com:30443/health
# {"status":"ok","db":"connected","timestamp":1716163200.123}

# Create an item
curl -X POST https://api.yourdomain.com:30443/items \
  -H "Content-Type: application/json" \
  -d '{"name":"Test Item","description":"From curl"}'
# {"id":1,"name":"Test Item","description":"From curl",...}

# List items
curl https://api.yourdomain.com:30443/items
# [{"id":1,"name":"Test Item",...}]

# Check that both replicas are running
kubectl get pods -n production -l app=backend
# NAME                      READY   STATUS    RESTARTS   AGE
# backend-xxxxx-xxxxx       1/1     Running   0          1m
# backend-xxxxx-yyyyy       1/1     Running   0          1m

Enter fullscreen mode Exit fullscreen mode


What You Learned in This Part

  • CloudNativePG as a PostgreSQL operator for Kubernetes
  • Kubernetes internal DNS: guayoyo-db-rw.production.svc.cluster.local
  • Secrets for credentials (with serious note about SealedSecrets/ExternalSecrets)
  • FastAPI with connection pooling to PostgreSQL via asyncpg
  • Liveness and readiness probes — the difference and why both matter
  • CORS configured to accept traffic from frontends
  • Multi-stage Docker build optimized for production

In Part 5, we'll deploy both frontends (React + Vue), connect them to the backend, and run a full integration test where one app creates data and the other consumes it in real time.


At Guayoyo Tech, we design backends that don't crash under real traffic. FastAPI, Go, Node.js — whatever your project needs, with PostgreSQL, health checks, metrics, and automated Kubernetes deployment. Talk to us free for 15 minutes and we'll show you how.