Run Flask on Kubernetes

Khimananda Oli 8 min read Programming and Languages
Run Flask on Kubernetes

By Khimananda Oli | Last reviewed: August 2026

Deploying Python web applications requires bridging the gap between synchronous frameworks and distributed orchestration. To successfully run Flask on Kubernetes, you must treat the framework as a library served by a production WSGI server like Gunicorn, not a standalone script. This guide covers the exact containerization, manifest configuration, and operational guardrails needed to move beyond local development into a resilient cluster environment.

How do you containerize Flask for Kubernetes production?

The most common failure point when teams attempt to run Flask on Kubernetes is shipping a development-grade artifact. Flask’s built-in server is single-threaded, lacks security hardening, and cannot handle concurrent requests efficiently. In my experience auditing SOC 2 environments, finding a raw flask run command in a production pod is an immediate compliance flag. You need a proper application server interface.

Kubernetes Pod BoundaryGunicorn MasterWorker 1 (Sync)Worker 2 (Sync)Worker N (Sync)Flask App Code(WSGI Callable)Readiness ProbeGET /healthz/readyReturns 200 only whenDB + Cache connectedLiveness ProbeGET /healthz/liveLightweight checkProcess not deadlocked
Production Flask pod architecture with Gunicorn workers and distinct health probe endpoints

Your Dockerfile should follow a multi-stage pattern to minimize attack surface and image size. Start with a builder stage to compile dependencies, then copy only the runtime artifacts to a slim base image. Always pin your Python version and dependency hashes for reproducible builds—a critical requirement for audit trails.

# Dockerfile.production
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /install /usr/local
COPY . .
RUN useradd -m appuser && chown -R appuser:appuser /app
USER appuser
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "4", "--threads", "2", "wsgi:app"]

Note the explicit worker count. A good starting formula for CPU-bound Flask apps is (2 × CPU_CORES) + 1. For I/O-bound workloads typical in API services, consider adding threads or switching to async workers like gevent. Never leave worker counts unconfigured in production; default settings rarely match your cluster's resource allocation.

What Kubernetes resources are required to run Flask reliably?

Kubernetes manifests for Flask require more than just a Deployment and Service. You need deliberate configuration for resource management, secret handling, and traffic routing. If you're new to cluster fundamentals, review Kubernetes basics: deploy your first app before proceeding to production patterns.

Resource requests and limits

Flask applications can have unpredictable memory profiles depending on request payload size and library usage. Without explicit boundaries, a single memory leak can destabilize an entire node. Set requests based on observed P95 usage during load testing, and limits at 1.5–2× that value to allow burst capacity without OOM kills.

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

For deeper guidance on right-sizing these values, see Kubernetes resource limits and requests. Undersized limits cause throttling and latency spikes; oversized requests waste cluster budget and trigger unnecessary autoscaling.

Secrets and configuration

Never bake credentials into your Flask container image. Use Kubernetes Secrets mounted as environment variables or volume files. For sensitive deployments, integrate with external secret stores like HashiCorp Vault or AWS Secrets Manager. The pattern described in Kubernetes secrets management done right prevents credential leakage in image layers and logs.

Ingress and TLS termination

Flask should never terminate TLS directly in production. Let your Ingress controller handle certificate management and HTTPS termination, forwarding plain HTTP to the pod on port 8000. This simplifies certificate rotation and keeps your application code focused on business logic rather than cryptographic operations.

How do you configure health checks for Flask pods?

Health probes are the difference between self-healing infrastructure and silent failures. Flask applications need two distinct endpoints: one for liveness (is the process alive?) and one for readiness (can it serve traffic?). Conflating these causes cascading outages during deployments and dependency failures.

KubeletProbe Request/healthz/liveNo DB callsReturns 200 instantly200 OKPod stays runningTraffic continuesNon-200 / TimeoutContainer restartedReadiness Probe (/healthz/ready)Checks DB, Redis, external depsFailure = remove from Service endpoints
Decision flow distinguishing Flask liveness and readiness probe behaviors in Kubernetes

Your liveness endpoint should be intentionally simple. It confirms the Gunicorn worker process is responsive but does not verify database connectivity or cache availability. If your liveness check depends on downstream services, a transient network blip will trigger pod restarts instead of graceful degradation.

@app.route('/healthz/live')
def liveness():
    return jsonify(status='ok'), 200

@app.route('/healthz/ready')
def readiness():
    try:
        db.session.execute(text('SELECT 1'))
        redis_client.ping()
        return jsonify(status='ready'), 200
    except Exception as e:
        current_app.logger.error(f'Readiness failed: {e}')
        return jsonify(status='unavailable'), 503

Configure probe timing carefully. Set initialDelaySeconds high enough for Gunicorn workers to boot and establish connections—typically 10–15 seconds for Flask apps with ORM initialization. Use periodSeconds: 10 and failureThreshold: 3 to tolerate brief hiccups without premature restarts. If you're debugging repeated restarts, consult debug a CrashLoopBackOff in Kubernetes for systematic diagnosis.

How does Flask autoscaling differ from other frameworks?

Horizontal Pod Autoscaler (HPA) behavior depends heavily on your workload characteristics. Flask’s synchronous nature means each worker handles one request at a time (unless threaded), making CPU utilization a more reliable scaling metric than for async frameworks. Memory-based scaling is riskier because Python’s garbage collection can cause delayed reclamation, leading to laggy scale-down events.

Scaling MetricFlask SuitabilityRecommended ThresholdCaveats
CPU UtilizationHigh60–70%Best for compute-heavy serialization/parsing workloads
Memory UtilizationMedium70–75%GC delays cause oscillation; pair with CPU metric
Custom (Requests/sec)HighestPer-worker throughput capRequires Prometheus adapter; most accurate for I/O-bound APIs
Queue DepthSituationalTask-specific thresholdOnly relevant if Flask consumes from Celery/RQ backends

For custom metrics, instrument your Flask app with OpenTelemetry and export request rate counters. The approach in instrument an app with OpenTelemetry gives you granular visibility without vendor lock-in. Configure HPA with both CPU and custom metrics using MetricSpec arrays to prevent under-provisioning during mixed workload patterns.

What security hardening is mandatory for Flask containers?

Running Flask on Kubernetes introduces shared-tenant risks that don't exist in single-server deployments. Apply defense-in-depth at every layer. Start with Pod Security Standards (PSS) enforced at the namespace level—baseline or restricted profiles prevent privilege escalation and host namespace access.

  • Non-root execution: Your Dockerfile must create and switch to an unprivileged user. Gunicorn drops privileges automatically when started as root, but explicit USER directives prevent accidental misconfiguration.
  • Read-only filesystem: Mount /tmp as an emptyDir volume and set readOnlyRootFilesystem: true in your security context. This prevents attackers from writing persistent payloads if they achieve RCE.
  • Network policies: Restrict egress to only required destinations (database, cache, external APIs). Default-deny ingress except from your Ingress controller. See Kubernetes network policies explained for implementation patterns.
  • Dependency scanning: Integrate Trivy or Grype into your CI pipeline. Fail builds on critical CVEs. Flask extensions often pull transitive dependencies with known vulnerabilities—automated scanning catches these before deployment.
  • Secret rotation: Implement automated credential rotation. Static database passwords in Kubernetes Secrets violate SOC 2 controls. Use sealed secrets or external operators that sync with managed secret services.
Insecure Deployment❌ Running as root userFull container escape potential❌ No resource limitsNoisy neighbor + DoS vector❌ Plaintext env secretsExposed in pod spec + logs❌ Unrestricted networkLateral movement possible❌ Dev server in prodDebugger PIN exposure riskHardened Deployment✅ Non-root + readOnly FSPrivilege escalation blocked✅ Requests + limits setPredictable scheduling✅ External secrets operatorAuto-rotation + encryption✅ NetworkPolicy egress denyZero-trust pod networking✅ Gunicorn + pinned depsSBOM verified in CI
Security posture comparison for Flask Kubernetes deployments highlighting hardening controls

