Skip to main content
CleanStart

Knowledge Hub

End-to-End Secure Deployment: From Code to Production

The Complete Journey

This is the single most important tutorial. You'll take a simple Flask application from your laptop all the way to production, with security checks at every step. You'll write a simple Flask app, create a hardened Dockerfile, build and scan locally, generate a Software Bill of Materials, sign the image with cryptographic signatures, test locally with docker-compose, deploy to Kubernetes, and finally verify everything works in production.

Time: 45 minutes end-to-end

Prerequisites include Docker installed, kubectl installed for deployment to Kubernetes, Helm installed for package management, and a container registry account such as Docker Hub, GCR, or similar services.

Step 1: Write a Simple Flask Application

Create a new directory and set up your Flask project:

mkdir myflaskapp && cd myflaskapp

Create app.py:

#!/usr/bin/env python3from flask import Flask, jsonifyimport logging app = Flask(__name__) # Setup logginglogging.basicConfig(level=logging.INFO)logger = logging.getLogger(__name__) @app.route('/health', methods=['GET'])def health():    """Health check endpoint"""    return jsonify({"status": "healthy"}), 200 @app.route('/api/version', methods=['GET'])def version():    """Return app version"""    return jsonify({"version": "1.0.0", "app": "myflaskapp"}), 200 @app.route('/api/message', methods=['GET'])def message():    """Return a simple message"""    return jsonify({"message": "Hello from CleanStart!"}), 200 if __name__ == '__main__':    logger.info("Starting myflaskapp")    app.run(host='0.0.0.0', port=5000, debug=False)

Create requirements.txt:

Flask==2.3.0

Test locally (optional):

pip install -r requirements.txtpython app.py # In another terminal:curl http://localhost:5000/health# {"status":"healthy"}

Step 2: Create Multi-Stage Dockerfile with CleanStart Base

A proper Dockerfile uses multi-stage builds to separate build tooling from the final runtime image.

Create Dockerfile:

# Stage 1: Builder (has all dev tools)FROM cleanstart/python:3.11-dev as builder WORKDIR /build # Copy requirements and install into /usr/localCOPY requirements.txt .RUN pip install --user --no-cache-dir -r requirements.txt # Stage 2: Runtime (tiny, production-ready)FROM cleanstart/python:3.11-prod # Set working directoryWORKDIR /app # Copy installed packages from builderCOPY --from=builder /root/.local /root/.local # Update PATH to use user-installed packagesENV PATH=/root/.local/bin:$PATH \    PYTHONUNBUFFERED=1 \    PYTHONDONTWRITEBYTECODE=1 # Copy application codeCOPY app.py . # Create non-root user (CleanStart default: UID 65532)USER 65532 # Expose portEXPOSE 5000 # Health checkHEALTHCHECK --interval=10s --timeout=3s --start-period=5s \  CMD curl -f http://localhost:5000/health || exit 1 # Start applicationCMD ["python", "-m", "flask", "--app", "app", "run", "--host", "0.0.0.0", "--port", "5000"]

Why this Dockerfile is good: This Dockerfile follows production-ready best practices in every way. The multi-stage build approach ensures the final image contains no compiler or build tools, reducing its size from approximately 2GB to around 350MB. Using CleanStart base images provides pre-scanned, signed, and hardened foundations that have already been verified for security vulnerabilities. The non-root user (UID 65532) prevents privilege escalation attacks that could give attackers root access. The built-in health check enables orchestrators like Kubernetes to detect and respond to failures automatically. The minimal surface area, containing only Flask and its dependencies, reduces attack vectors and security maintenance overhead.

Step 3: Build Docker Image Locally

# Build the imagedocker build -t myflaskapp:1.0.0 . # Expected output:# [+] Building 45.2s (11/11) FINISHED# => => naming to docker.io/library/myflaskapp:1.0.0 # List the image (check size)docker images | grep myflaskapp# myflaskapp   1.0.0   a1b2c3d4   10 minutes ago   350MB

Step 4: Scan for Vulnerabilities

Use Grype to detect known vulnerabilities in your image.

# Install Grype (if not already installed)curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin # Scan the imagegrype myflaskapp:1.0.0 --fail-on high # Expected output (if no vulnerabilities):# 0 critical# 0 high# 0 medium# 0 low# All vulnerabilities have been remediated!

If vulnerabilities are found, show detailed reports and update dependencies as needed.

Step 5: Generate SBOM (Software Bill of Materials)

Create a record of every component in your image.

# Install Syft (if not already installed)curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin # Generate SBOMsyft myflaskapp:1.0.0 --output cyclonedx-json > sbom.json # View the SBOM (human-readable)jq '.components[] | {name, version}' sbom.json | head -20 # Store the SBOMcp sbom.json sbom-myflaskapp-1.0.0.jsongit add sbom-myflaskapp-1.0.0.jsongit commit -m "feat: add SBOM for myflaskapp:1.0.0"

Step 6: Sign the Image with Cosign

Prove that this image came from your trusted build system.

# Install Cosigncurl -sSL https://github.com/sigstore/cosign/releases/download/v2.0.0/cosign-linux-amd64 \  -o /usr/local/bin/cosign && chmod +x /usr/local/bin/cosign # Sign the image (using keyless signing for testing)COSIGN_EXPERIMENTAL=1 cosign sign --yes myflaskapp:1.0.0 # Expected output:# WARNING: experimental mode enabled. DO NOT use in production.# Signing myflaskapp:1.0.0# WARNING: Changing signing key to: ...# Successfully signed myflaskapp:1.0.0

Verify the signature:

COSIGN_EXPERIMENTAL=1 cosign verify --insecure-ignore-tlog myflaskapp:1.0.0 # Expected output:# The following checks were performed on each of these signatures:# - The cosign claims were validated# - Existence of the claims in the transparency log was verified offline

Step 7: Run Locally with Docker

Test that the image actually works before deploying anywhere.

# Run the containerdocker run -p 5000:5000 myflaskapp:1.0.0 # Expected output:# WARNING in flask.app: This is a development server. Do not use it in production.#  * Running on http://0.0.0.0:5000 # In another terminal, test the endpointscurl http://localhost:5000/health# {"status":"healthy"} curl http://localhost:5000/api/version# {"version":"1.0.0","app":"myflaskapp"} curl http://localhost:5000/api/message# {"message":"Hello from CleanStart!"} # Stop the container# Press Ctrl+C in the running container

Step 8: Deploy with Docker-Compose

Create a multi-service stack with Flask app, PostgreSQL, and Redis.

Create docker-compose.yml:

version: '3.8' services:  # Flask Application  app:    image: myflaskapp:1.0.0    ports:      - "5000:5000"    environment:      - DATABASE_URL=postgresql://myuser:mypass@db:5432/mydb      - REDIS_URL=redis://cache:6379/0      - FLASK_ENV=production    depends_on:      db:        condition: service_healthy      cache:        condition: service_healthy    healthcheck:      test: ["CMD", "curl", "-f", "http://localhost:5000/health"]      interval: 10s      timeout: 3s      retries: 3      start_period: 5s    networks:      - mynet    restart: unless-stopped    security_opt:      - no-new-privileges:true    read_only_root_filesystem: true    tmpfs:      - /tmp      - /var/tmp   # PostgreSQL Database  db:    image: postgres:15-alpine    environment:      - POSTGRES_USER=myuser      - POSTGRES_PASSWORD=mypass      - POSTGRES_DB=mydb    volumes:      - db_data:/var/lib/postgresql/data    healthcheck:      test: ["CMD-SHELL", "pg_isready -U myuser"]      interval: 10s      timeout: 3s      retries: 3    networks:      - mynet    restart: unless-stopped    security_opt:      - no-new-privileges:true   # Redis Cache  cache:    image: redis:7-alpine    healthcheck:      test: ["CMD", "redis-cli", "ping"]      interval: 10s      timeout: 3s      retries: 3    networks:      - mynet    restart: unless-stopped    security_opt:      - no-new-privileges:true volumes:  db_data: networks:  mynet:    driver: bridge

Create .env file:

DATABASE_URL=postgresql://myuser:mypass@db:5432/mydbREDIS_URL=redis://cache:6379/0FLASK_ENV=production

Deploy with docker-compose:

# Start all servicesdocker-compose up -d # Check statusdocker-compose ps # Test the appcurl http://localhost:5000/health# {"status":"healthy"} # View logsdocker-compose logs app # Stop all servicesdocker-compose down

Step 9: Push to Container Registry

Upload your image so it can be deployed anywhere.

# Option A: Docker Hubdocker login docker.iodocker tag myflaskapp:1.0.0 username/myflaskapp:1.0.0docker push username/myflaskapp:1.0.0 # Option B: Google Container Registrygcloud auth configure-docker gcr.iodocker tag myflaskapp:1.0.0 gcr.io/my-project/myflaskapp:1.0.0docker push gcr.io/my-project/myflaskapp:1.0.0 # Option C: GitHub Container Registryecho $GITHUB_TOKEN | docker login ghcr.io -u USERNAME --password-stdindocker tag myflaskapp:1.0.0 ghcr.io/myusername/myflaskapp:1.0.0docker push ghcr.io/myusername/myflaskapp:1.0.0 # Verify it's in the registrydocker pull username/myflaskapp:1.0.0

Step 10: Deploy to Kubernetes

Deploy to a Kubernetes cluster.

Create deployment.yaml:

apiVersion: apps/v1kind: Deploymentmetadata:  name: myflaskapp  labels:    app: myflaskappspec:  replicas: 3  strategy:    type: RollingUpdate    rollingUpdate:      maxSurge: 1      maxUnavailable: 0  selector:    matchLabels:      app: myflaskapp  template:    metadata:      labels:        app: myflaskapp      annotations:        container.apparmor.security.beta.kubernetes.io/app: runtime/default    spec:      serviceAccountName: default      securityContext:        runAsNonRoot: true        runAsUser: 65532        runAsGroup: 65532        fsGroup: 65532        seccompProfile:          type: RuntimeDefault      containers:      - name: app        image: username/myflaskapp:1.0.0        imagePullPolicy: IfNotPresent        ports:        - containerPort: 5000          protocol: TCP        env:        - name: FLASK_ENV          value: production        - name: DATABASE_URL          valueFrom:            secretKeyRef:              name: myflaskapp-secrets              key: database-url        resources:          requests:            memory: "128Mi"            cpu: "100m"          limits:            memory: "256Mi"            cpu: "500m"        livenessProbe:          httpGet:            path: /health            port: 5000          initialDelaySeconds: 5          periodSeconds: 10          timeoutSeconds: 3          failureThreshold: 3        readinessProbe:          httpGet:            path: /health            port: 5000          initialDelaySeconds: 5          periodSeconds: 5          timeoutSeconds: 3          failureThreshold: 3        securityContext:          allowPrivilegeEscalation: false          capabilities:            drop:              - ALL          readOnlyRootFilesystem: true          runAsNonRoot: true        volumeMounts:        - name: tmp          mountPath: /tmp        - name: var-tmp          mountPath: /var/tmp      volumes:      - name: tmp        emptyDir: {}      - name: var-tmp        emptyDir: {}      affinity:        podAntiAffinity:          preferredDuringSchedulingIgnoredDuringExecution:          - weight: 100            podAffinityTerm:              labelSelector:                matchExpressions:                - key: app                  operator: In                  values:                  - myflaskapp              topologyKey: kubernetes.io/hostname ---apiVersion: v1kind: Servicemetadata:  name: myflaskappspec:  type: LoadBalancer  selector:    app: myflaskapp  ports:  - protocol: TCP    port: 80    targetPort: 5000

Deploy to Kubernetes:

# Create namespacekubectl create namespace myflaskapp # Create secrets (if needed)kubectl create secret generic myflaskapp-secrets \  --from-literal=database-url=postgresql://user:pass@db:5432/db \  -n myflaskapp # Apply the deploymentkubectl apply -f deployment.yaml -n myflaskapp # Check rollout statuskubectl rollout status deployment/myflaskapp -n myflaskapp # Get the service URLkubectl get service myflaskapp -n myflaskapp # Test the appcurl http://<EXTERNAL-IP>/health# {"status":"healthy"} # View logskubectl logs -l app=myflaskapp -n myflaskapp # Verify security contextkubectl exec -it $(kubectl get pod -l app=myflaskapp -n myflaskapp -o jsonpath='{.items[0].metadata.name}') -- whoami# Output should be: 65532 (non-root user)

Step 11: Deploy with Helm (Cloud-Native)

Helm packages Kubernetes deployments for reusability.

Create helm chart structure:

helm create myflaskapp-chart

This generates a standard Helm chart with Chart.yaml defining metadata, values.yaml containing default configuration, and templates/ directory with Kubernetes manifests.

Update values.yaml:

image:  repository: username/myflaskapp  tag: "1.0.0"  pullPolicy: IfNotPresent replicaCount: 3 service:  type: LoadBalancer  port: 80  targetPort: 5000 resources:  limits:    cpu: 500m    memory: 256Mi  requests:    cpu: 100m    memory: 128Mi autoscaling:  enabled: true  minReplicas: 3  maxReplicas: 10  targetCPUUtilizationPercentage: 80

Deploy with Helm:

# Create namespacekubectl create namespace myflaskapp # Install charthelm install myflaskapp ./myflaskapp-chart -n myflaskapp # Verify deploymenthelm list -n myflaskappkubectl get pods -n myflaskapp # Update chart (new version)helm upgrade myflaskapp ./myflaskapp-chart -n myflaskapp \  --set image.tag=1.1.0 # Rollback if neededhelm rollback myflaskapp 1 -n myflaskapp # Uninstallhelm uninstall myflaskapp -n myflaskapp

Step 12: Verify Production Deployment

Final checks to ensure everything is secure and working.

# 1. Check image signaturesCOSIGN_EXPERIMENTAL=1 cosign verify --insecure-ignore-tlog myflaskapp:1.0.0 # 2. Check SBOM is presentsyft myflaskapp:1.0.0 --output cyclonedx-json | jq '.components | length'# Should show hundreds of components # 3. Scan production image for vulnerabilitiesgrype myflaskapp:1.0.0# Should show 0 critical, 0 high vulnerabilities # 4. Check running container securitykubectl get pods -n myflaskapp -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.securityContext}{"\n"}{end}' # 5. Verify health checks are workingkubectl describe deployment myflaskapp -n myflaskapp | grep -A5 "Liveness\|Readiness" # 6. Test the applicationAPP_URL=$(kubectl get service myflaskapp -n myflaskapp -o jsonpath='{.status.loadBalancer.ingress[0].ip}')curl http://$APP_URL/healthcurl http://$APP_URL/api/version # 7. Check logs for errorskubectl logs -l app=myflaskapp -n myflaskapp --tail=50 # 8. Verify no privilege escalation is possiblekubectl exec -it $(kubectl get pod -l app=myflaskapp -n myflaskapp -o jsonpath='{.items[0].metadata.name}') -- whoami# Output should be: 65532 (non-root user) # 9. Check that root filesystem is read-onlykubectl exec -it $(kubectl get pod -l app=myflaskapp -n myflaskapp -o jsonpath='{.items[0].metadata.name}') -- touch /test.txt# Should fail: Read-only file system

Complete Checklist

After finishing all steps, verify completion of the following items: Flask app is written and tested locally, Dockerfile uses multi-stage build and CleanStart base, image builds without errors, Grype scan shows zero critical and zero high vulnerabilities, SBOM is generated and stored, image is signed with Cosign, Docker-compose stack runs all services in healthy status, image is pushed to registry, Kubernetes deployment runs three replicas, all endpoints respond correctly, health checks are working, security context is enforced with non-root user, read-only filesystem, and no new privileges, and logs are clean with no errors.

The Bottom Line

The journey from code to production requires attention to detail at every step. Your Dockerfile matters and should use multi-stage builds with CleanStart base images and non-root users. Scanning with Grype catches vulnerabilities before production. SBOMs track all components in your image. Signing with Cosign proves image authenticity. Docker-compose allows testing multi-service stacks locally. Kubernetes enables production deployments with automatic health management and security controls. Every step has a purpose and skipping security checks leads to compromised systems.

Next Steps: Deepen Your Secure Deployment Knowledge

Foundation — To understand the concepts behind secure deployment, read Container Image Fundamentals for what images are and how they work, Container Security Best Practices for principles applicable to every deployment, and What is Supply Chain Security? for why every step in the chain matters.

Deep dives — To learn the technologies you just used, explore Build Stage Security for secure Dockerfiles and builds, The Continuous Trust Loop for automated vulnerability remediation, and Strip-Down vs Source-Built for image composition approaches.

Practical examples — To apply patterns to your services, see Docker-Compose Examples for multi-service deployments locally and Getting Started: Python/Node.js/Go/Java for language-specific deployment guides.

Operations at scale — For managing secure images in production, consult Helm Charts & Kubernetes for deploying at scale securely, Upgrade & Patching Playbook for keeping images patched, and Multi-Cloud Registry Operations for managing images across clouds.