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:#99ff99Quick Start: A Simple Flask Application
Step 1: Create Your Project
mkdir my-python-appcd my-python-appCreate 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.0Step 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:5000Multi-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-covCreate 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-missingProduction 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:latestLogging 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)}, 503Securing Your Python Application
User Isolation
Always run as non-root:
RUN useradd -m -u 1000 appuserUSER appuserMinimal Dependencies
Keep your requirements.txt lean:
# Audit dependencies for vulnerabilitiespip install pip-auditpip-auditSecurity 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 responsePerformance Optimization
Gunicorn Workers
Calculate optimal worker count:
workers = (2 × CPUs) + 1For 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 |
|---|---|
| Latest features, data science |
| Stable, recommended for production |
| Legacy support |
| 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: 512MiNext Steps
Read Advanced Python Deployment, Explore CleanStart Image Registry, and Join the Community Slack.
