Skip to content

7 Common Docker Mistakes Beginners Make and How to Fix Them

Docker broke my production build last month. I spent four hours debugging why my app worked locally but crashed in production. The culprit? I used node:latest in my Dockerfile, and Node.js had released a new version overnight. My dependencies weren’t compatible.

That mistake taught me something important. Docker’s simplicity hides traps that bite beginners repeatedly. I’ve made most of these mistakes myself. Here are the seven most common ones, why they cause problems, and exactly how to fix each one.

Mistake 1: Using :latest Tags

I used to think :latest meant “the most recent stable version.” It doesn’t. It just means “whatever was built last.”

When you pull node:latest, you get whatever version the Docker Hub maintainers pushed most recently. If that version changes between builds, your application might break. You won’t know why because you didn’t change anything.

The fix: Pin specific versions.

Dockerfile
# WRONG: Unpredictable
FROM node:latest
# CORRECT: Reproducible
FROM node:18.17-alpine

Pin both the major version and the specific release. This ensures every build pulls the exact same image. When you want to upgrade, you do it intentionally, not by accident.

Mistake 2: Missing .dockerignore

My first Docker image was 2.3 GB. I couldn’t understand why. Then I discovered I was copying my entire .git folder, node_modules, build artifacts, and even .env files into the image.

Docker copies everything in your build context unless you tell it not to. Without a .dockerignore file, you bloat your images and potentially expose sensitive data.

The fix: Create a .dockerignore file.

.dockerignore
.git
.gitignore
node_modules
npm-debug.log
logs
.env
.env.local
*.md
.DS_Store
coverage
.nyc_output

Add this file to your project root. Docker will exclude these files from the build context. Your images become smaller, builds become faster, and you avoid accidentally including secrets.

Mistake 3: Running Containers as Root

Docker containers run as root by default. I didn’t think much about this until I read about container escape vulnerabilities. If an attacker compromises your container, they have root access inside it. That’s one step closer to your host system.

The fix: Create a non-root user in your Dockerfile.

Dockerfile
FROM node:18-alpine
# Create non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --chown=appuser:appgroup . .
# Switch to non-root user
USER appuser
CMD ["node", "index.js"]

This adds a few lines but significantly improves security. The container can only access what the appuser can access, limiting damage from potential breaches.

Mistake 4: Not Cleaning Up Orphaned Resources

Docker doesn’t automatically delete old images, stopped containers, or unused volumes. They pile up like unwashed dishes. After a few months of development, I had 47 GB of Docker junk on my machine.

The fix: Run cleanup commands regularly.

cleanup commands
# Remove stopped containers, unused networks, and dangling images
docker system prune
# More aggressive: remove all unused images and volumes
# WARNING: This deletes images not currently in use
docker system prune -a --volumes
# Remove only orphaned images (no container references)
docker image prune
# Remove unused volumes
docker volume prune

Run docker system df first to see how much space Docker is using. Then decide which cleanup level you need. I run docker system prune weekly.

Mistake 5: Confusing Images and Containers

I edited my code and ran docker run my-app. The app still showed the old behavior. I was confused for an hour.

The problem was thinking “running” an image updates it. It doesn’t. Images are templates. Containers are instances. You need to rebuild the image first.

The fix: Always rebuild after changes.

rebuild workflow
# 1. Make code changes
# 2. Rebuild the image
docker build -t my-app .
# 3. Stop old container
docker stop my-container
docker rm my-container
# 4. Run new container
docker run -d --name my-container my-app

Or use Docker Compose, which handles this better:

docker compose rebuild
docker-compose up --build

This rebuilds images and recreates containers in one command.

Mistake 6: Hardcoding Secrets in Dockerfiles

I’ve seen Dockerfiles with database passwords directly in the file. That’s a security disaster waiting to happen. Even if you don’t push to a public registry, secrets in Dockerfiles leak through version control, logs, and image layers.

The fix: Use environment variables or Docker secrets.

docker-compose.yml
services:
app:
build: .
environment:
- DB_PASSWORD=${DB_PASSWORD}
- API_KEY=${API_KEY}

Create a .env file locally:

.env
DB_PASSWORD=your_secure_password
API_KEY=your_api_key

Add .env to your .gitignore immediately. Never commit it.

For production, use Docker Swarm secrets or Kubernetes secrets. These mount secrets as files or inject them securely at runtime.

Mistake 7: Stuffing Multiple Services in One Container

I once put Node.js, Redis, and a cron job scheduler in one container. It worked, but it violated Docker’s core principle: each container should do one thing well.

That monolithic container was hard to debug, hard to scale, and hard to update. When Redis crashed, the whole container went down.

The fix: Use Docker Compose for multiple services.

docker-compose.yml
services:
backend:
build: ./backend
ports:
- "5000:5000"
depends_on:
- db
- redis
frontend:
build: ./frontend
ports:
- "3000:3000"
depends_on:
- backend
db:
image: postgres:15
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
redis:
image: redis:7-alpine
volumes:
postgres_data:

Each service runs in its own container. You can scale them independently, restart them separately, and debug them in isolation.

Quick Reference Table

Here’s a summary of each mistake and its fix:

MistakeConsequenceFix
Using :latest tagUnpredictable builds, breaks when images updatePin versions like node:18.17
Missing .dockerignoreBloated images, secrets leak, slow buildsExclude .git, node_modules, .env
Running as rootSecurity vulnerabilityAdd non-root user with USER appuser
Not cleaning upDisk space wasted (GBs of junk)Run docker system prune regularly
Image vs container confusionChanges don’t appearRebuild image before running
Hardcoding secretsCredentials leakUse .env files and Docker secrets
Monolithic containersHard to debug, scale, and updateUse Compose, one service per container

Why These Fixes Matter

Each mistake compounds over time. A missing .dockerignore slows every build. An unpinned :latest tag breaks deployments randomly. Root containers create security vulnerabilities that attackers can exploit.

But the fixes are straightforward. Add a .dockerignore file today—it takes two minutes. Pin your image versions. Create non-root users. These small changes make Docker reliable instead of frustrating.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments