Docker Image Optimization: The Problem & Real Example

10/9/2025

Learn why Docker images become bloated and see a real-world example with size comparisons. Discover which base images work best for Python applications.

Read time: 6 min read

Docker Image Optimization: The Problem & Real Example

Ever wondered why your Docker images are taking forever to build and deploy? You’re not alone. Bloated Docker images are one of the most common problems in containerized applications.

In large scale CI/CD pipelines, bloated images can waste hours of compute time and gigabytes of storage per build. Let’s fix this once and for all.

The Problem: Bloated Images

Common Culprits

  • Large base images
  • Unnecessary packages and dependencies
  • Build artifacts left in final image
  • Multiple layers with redundant data
  • No cleanup after package installation

Real-World Impact

  • Slower deployments (5-10x longer)
  • Higher storage costs (10x more space)
  • Network bottlenecks (longer downloads)
  • Security risks (more attack surface)

Before and After: A Real Example

For the example below, I used an app.py file that demonstrates a real Flask web application with dependencies.

Step 1: Create a folder

mkdir docker-tests

Step 2: In the folder, create an app.py file.

[app.py]

from flask import Flask, jsonify
import requests
import json
from datetime import datetime

app = Flask(__name__)

@app.route('/')
def home():
    return jsonify({
        "message": "Hello from Docker!",
        "timestamp": datetime.now().isoformat(),
        "status": "running"
    })

@app.route('/health')
def health():
    return jsonify({"status": "healthy"})

@app.route('/external')
def external():
    try:
        response = requests.get('https://httpbin.org/json', timeout=5)
        return jsonify({
            "external_data": response.json(),
            "status": "success"
        })
    except Exception as e:
        return jsonify({"error": str(e), "status": "failed"}), 500

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=False)

Step 3. Create a requirements.txt file

flask==2.3.3
requests==2.31.0
numpy==1.24.3
pandas==2.0.3

Note: NumPy and Pandas are installed for demonstration purposes only and aren’t used in app.py.


The Bloated Version

Step 4: Create a dockerfile

Dockerfile.bloated

FROM ubuntu:20.04
RUN apt-get update
RUN apt-get install -y python3 python3-pip
RUN pip3 install flask requests numpy pandas
COPY . /app
WORKDIR /app
EXPOSE 5000
CMD ["python3", "app.py"]

Step 5: Build and test

docker build -f Dockerfile.bloated -t ubuntu-bloated .
docker run -p 5000:5000 ubuntu-bloated

The Optimized Version of Ubuntu

Step 6: Create a dockerfile

Dockerfile.optimized

# ✅ GOOD: Optimized image (194MB but compatible)
FROM ubuntu:20.04
RUN apt-get update && \
    apt-get install -y --no-install-recommends python3 python3-pip && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip3 install --no-cache-dir -r requirements.txt
COPY . /app
WORKDIR /app
EXPOSE 5000
CMD ["python3", "app.py"]

Step 7: Build and test

docker build -f Dockerfile.optimized -t ubuntu-optimized .
docker run -p 5000:5000 ubuntu-optimized

The Slim Build

Step 8: Create a dockerfile

Dockerfile.slim

FROM python:3.9-slim
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . /app
WORKDIR /app
EXPOSE 5000
CMD ["python", "app.py"]

Step 9: Build and test

docker build -f Dockerfile.slim -t slim-optimized .
docker run -p 5000:5000 slim-optimized

The Alpine Build (with compatibility issues)

Step 10: Create the docker file

Dockerfile.alpine

# ⚠️ WARNING: Alpine has compatibility issues with Python C extensions
FROM python:3.9-alpine
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . /app
WORKDIR /app
EXPOSE 5000
CMD ["python", "app.py"]

Step 11: Build and test

docker build -f Dockerfile.alpine -t alpine-optimized .
docker run -p 5000:5000 alpine-optimized

Note: Alpine builds may fail due to musl libc compatibility issues with numpy/pandas.

Results

Lets first look at the base images and then current image sizes.

Base images

docker pull ubuntu:20.04
docker pull python:3.9-slim
docker pull python:3.9-alpine
docker images | grep -E "(ubuntu|python)"

Results:

ubuntu:20.04        72MB
python:3.9-slim     194MB  
python:3.9-alpine   49MB

Build Image sizes

Get the sizes

docker images | grep -E "(ubuntu-bloated|ubuntu-optimized|slim-optimized|alpine-optimized)"

Results:

ubuntu-bloated      931MB
ubuntu-optimized    194MB
slim-optimized      194MB
alpine-optimized    49MB (if it builds successfully)

Complete Size Comparison

ImageBase SizeFinal SizeReductionNotes
ubuntu-bloated72MB931MB-Bloated
ubuntu-optimized72MB194MB79%Recommended
slim-optimized194MB194MB79%Good alternative
alpine-optimized49MB49MB95%Compatibility issues

The winner?

  • For our example, Ubuntu-optimized.

Python Base Image Decision Matrix

ImageSizeCompatibilitySecurityDebuggingUse CaseOverall RatingReason
Ubuntu Optimized194MB⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐Production⭐⭐⭐⭐⭐Best overall - Full compatibility, easy debugging, stable
Python Slim194MB⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐Pure Python apps⭐⭐⭐⭐Great for simple apps, limited C extension support
Alpine49MB⭐⭐⭐⭐⭐⭐⭐⭐⭐Minimal apps⭐⭐⭐Smallest but compatibility issues with numpy/pandas
Distroless49MB⭐⭐⭐⭐⭐⭐⭐⭐Production only⭐⭐⭐Most secure but hard to debug

Note: Size includes with dependencies

Recommendation for python applications:

  • Use ubuntu:20.04 optimized for Python apps with C-extension dependencies.

  • **Pros:**Great for simple apps, limited C extension support

    • Full compatibility with numpy, pandas, scipy
    • Easy debugging with shell access
    • Stable, predictable environment
    • Can install system libraries as needed
    • Good balance of size and functionality

Close Runner-Up: python:3.9-slim

  • Pros: Minimal attack surface, small size, no shell/package manager
  • Cons: Hard to debug, no shell access, limited to pure Python apps

Close Runner-Up: python:3.9-slim

  • Pros: Minimal attack surface, small size, no shell/package manager
  • Cons: Hard to debug, no shell access, limited to pure Python apps

Takeaway

For Python applications that rely on system libraries or heavy C-extension packages like numpy, pandas, or scipy, Ubuntu-optimized is often the better choice. It provides a stable, predictable environment with full shell access, easier debugging, and the flexibility to install exactly what your application needs. In many cases, the final image size can even be smaller than Python slim, because you avoid pulling in extra build dependencies that slim requires for C extensions.

That said, Python slim remains a strong alternative for applications that are mostly pure Python. It offers a minimal attack surface, a smaller base, and fewer moving parts, making it ideal for lightweight apps where full system libraries aren’t needed. Choosing slim is especially valuable when security and minimal runtime footprint are top priorities, or when you want to enforce a strict “pure Python only” environment.

Rule of Thumb:

  • Ubuntu-optimized: Use for complex Python apps with compiled dependencies.
  • Python slim: Use for lightweight, pure-Python apps where minimal attack surface and small image size matter most.

Next Steps

Now that you understand the problem and have seen real examples, you’re ready to learn the 7 essential techniques to shrink your Docker images even further.

Continue reading: Docker Image Optimization: 7 Essential Techniques

Or jump to advanced techniques: Docker Image Optimization: Advanced Techniques & Tools