Run Flask on Kubernetes with confidence

Successfully operating Flask in a cluster demands discipline across containerization, manifest design, observability, and security. The patterns above reflect real production deployments I've architected for teams ranging from Kathmandu startups to multinational SaaS platforms. Start with the multi-stage Dockerfile and health probe separation—these two changes eliminate the majority of early-stage failures. From there, layer in resource governance, network segmentation, and automated secret management as your compliance requirements mature. If your team needs hands-on guidance implementing these patterns or preparing for SOC 2 audits on Kubernetes infrastructure, reach out through my contact page to discuss your specific architecture.

Frequently Asked Questions

Use a multi-stage Dockerfile with Python 3.12 slim base. Install dependencies via pip, copy application code, and set Gunicorn as the entrypoint. Avoid running as root by creating a non-privileged user in the final image stage.

Gunicorn with gevent workers is standard for 2026 deployments. It handles concurrent requests efficiently within pods. Configure worker count based on CPU limits, typically two to four workers per core to prevent memory exhaustion during traffic spikes.

Expose a lightweight /health endpoint returning HTTP 200 without database calls. Configure liveness probes to detect deadlocks and readiness probes to verify dependency connectivity. Set initialDelaySeconds appropriately to prevent premature pod termination during slow Flask cold starts.

Store credentials in Kubernetes Secrets or external vaults like HashiCorp Vault. Mount them as environment variables or volume files. Never commit secrets to container images. Use sealed-secrets or external-secrets operator for GitOps-compatible secret management across clusters.

Yes, when paired with Horizontal Pod Autoscaler. Configure HPA based on custom metrics like request latency or CPU utilization. Ensure Flask remains stateless by storing sessions in Redis and using shared storage for uploads to enable safe scaling.

Start with 256Mi memory and 250m CPU requests for small apps. Profile your specific workload using kubectl top pods under load. Set limits at twice the requests to prevent OOM kills while allowing burst capacity during traffic spikes.

Serve static assets through Nginx ingress or CDN rather than Flask directly. Mount static files as read-only volumes or build them into a separate Nginx container. This reduces Flask pod memory usage and improves response times significantly.

Gunicorn workers consume significant memory. Reduce worker count or switch to async workers. Check for memory leaks using tracemalloc. Increase pod memory limits gradually while monitoring actual usage patterns with Prometheus metrics to find optimal allocation.

Execute migrations as a Kubernetes Job before deploying new app versions. Use init containers or Helm pre-upgrade hooks to ensure schema changes complete successfully. Never run migrations inside application pods to avoid race conditions during rolling updates.

Blueprints organize code but do not create true microservices. For Kubernetes, deploy separate Flask applications with independent scaling and failure domains. Use blueprints only for modularizing monolithic components within a single service boundary.

Terminate TLS at the ingress controller level using cert-manager for automatic certificate provisioning. Configure Flask to trust proxy headers via ProxyFix middleware. Internal pod-to-pod communication can remain HTTP to reduce encryption overhead and latency.

Output structured JSON logs to stdout for collection by Fluent Bit or Vector. Include request IDs, timestamps, and log levels. Avoid file-based logging since pod filesystems are ephemeral. Use correlation IDs to trace requests across multiple services.

Check pod events with kubectl describe pod and logs with kubectl logs. Verify configmap mounts and environment variables are correct. Test the container locally with identical settings. Use ephemeral debug containers for inspecting running pods without restarting them.

Yes, with proper async workers and caching. Flask handles thousands of RPS when optimized. Consider FastAPI only if you need native async support. Most bottlenecks stem from database queries or external APIs, not the framework itself.

Use rolling updates with maxSurge and maxUnavailable configured. Implement graceful shutdown handlers to finish active requests before termination. Set preStop hooks with sleep delays to allow load balancers to deregister pods before SIGTERM reaches the application process.