Skip to main content
CleanStart

Knowledge Hub

Getting Started with Python on CleanStart

New to containers? Start with Container Image Fundamentals to understand the basics before diving into this guide.

Building Python Applications with CleanStart

CleanStart provides hardened Python base images (cleanstart/python) with pre-scanned OS dependencies (zero CVEs), Python 3.11, 3.12, 3.13 variants, multi-stage build support, FIPS 140-3 mode available, and optimization for Cloud Native deployments.

The following diagram illustrates the Python application deployment workflow with CleanStart:

graph LR    A["Python Source<br/>app.py<br/>requirements.txt"] -->|Build Stage| B["cleanstart/python:3.12<br/>Builder"]     B -->|Install| C["pip install<br/>requirements.txt"]    B -->|Dependencies| D["Flask 3.0.0<br/>gunicorn 21.2.0<br/>Transitive deps"]     C -->|Cache| E["Wheel files<br/>Pre-compiled<br/>Packages"]     E -->|Copy Only| F["Production Stage<br/>cleanstart/python:3.12"]     F -->|Setup| G["Create App User<br/>Non-root"]     G -->|Security| H["No build tools<br/>Read-only FS<br/>Port 5000"]     H -->|Entrypoint| I["gunicorn app:app"]     I -->|Deploy| J["Container Registry<br/>registry.cleanstart.com"]     J -->|Push with| K["SBOM<br/>Cosign Signature<br/>Provenance"]     K -->|Runtime| L["Kubernetes Pod<br/>or Docker"]     L -->|Port| M["Port 5000<br/>JSON Responses"]     M -->|Request| N["curl localhost:5000"]     N -->|Response| O["Hello from CleanStart<br/>JSON Output"]     style A fill:#ccffcc    style B fill:#99ccff    style C fill:#99ccff    style F fill:#99ff99    style I fill:#ffff99    style O fill:#99ff99

Quick Start: A Simple Flask Application

Step 1: Create Your Project

mkdir my-python-appcd my-python-app

Create app.py:

from flask import Flaskimport logging app = Flask(__name__)logging.basicConfig(level=logging.INFO) @app.route('/')def hello():    logging.info('Request received')    return {'message': 'Hello from CleanStart', 'version': '1.0'} if __name__ == '__main__':    app.run(host='0.0.0.0', port=5000, debug=False)

Create requirements.txt:

Flask==3.0.0gunicorn==21.2.0

Step 2: Create a Production Dockerfile

# Build stageFROM cleanstart/python:3.12 AS builder WORKDIR /appRUN pip install --upgrade pip setuptools wheelCOPY requirements.txt .RUN pip install --no-cache-dir -r requirements.txt # Production stageFROM cleanstart/python:3.12 WORKDIR /app # Create app userRUN useradd -m -u 1000 appuser # Copy dependencies from builderCOPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packagesCOPY --from=builder /usr/local/bin /usr/local/bin COPY --chown=appuser:appuser app.py . USER appuserEXPOSE 5000 HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \    CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:5000').read()" || exit 1 CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "--timeout", "60", "app:app"]

Step 3: Build and Run Locally

# Build the imagedocker build -t my-python-app:latest . # Run the containerdocker run --rm -p 5000:5000 my-python-app:latest # Test the applicationcurl http://localhost:5000

Multi-Stage Builds: The Complete Pattern

Multi-stage builds reduce final image size by excluding build tools:

# Stage 1: DependenciesFROM cleanstart/python:3.12 AS dependencies WORKDIR /tmpCOPY requirements.txt . RUN pip install --user --no-cache-dir \    -r requirements.txt # Stage 2: Source buildFROM cleanstart/python:3.12 AS builder WORKDIR /buildCOPY . . # Stage 3: RuntimeFROM cleanstart/python:3.12 WORKDIR /app RUN useradd -m -u 1000 appuser && \    chown -R appuser:appuser /app # Copy Python packages from dependencies stageCOPY --from=dependencies /root/.local /home/appuser/.local # Copy application code from builder stageCOPY --from=builder --chown=appuser:appuser /build . ENV PATH=/home/appuser/.local/bin:$PATHUSER appuser EXPOSE 8000HEALTHCHECK --interval=30s --timeout=3s CMD python -c "import requests; requests.get('http://localhost:8000/health')" || exit 1 CMD ["python", "-m", "uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]

Testing Your Application

Add pytest to development requirements:

pip install pytest pytest-cov

Create test_app.py:

