Docker Image Optimization: From 266MB to 52MB - Complete Beginner's Guide (Part 1)

Master Docker image optimization techniques including Alpine Linux, multi-stage builds, .dockerignore, and layer reduction. Learn to build production-ready containers that are 80% smaller.

16 min read

Large Docker images slow down deployments, waste storage, and increase security vulnerabilities. This comprehensive guide teaches you proven optimization techniques to reduce image sizes by up to 80% while maintaining functionality and improving performance.

💡

🎯 What You'll Learn: In this hands-on tutorial, you'll discover:

  • Building unoptimized vs optimized Docker images and comparing sizes
  • Using Alpine Linux for minimal base images
  • Implementing multi-stage builds for production efficiency
  • Creating effective .dockerignore files to exclude unnecessary content
  • Adding metadata with LABEL instructions
  • Layer optimization and caching strategies
  • Real-world size comparisons (266MB → 52MB reduction!)

🚀 Why Image Optimization Matters

Docker image size directly impacts:

FactorUnoptimized ImpactOptimized Benefit
Deployment SpeedSlower push/pull times5-10x faster deployments
Storage CostsHigher registry costs80% storage reduction
SecurityMore attack surfaceMinimal attack vectors
Network UsageHigh bandwidth consumptionReduced transfer costs
Startup TimeLonger container startsNear-instant startup

Prerequisites

Before we begin, make sure you have:

  • Docker installed and running
  • Basic understanding of Dockerfile syntax
  • Terminal access with Docker permissions
  • A text editor for creating files

📁 Step 1: Create Project Structure

Set up a dedicated directory for this optimization lab:

mkdir docker-optimization
cd docker-optimization

This creates a clean workspace for all our optimization experiments.

🌐 Step 2: Create a Simple Web Application

Create a basic HTML file that we'll serve using Nginx:

