Tech Expert & Vibe Coder

With 14+ years of experience, I specialize in self-hosting, AI automation, and Vibe Coding – building applications using AI-powered tools like Google Antigravity, Dyad, and Cline. From homelabs to enterprise solutions.

Implementing Docker Container Lottery Ticket Pruning: Reducing Image Sizes by 60% Using Sparse Layer Analysis

Why I Started Looking at Container Image Sizes

I run most of my services in Docker containers on Proxmox. Over time, my storage started filling up faster than expected. When I checked, I had multiple images sitting around—some over 1GB—for services that barely did anything. One container running a simple Python script was pulling a full Ubuntu base image plus packages I never touched.

That's when I started digging into how Docker images are actually built and whether I could strip them down without breaking functionality.

What I Actually Did

I didn't implement "lottery ticket pruning" in the academic sense. That term comes from neural network research—finding which weights in a model can be removed without losing accuracy. What I did was more practical: I analyzed which layers in my container images were actually being used and removed the rest.

Step 1: Identifying Bloat

I started by running docker history on my largest images to see what was taking up space:

docker history my-service:latest

This showed me every layer—package installs, file copies, dependency downloads. Most of the bulk came from:

  • Base images with full OS tools I never used
  • Build dependencies left in the final image
  • Cached package indexes and temporary files

Step 2: Switching to Alpine

For services where I controlled the build, I switched from ubuntu:latest or python:3.11 to Alpine-based images. Alpine uses musl libc instead of glibc, which makes it smaller but occasionally incompatible with certain binaries.

My Python automation container went from 900MB to 180MB just by changing the base image to python:3.11-alpine.

Step 3: Multi-Stage Builds

For anything that required compilation or heavy build tools, I used multi-stage Dockerfiles. The first stage installs build dependencies and compiles the app. The second stage copies only the compiled output into a clean base image.

FROM python:3.11-alpine AS builder
RUN apk add --no-cache gcc musl-dev
COPY requirements.txt .
RUN pip install --user -r requirements.txt

FROM python:3.11-alpine
COPY --from=builder /root/.local /root/.local
COPY . /app
WORKDIR /app
CMD ["python", "main.py"]

This kept build tools out of the final image. One container dropped from 1.2GB to 450MB.

Step 4: Removing Unused Layers

I audited my Dockerfiles for redundant commands. Every RUN creates a new layer, even if it deletes files. I combined commands where possible:

# Before (creates 3 layers)
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*

# After (creates 1 layer)
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

This doesn't always save space, but it reduces metadata overhead and makes images faster to pull.

Step 5: Using .dockerignore

I added a .dockerignore file to stop copying unnecessary files into the image. Things like .git, node_modules, and local config files were inflating my builds.

.git
*.log
node_modules
__pycache__

This alone cut 200MB from one Node.js container.

What Worked

After going through my most-used images, I reduced total storage from about 18GB to 7GB. Most services still run exactly the same. A few key wins:

  • Alpine-based images where compatibility wasn't an issue
  • Multi-stage builds for compiled apps
  • Cleaning up package caches in the same RUN command that installs them

I also started using docker system prune regularly to clear dangling images and unused layers:

docker system prune -a

This removes everything not currently in use. I run it manually after rebuilding images.

What Didn't Work

Not every service could move to Alpine. One container running a monitoring tool required glibc and broke completely on musl. I tried patching it but gave up and kept it on Debian Slim.

I also tried using distroless images for a Go service. The image was tiny (under 20MB), but debugging became a nightmare. No shell, no package manager, no standard tools. When something broke, I had no way to inspect it without rebuilding the image with debug layers.

Multi-stage builds added complexity. For simple scripts, the extra Dockerfile logic wasn't worth it. I only use them now for services with real build steps.

Key Takeaways

  • Start with the base image. Alpine saves space if your dependencies support it.
  • Use multi-stage builds for anything that compiles or installs heavy tooling.
  • Combine RUN commands to reduce layer count and clean up in the same step.
  • Add a .dockerignore file to avoid copying junk into the image.
  • Don't optimize for size at the cost of debuggability unless you're deploying at scale.

I didn't achieve a perfect 60% reduction across everything, but most images are now 40-50% smaller. For my Proxmox setup, that's enough to stop worrying about storage for a while.