import pytestfrom app import app @pytest.fixturedef client():    app.config['TESTING'] = True    with app.test_client() as client:        yield client def test_hello(client):    response = client.get('/')    assert response.status_code == 200    assert response.json['message'] == 'Hello from CleanStart'

Run tests in the Docker container:

FROM cleanstart/python:3.12 AS test WORKDIR /appCOPY requirements.txt .RUN pip install --no-cache-dir -r requirements.txt pytest COPY . .RUN pytest --cov=. --cov-report=term-missing

Production Deployment Considerations

Environment Variables

Externalize configuration using environment variables:

import osfrom dotenv import load_dotenv load_dotenv() LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO')DATABASE_URL = os.getenv('DATABASE_URL', 'sqlite:///app.db')SECRET_KEY = os.getenv('SECRET_KEY', 'dev-key-change-in-production')

Run with secrets:

docker run \    -e LOG_LEVEL=INFO \    -e DATABASE_URL='postgresql://user:pass@db:5432/myapp' \    -e SECRET_KEY='production-secret-key' \    my-python-app:latest

Logging to Stdout

For container orchestration (Kubernetes, Cloud Run), send logs to stdout:

import loggingimport sys # Configure logging to stdoutlogging.basicConfig(    level=logging.INFO,    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',    stream=sys.stdout,    handlers=[logging.StreamHandler(sys.stdout)]) logger = logging.getLogger(__name__)

Health Checks

Implement health check endpoints for orchestrators:

@app.route('/health')def health():    return {'status': 'healthy', 'version': '1.0'}, 200 @app.route('/ready')def readiness():    # Check database connectivity, external services, etc.    try:        # db.ping()        return {'ready': True}, 200    except Exception as e:        logger.error(f'Readiness check failed: {e}')        return {'ready': False, 'error': str(e)}, 503

Securing Your Python Application

User Isolation

Always run as non-root:

RUN useradd -m -u 1000 appuserUSER appuser

Minimal Dependencies

Keep your requirements.txt lean:

# Audit dependencies for vulnerabilitiespip install pip-auditpip-audit

Security Headers (for web apps)

@app.after_requestdef set_security_headers(response):    response.headers['X-Content-Type-Options'] = 'nosniff'    response.headers['X-Frame-Options'] = 'DENY'    response.headers['Strict-Transport-Security'] = 'max-age=31536000'    return response

Performance Optimization

Gunicorn Workers

Calculate optimal worker count:

workers = (2 × CPUs) + 1

For a 4-CPU container: --workers 9

CMD ["gunicorn", "--workers", "9", "--worker-class", "sync", \     "--bind", "0.0.0.0:8000", "--timeout", "120", \     "--access-logfile", "-", "--error-logfile", "-", "app:app"]

Uvicorn (for async)

For high-concurrency workloads:

CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000", \     "--workers", "4", "--loop", "uvloop"]

Deploying to Cloud Run (Google Cloud)

# Build and pushgcloud builds submit --tag gcr.io/my-project/my-python-app # Deploygcloud run deploy my-python-app \    --image gcr.io/my-project/my-python-app:latest \    --platform managed \    --region us-central1 \    --allow-unauthenticated \    --memory 512Mi \    --cpu 1 \    --timeout 60s \    --set-env-vars LOG_LEVEL=INFO,DATABASE_URL=...

Image Options

CleanStart provides Python variants:

Image

Use Case

cleanstart/python:3.13

Latest features, data science

cleanstart/python:3.12

Stable, recommended for production

cleanstart/python:3.11

Legacy support

cleanstart/python:3.12-fips

FIPS 140-3 compliance required

Troubleshooting

Build Issues

Problem: Slow pip installs during Docker build Cause: Compiling packages from source in every layer rebuild; pip cache in read-only FS not persisting between layers Fix: Use --no-cache-dir flag and pre-download wheels in builder stage. Consider using --pre-built-wheels for faster installations. Also copy requirements.txt before application code for layer caching.

Problem: ModuleNotFoundError: No module named 'xyz' at runtime Cause: PYTHONPATH not set correctly in CleanStart image; site-packages permissions issue with UID 65532; module not copied in multi-stage build Fix: Verify the site-packages path matches your Python version. In multi-stage builds, copy with explicit paths: COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages. Check sys.path in running container with python -c "import sys; print(sys.path)". Ensure files are readable by your application user.

Problem: pip install fails with "Permission denied" or "Read-only file system" errors Cause: Read-only root filesystem enforcement; /tmp mounted as tmpfs that's too small; pip cache directory not writable Fix: In production with readOnlyRootFilesystem: true, pip must complete in build stage. If building at runtime, mount writable volumes: /tmp, /home/appuser/.cache, and /home/appuser/.local. For development in read-only mode, use emptyDir volumes in Kubernetes.

