Common Docker Mistakes
Docker is powerful but has many pitfalls. This guide covers the most common Docker mistakes including large image layers, dangling images, exposed ports, and provides practical solutions to avoid them.
Using `:latest` tag in production
Many developers use `nginx:latest` or `myapp:latest` in production. The `latest` tag is mutable—it changes. This leads to unreproducible deployments and unexpected updates. Different nodes might run different versions of your application.
# Bad - using latest
FROM node:latest
docker run myapp:latest
# Good - using specific version
FROM node:18.17.0-alpine3.18
docker run myapp:v1.2.3
# Best - using image digest
docker run myapp@sha256:abc123def456...
Not cleaning up temporary files in the same layer
Each RUN instruction creates a layer. If you install packages and then clean up in a separate RUN, the cleanup doesn't remove files from previous layers—they still exist in the image. This creates large images with unnecessary files.
# Bad - separate layers, large image
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get clean
RUN rm -rf /var/lib/apt/lists/*
# Good - single layer, small image
RUN apt-get update && \
apt-get install -y curl && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
Not cleaning up unused images and layers
Every time you build a new image, old layers and dangling images (untagged images) accumulate. Over time, this can consume gigabytes of disk space. Many developers don't realize how much space is wasted.
# Check disk usage
docker system df
# Remove dangling images only
docker image prune
# Remove all unused images (including those without tags)
docker image prune -a
# Complete cleanup (containers, networks, images, cache)
docker system prune -a
# Remove volumes as well (caution: data loss)
docker system prune -a --volumes
# Show detailed disk usage
docker system df -v
Publishing database ports (-p 5432:5432) to the host
Many developers publish database ports (5432, 3306, 27017) to the host using `-p`. This exposes your database to anyone who can reach the host. In production, this is a security risk. Databases should only be accessible from within the Docker network.
# Bad - exposing database port
docker run -d -p 5432:5432 --name postgres postgres
# Good - no port exposure, internal only
docker network create app-network
docker run -d --network app-network --name postgres postgres
docker run -d --network app-network --name app -p 8080:3000 myapp
# Database is only accessible from 'app' container via 'postgres:5432'
Not creating a non-root user in Dockerfile
By default, containers run as root. If an attacker compromises your container, they have root access. This is one of the most common security mistakes in Docker.
# Dockerfile with non-root user
FROM node:18-alpine
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
WORKDIR /app
COPY --chown=nodejs:nodejs . .
# Switch to non-root user
USER nodejs
CMD ["node", "server.js"]
Using environment variables for passwords, API keys
Environment variables are visible in `docker inspect`, `docker exec env`, and logs. They're not secure for secrets. Attackers with container access can read them, and they can leak through debugging output.
# Bad - using environment variables for secrets
docker run -e DB_PASSWORD=secretpassword postgres
# Good - using Docker secrets (Swarm mode)
echo "secretpassword" | docker secret create db_password -
docker service create --secret db_password --name db postgres
# Good - using secret file
echo "secretpassword" > db_password.txt
chmod 600 db_password.txt
docker run -v $(pwd)/db_password.txt:/run/secrets/db_password:ro postgres
Running containers without memory or CPU limits
Without limits, a single container can consume all host resources, causing a denial-of-service for other containers. This is especially dangerous in multi-tenant environments.
# Bad - no limits
docker run -d nginx
# Good - with resource limits
docker run -d --memory=512m --cpus=0.5 --pids-limit=100 nginx
# In docker-compose.yml
services:
app:
image: myapp
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
Sending unnecessary files to Docker daemon
Without `.dockerignore`, the entire build context is sent to the Docker daemon. This includes `node_modules`, `.git`, temporary files, and secrets, making builds slower and images larger.
# .dockerignore file
node_modules/
npm-debug.log
.git/
.gitignore
.env
*.md
Dockerfile
.dockerignore
.idea/
.vscode/
coverage/
.nyc_output/
*.log
Hardcoding API endpoints, database URLs in Dockerfile
Hardcoding configuration in images makes them environment-specific. You need separate images for dev, staging, and production, which is wasteful and error-prone.
# Bad - hardcoded in Dockerfile
ENV API_URL=https://api.production.com
# Good - use environment variable at runtime
ENV API_URL=${API_URL}
# Pass at runtime: docker run -e API_URL=https://api.dev.com myapp
# Better - use config file mounted at runtime
docker run -v $(pwd)/config.json:/app/config.json myapp
Including build tools and source code in production images
Production images often contain compilers, SDKs, and source code that aren't needed at runtime. This increases image size, attack surface, and deployment time.
# Multi-stage build
# Stage 1: Build (includes compilers)
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp .
# Stage 2: Runtime (no compilers)
FROM alpine:latest
COPY --from=builder /app/myapp .
CMD ["./myapp"]
- Use specific image tags, never `:latest` in production
- Clean up temporary files in the same RUN layer
- Regularly prune unused images, containers, and volumes
- Don't expose database ports to the host
- Run containers as non-root user
- Never store secrets in environment variables
- Always set resource limits (memory, CPU, PIDs)
- Use `.dockerignore` to exclude unnecessary files
- Keep images environment-agnostic with environment variables
- Use multi-stage builds to reduce image size
- Scan images for vulnerabilities before deployment
- Use health checks for production containers
Avoid these common Docker mistakes to build more secure, efficient, and maintainable containers. Learn from others' errors and follow best practices from the start.