cat > index.html << 'EOF'
<!DOCTYPE html>
<html>
<head>
    <title>Docker Optimization Lab</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 40px; }
        .container { max-width: 600px; margin: 0 auto; }
        h1 { color: #2c3e50; }
        .info { background: #ecf0f1; padding: 20px; border-radius: 5px; }
    </style>
</head>
<body>
    <div class="container">
        <h1>Welcome to Docker Optimization Lab</h1>
        <div class="info">
            <p>This is a sample web application running in a Docker container.</p>
            <p>Image optimization techniques help reduce size and improve performance.</p>
        </div>
    </div>
</body>
</html>
EOF

What this does:

  • Creates a simple, self-contained HTML file
  • Includes inline CSS for styling
  • No external dependencies required
  • Perfect for demonstrating image optimization

❌ Step 3: Build an Unoptimized Image (The Wrong Way)

Let's first build an image the wrong way to understand what NOT to do:

cat > Dockerfile.unoptimized << 'EOF'
FROM ubuntu:20.04

RUN apt-get update

RUN apt-get install -y nginx

RUN apt-get install -y curl

RUN apt-get install -y vim

RUN apt-get clean

RUN rm /var/www/html/index.nginx-debian.html

COPY index.html /var/www/html/

EXPOSE 80

CMD [ "nginx", "-g", "daemon off;" ]
EOF

Understanding the Problems

IssueProblemImpact
FROM ubuntu:20.04Heavy base image (~75MB)Unnecessary bulk
Multiple RUN commandsCreates separate layersLarger final size
Installing vim, curlUnnecessary toolsWasted space
apt-get clean separateCache already in layersCleanup doesn't reduce size

Build the Unoptimized Image

docker build -f Dockerfile.unoptimized -t webapp-unoptimized:v1 .

Expected Output:

[+] Building 68.3s (13/13) FINISHED
 => [internal] load build definition from Dockerfile.unoptimized        0.1s
 => [internal] load metadata for docker.io/library/ubuntu:20.04         0.0s
 => [1/8] FROM docker.io/library/ubuntu:20.04                           0.0s
 => [2/8] RUN apt-get update                                           13.1s
 => [3/8] RUN apt-get install -y nginx                                 23.1s
 => [4/8] RUN apt-get install -y curl                                  15.8s
 => [5/8] RUN apt-get install -y vim                                   13.5s
 => [6/8] RUN apt-get clean                                             0.4s
 => [7/8] RUN rm /var/www/html/index.nginx-debian.html                 0.5s
 => [8/8] COPY index.html /var/www/html/                               0.1s
 => exporting to image                                                  1.3s

Build time: 68.3 seconds - Notice how long it takes!

Check the Image Size

docker images webapp-unoptimized:v1

Output:

REPOSITORY           TAG       IMAGE ID       CREATED              SIZE
webapp-unoptimized   v1        4290998e4fdf   About a minute ago   266MB

Result: 266MB! This is enormous for a simple static website.

⚠️

⚠️ Why 266MB is Too Large: For serving a single HTML file, 266MB is wasteful. This includes the entire Ubuntu system, package manager caches, and unnecessary utilities like vim and curl.

✅ Step 4: Build an Optimized Image (The Right Way)

Now let's create a properly optimized Dockerfile:

cat > Dockerfile.optimized << 'EOF'
FROM nginx:alpine

COPY index.html /usr/share/nginx/html/

EXPOSE 80
EOF

Why This is Better

OptimizationBenefit
nginx:alpineMinimal base (~5MB vs 75MB)
Nginx pre-installedNo installation overhead
Single COPY instructionMinimal layers
No unnecessary toolsSmaller attack surface

Build the Optimized Image

docker build -f Dockerfile.optimized -t webapp-optimized:v1 .

Expected Output:

[+] Building 0.7s (7/7) FINISHED
 => [internal] load build definition from Dockerfile.optimized          0.1s
 => [internal] load metadata for docker.io/library/nginx:alpine         0.0s
 => [1/2] FROM docker.io/library/nginx:alpine                           0.2s
 => [2/2] COPY index.html /usr/share/nginx/html/                        0.1s
 => exporting to image                                                  0.1s

Build time: 0.7 seconds - 97x faster!

Compare Image Sizes

docker images | grep webapp

Output:

webapp-optimized     v1        c6ae8c7db208   15 seconds ago   52.5MB
webapp-unoptimized   v1        4290998e4fdf   3 minutes ago    266MB

🎉 Optimization Result

Size Reduction: 266MB → 52.5MB

  • 80.3% smaller
  • 5x smaller image
  • 97x faster build time

Same functionality, fraction of the size!

🏗️ Step 5: Multi-Stage Builds for Node.js Applications

For applications with build dependencies, multi-stage builds are essential. Let's create a Node.js example:

Create Node.js Application Files

cat > package.json << 'EOF'
{
  "name": "docker-optimization-demo",
  "version": "1.0.0",
  "description": "Demo app for Docker optimization",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^4.18.0"
  }
}
EOF
cat > server.js << 'EOF'
const express = require('express');
const path = require('path');
const app = express();
const PORT = 3000;

// Serve static files
app.use(express.static('.'));

app.get('/', (req, res) => {
    res.sendFile(path.join(__dirname, 'index.html'));
});

app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
});
EOF

Create Multi-Stage Dockerfile

cat > Dockerfile.multistage << 'EOF'
FROM node:16-alpine AS builder

WORKDIR /app

COPY package*.json ./

RUN npm install --only=production

FROM node:16-alpine AS production

WORKDIR /app

COPY --from=builder /app/node_modules ./node_modules

COPY package*.json ./

COPY server.js ./

COPY index.html ./

RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001

USER nodejs

EXPOSE 3000

CMD [ "npm", "start" ]
EOF

Understanding Multi-Stage Builds

StagePurposeWhat Happens
Stage 1 - builderBuild environmentInstalls dependencies
Stage 2 - productionRuntime environmentCopies only needed files
COPY --from=builderSelective copyingTakes only node_modules
USER nodejsSecurityNon-root user execution

Build Multi-Stage Image

docker build -f Dockerfile.multistage -t webapp-multistage:v1 .

Expected Output (abbreviated):

[+] Building 21.4s (14/14) FINISHED
 => [internal] load build definition from Dockerfile.multistage         0.0s
 => [internal] load metadata for docker.io/library/node:16-alpine       3.2s
 => [builder 1/4] FROM docker.io/library/node:16-alpine                10.0s
 => [builder 2/4] WORKDIR /app                                          0.4s
 => [builder 3/4] COPY package*.json ./                                 0.1s
 => [builder 4/4] RUN npm install --only=production                     5.3s
 => [production 3/7] COPY --from=builder /app/node_modules ./node_modules  0.2s
 => [production 4/7] COPY package*.json ./                              0.1s
 => [production 5/7] COPY server.js ./                                  0.1s
 => [production 6/7] COPY index.html ./                                 0.1s
 => [production 7/7] RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001  0.5s
 => exporting to image                                                  1.0s