Problem: ERROR: Could not install packages due to an EnvironmentError Cause: Pip cache issues; incompatible Python version; corrupted wheels Fix: Add RUN pip cache purge before final stage. Explicitly pin Python version in Dockerfile. Test wheel compatibility: pip download --only-binary=:all: -r requirements.txt to verify pre-built wheels exist for your platform.

Problem: Build layer caching not working; every rebuild recompiles everything Cause: Requirements.txt copied together with application code; Docker detects source changes and rebuilds Fix: Structure Dockerfile with dependency layer first: COPY requirements.txt . && RUN pip install before COPY . .. This way, dependency layer caches when requirements unchanged.

Problem: __pycache__ directories bloating image size Cause: Bytecode cache not excluded in multi-stage copy Fix: Add .dockerignore file with __pycache__ and *.pyc entries. In Dockerfile, explicitly exclude: RUN find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true

Runtime Issues

Problem: ModuleNotFoundError or import errors in shell-less container (with cleanimg-init) Cause: Shell-less containers can't execute bash for debugging; Python path issues with CleanStart UID 65532 Note: CleanStart's default user is UID 65532. If your Dockerfile creates a custom user (e.g., UID 1000), adjust the troubleshooting commands accordingly. Fix: Add diagnostic endpoint to your Flask/FastAPI app to print sys.path and installed modules. Access via HTTP instead of shell. Log sys.path at startup: import sys; logger.info(f"Python path: {sys.path}"). Verify user has read permissions on site-packages.

Problem: Application crashes with permission errors after using cleanimg-init Cause: File ownership issues when copying with COPY --chown but container running as UID 65532; pip packages installed as root, not accessible to non-root user Fix: Use explicit chown in build: COPY --chown=65532:65532 for all files. For pip packages in multi-stage, copy to user-accessible directory: COPY --from=builder /home/appuser/.local /home/appuser/.local. Verify with docker run --rm my-app ls -la /usr/local/lib/python3.12/site-packages/ to check ownership.

Problem: Gunicorn workers crash with "Worker timeout" or hang during requests Cause: Workers set too high relative to CPU cores; timeout too aggressive for slow requests; single-threaded blocking operations Fix: Calculate workers as (2 * CPU) + 1. In containers, check actual CPU: docker run --cpus=1 my-app cat /proc/cpuinfo | grep processor | wc -l. Increase timeout: --timeout 120. For async operations, use uvicorn workers instead: --worker-class uvicorn.workers.UvicornWorker.

Problem: Uvicorn/Flask server doesn't bind to port, "Address already in use" Cause: Binding to localhost (127.0.0.1) instead of 0.0.0.0; port already in use from previous container; CleanStart network namespace issues Fix: Always bind to 0.0.0.0: app.run(host='0.0.0.0', port=5000). Verify exposure in Dockerfile: EXPOSE 5000. Check for orphaned processes: docker ps -a | grep exited. For CleanStart, ensure --net=host or proper port mapping: docker run -p 5000:5000.

Problem: HEALTHCHECK fails in shell-less container with cleanimg-init Cause: Health check command tries to use shell (sh) which doesn't exist; curl or wget not available; Python not in PATH Fix: Use Python directly without shell: HEALTHCHECK CMD python -c "import requests; requests.get('http://localhost:8000/health')". Alternatively, use a direct socket check instead of HTTP: HEALTHCHECK --interval=30s CMD python -c "import socket; socket.create_connection(('127.0.0.1', 8000), timeout=1)". Or implement TCP socket check without external tools.

Problem: Graceful shutdown not working with cleanimg-init as PID 1 Cause: Signals not properly forwarded to Python process; missing signal handlers in application Fix: Ensure your Python app explicitly handles SIGTERM: signal.signal(signal.SIGTERM, signal.SIGINT) or use Gunicorn which handles signals. With cleanimg-init, signals are forwarded to child process. Test with docker kill -s TERM container-id and verify app logs show shutdown message.

Problem: Out of Memory (OOM) killed; Python process consuming excess memory Cause: Memory leaks in application; unbounded caching; large data structures in request handlers; site-packages taking too much memory Fix: Increase container memory limit: docker run -m 1g my-app:latest. Monitor with docker stats. For production, set resource requests/limits in Kubernetes. If issue persists, profile with memory_profiler: install with pip install memory-profiler, decorate functions with @profile, and run python -m memory_profiler app.py. Consider using pympler for object tracking.

Problem: UID 65532 (non-root user in CleanStart) can't write to /tmp or application directories Cause: /tmp in read-only filesystem; files copied with wrong ownership; application tries to write to read-only locations Fix: Mount writable emptyDir volumes in Kubernetes for /tmp and application cache. In Dockerfile, ensure proper ownership with COPY --chown=65532:65532. Create writable directories: RUN mkdir -p /home/appuser/.cache && chown 65532:65532 /home/appuser/.cache. For read-only root, mount /tmp as tmpfs in pod spec.

Problem: ImportError or missing C extension modules Cause: Package compiled for different platform; system libraries missing; binary wheel not available for platform Fix: Ensure pure Python builds or pre-compiled wheels. Check package documentation for binary dependencies. For packages with C extensions (e.g., psycopg2), install build-essential in builder stage: RUN apt-get update && apt-get install -y build-essential python3-dev && pip install --no-cache-dir psycopg2-binary (note: prefer psycopg2-binary to avoid compilation).

Performance Issues

Problem: Application startup takes 30+ seconds Cause: Large site-packages being imported; lazy loading not used; many dependencies being loaded on startup Fix: Profile startup time: python -m cProfile -s cumulative app.py | head -20. Move imports into functions if not needed at startup. For Flask, use blueprint lazy registration. With Gunicorn, set --preload-app to load code once per worker. Consider gevent workers for reduced memory.

Problem: /tmp as tmpfs fills up during operation Cause: Temporary files not cleaned; cache accumulation; pip operations writing to /tmp Fix: Ensure application code cleans up tempfiles: use context managers with tempfile.NamedTemporaryFile() as f:. Monitor /tmp size: docker exec container df -h. For read-only root setup, mount larger tmpfs: emptyDir: {sizeLimit: "500Mi"} in Kubernetes.

Problem: venv vs system install causes path confusion Cause: Virtual environment in multi-stage build not copied correctly; PATH not set properly; Python finding system packages instead Fix: For pip --user installs in multi-stage, add to PATH: ENV PATH=/home/appuser/.local/bin:$PATH. For venv, copy entire venv directory and update shebang lines. Prefer pip install --user in CleanStart for simpler multi-stage (no need for venv activation).

CleanStart Production Hardening

Read-Only Root Filesystem

Enable immutable infrastructure by making the root filesystem read-only, with specific writable paths for your application:

apiVersion: v1kind: Podmetadata:  name: python-appspec:  containers:  - name: app    image: cleanstart/python:3.12    securityContext:      readOnlyRootFilesystem: true    volumeMounts:    - name: tmp      mountPath: /tmp    - name: app-cache      mountPath: /home/appuser/.cache    - name: pip-cache      mountPath: /home/appuser/.local  volumes:  - name: tmp    emptyDir: {}  - name: app-cache    emptyDir: {}  - name: pip-cache    emptyDir: {}

Shell-Less ENTRYPOINT

Remove shell from the container for reduced attack surface. Update your Dockerfile:

FROM cleanstart/python:3.12 WORKDIR /appCOPY requirements.txt .RUN pip install -r requirements.txt COPY . . # Declarative Image Builder: # Use cleanimg-init as PID 1ENTRYPOINT ["/cleanimg-init", "--"]CMD ["python", "-u", "app.py"]

cleanimg-customize: Custom Configuration

Inject production settings for Python applications:

FROM cleanstart/python:3.12 WORKDIR /appCOPY requirements.txt .RUN pip install -r requirements.txt COPY . . # Create and inject custom Python configurationRUN mkdir -p /etc/cleanimg && \    echo 'import warnings; warnings.filterwarnings("ignore")' > /app/warnings.conf ENTRYPOINT ["/cleanimg-init", "--"]CMD ["python", "-u", "-W", "ignore", "app.py"]

Security Context

Complete Kubernetes securityContext for hardened Python containers:

apiVersion: apps/v1kind: Deploymentmetadata:  name: python-appspec:  template:    spec:      securityContext:        fsGroup: 65532        seccompProfile:          type: RuntimeDefault      containers:      - name: app        image: cleanstart/python:3.12        securityContext:          allowPrivilegeEscalation: false          readOnlyRootFilesystem: true          runAsNonRoot: true          runAsUser: 65532          runAsGroup: 65532          capabilities:            drop:            - ALL        resources:          requests:            cpu: 100m            memory: 128Mi          limits:            cpu: 500m            memory: 512Mi

Next Steps

Read Advanced Python Deployment, Explore CleanStart Image Registry, and Join the Community Slack.