Compare All Three Images

docker images | grep webapp

Output:

webapp-multistage    v1        258c77de1c40   15 seconds ago   120MB
webapp-optimized     v1        c6ae8c7db208   21 minutes ago   52.5MB
webapp-unoptimized   v1        4290998e4fdf   24 minutes ago   266MB

Analysis:

  • Unoptimized: 266MB (baseline)
  • Multi-stage: 120MB (55% reduction)
  • Alpine optimized: 52.5MB (80% reduction)
💡

💡 Why Multi-Stage is Larger: The Node.js multi-stage image is larger because it includes the Node.js runtime and Express dependencies. However, it's still 55% smaller than a single-stage build would be!

🚫 Step 6: Using .dockerignore for Cleaner Builds

The .dockerignore file prevents unnecessary files from being added to your image.

Create Dummy Files (Don't Include These!)

mkdir logs
echo "Error log content" > logs/error.log
echo "Access log content" > logs/access.log
echo "Temporary data" > temp.txt
echo "Cache data" > cache.tmp
mkdir .git
echo "Git repository data" > .git/config
mkdir docs
echo "# Documentation" > docs/README.md
mkdir node_modules_dev
echo "Development dependencies" > node_modules_dev/dev-package.js

Build Without .dockerignore

docker build -f Dockerfile.optimized -t webapp-with-junk:v1 .

The build includes ALL files in the directory (wasteful).

Create .dockerignore File

cat > .dockerignore << 'EOF'
# Logs
logs/
*.log

# Temporary files
*.tmp
temp.txt

# Version control
.git/
.gitignore

# Development dependencies
node_modules_dev/

# Documentation (not needed in production)
docs/
README.md

# IDE files
.vscode/
.idea/

# OS generated files
.DS_Store
Thumbs.db

# Docker files (don't include other Dockerfiles)
Dockerfile.*
!Dockerfile.optimized

# Build artifacts
dist/
build/
EOF

Understanding .dockerignore Patterns

PatternMeaningExample
logs/Ignore directoryExcludes entire logs folder
*.logWildcard patternAll files ending in .log
temp.txtSpecific fileOnly that exact file
Dockerfile.*Prefix wildcardAll Dockerfile variants
!Dockerfile.optimizedException (include)Include this specific file

Build With .dockerignore

docker build -f Dockerfile.optimized -t webapp-clean:v1 .

Verify Excluded Files

docker run --rm webapp-clean:v1 ls -la /usr/share/nginx/html/

Output:

total 8
drwxr-xr-x    1 root     root            24 Oct  2 19:04 .
drwxr-xr-x    1 root     root            18 Aug 13 18:52 ..
-rw-r--r--    1 root     root           497 Aug 13 15:10 50x.html
-rw-r--r--    1 root     root           661 Oct  2 18:53 index.html

Success! Only index.html was copied - no logs, no temp files, no git data!

🏷️ Step 7: Adding Metadata with LABEL

Labels provide important metadata about your images:

cat > Dockerfile << 'EOF'
FROM nginx:alpine

LABEL maintainer="owais.abbasi9@gmail.com"
LABEL version="1.0"
LABEL description="Optimized web application for Docker practice"

COPY index.html /usr/share/nginx/html/

RUN echo 'server { \
    listen 80; \
    server_name localhost; \
    location / { \
        root /usr/share/nginx/html; \
        index index.html; \
    } \
    # Security headers \
    add_header X-Frame-Options "SAMEORIGIN" always; \
    add_header X-Content-Type-Options "nosniff" always; \
}' > /etc/nginx/conf.d/default.conf

EXPOSE 80

CMD [ "nginx", "-g", "daemon off;" ]
EOF

Build Labeled Image

docker build -t mywebapp:1.0.0 .

Expected Output:

[+] Building 0.9s (8/8) FINISHED
 => [internal] load build definition from Dockerfile                   0.0s
 => [1/3] FROM docker.io/library/nginx:alpine                          0.0s
 => [2/3] COPY index.html /usr/share/nginx/html/                       0.0s
 => [3/3] RUN echo 'server { ... }' > /etc/nginx/conf.d/default.conf   0.5s
 => exporting to image                                                 0.1s

Benefits of Labels: Labels make images discoverable, document usage, and enable automation tools to process images intelligently.

🎯 Optimization Best Practices

1. Choose the Right Base Image

  • Use Alpine Linux variants when possible (nginx:alpine, node:alpine)
  • Choose minimal official images
  • Avoid general-purpose OS images (Ubuntu, CentOS) unless necessary

2. Minimize Layers

Combine multiple RUN commands into one to reduce layers.

3. Use Multi-Stage Builds

  • Separate build and runtime dependencies
  • Copy only artifacts needed for production
  • Reduces final image size significantly

4. Leverage .dockerignore

  • Exclude logs, temp files, and documentation
  • Don't include version control files
  • Skip development dependencies

5. Order Instructions Wisely

  • Place frequently changing instructions last
  • Copy dependency files before source code
  • Maximizes layer caching

6. Clean Up in Same Layer

Always clean up package manager caches in the same RUN command where you install packages.

7. Use Specific Tags

Always use specific version tags instead of latest for reproducible builds.

8. Remove Unnecessary Packages

  • Only install what you need
  • Avoid including debug tools in production images
  • Use --no-install-recommends with apt-get

Layer Minimization Examples:

# Bad - 3 layers
RUN apt-get update
RUN apt-get install -y nginx
RUN apt-get clean

# Good - 1 layer
RUN apt-get update && \
    apt-get install -y nginx && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

Cleanup in Same Layer:

# Bad - cleanup in different layer (doesn't reduce size)
RUN apt-get install -y package
RUN apt-get clean

# Good - cleanup in same layer
RUN apt-get install -y package && apt-get clean

Use Specific Tags:

# Bad - unpredictable
FROM node:latest

# Good - reproducible
FROM node:16-alpine

📊 Command Reference Cheat Sheet

CommandPurposeExample
docker build -f [file] -t [tag] .Build with specific Dockerfiledocker build -f Dockerfile.optimized -t app:v1 .
docker imagesList all imagesdocker images | grep webapp
docker run --rm [image] [cmd]Run command and remove containerdocker run --rm app:v1 ls /app
docker history [image]View image layersdocker history webapp:v1
docker inspect [image]View detailed metadatadocker inspect webapp:v1
docker rmi [image]Remove imagedocker rmi webapp:v1
docker system prune -fClean up unused resourcesdocker system prune -f

🎓 Key Takeaways

✅ Remember These Points

  1. Alpine Linux reduces base image size by 80-90%
  2. Multi-stage builds separate build-time and runtime dependencies
  3. .dockerignore prevents unnecessary files from entering images
  4. Layer consolidation reduces final image size
  5. Specific base image tags ensure reproducible builds
  6. Labels provide essential metadata for automation
  7. Optimization improves deployment speed, reduces costs, and enhances security

🚀 What's Next?

In Part 2, we'll cover:

  • Image tagging strategies and version management
  • Pushing images to Docker Hub
  • Pulling and deploying from registries
  • Running containers with environment variables and volumes
  • Container monitoring and resource usage
  • Troubleshooting common issues

📖 Further Reading

Official Resources


🎉 Excellent Work! You've learned to optimize Docker images from 266MB down to 52MB - an 80% reduction! You now understand Alpine Linux, multi-stage builds, .dockerignore files, and layer optimization.

Next Steps: Practice these techniques on your own applications and get ready for Part 2 where we'll push these optimized images to Docker Hub!

💬 Discussion

Share your optimization wins:

  • What size reductions have you achieved?
  • Which optimization technique surprised you most?
  • What challenges did you face with multi-stage builds?
  • How are you applying these in production?

Connect with me:

  • 🐙 GitHub - Docker optimization examples
  • 📧 Contact - Questions and discussions
Owais

Written by Owais

I'm an AIOps Engineer with a passion for AI, Operating Systems, Cloud, and Security—sharing insights that matter in today's tech world.

I completed the UK's Eduqual Level 6 Diploma in AIOps from Al Nafi International College, a globally recognized program that's changing careers worldwide. This diploma is:

  • ✅ Available online in 17+ languages
  • ✅ Includes free student visa guidance for Master's programs in Computer Science fields across the UK, USA, Canada, and more
  • ✅ Comes with job placement support and a 90-day success plan once you land a role
  • ✅ Offers a 1-year internship experience letter while you study—all with no hidden costs

It's not just a diploma—it's a career accelerator.

👉 Start your journey today with a 7-day free trial

Related Articles

Continue exploring with these handpicked articles that complement what you just read

More Reading

One more article you might find interesting