DEV Community

Haripriya Veluchamy
Haripriya Veluchamy

Posted on

Working with Docker Images: From Basics to Best Practices

After getting comfortable with running containers, the next step in my Docker journey was learning how to work with images effectively. This post covers everything I've learned about using existing images, creating custom ones, and managing them properly.

Using Existing Images

Pulling Images from Docker Hub

Docker Hub has thousands of pre-built images you can use. Here's how to get them:

# Pull the latest version of an image
docker pull nginx

# Pull a specific version using tags
docker pull nginx:1.21

# Pull an image from a different registry
docker pull mcr.microsoft.com/dotnet/sdk:6.0
Enter fullscreen mode Exit fullscreen mode

I usually pull images explicitly before using them, but docker run will automatically pull images if they're not available locally.

Understanding Image Tags and Versioning

Tags help you identify different versions of the same image:

[registry/][username/]repository[:tag]
Enter fullscreen mode Exit fullscreen mode

For example:

  • nginx:latest - Official Nginx image, latest version
  • username/my-app:1.0 - Custom app, version 1.0
  • registry.example.com/team/service:v1.2.3 - Private registry image

Important things I've learned about tags:

  • latest doesn't necessarily mean the newest version - it's just the default tag
  • Tags are just pointers to image IDs
  • Multiple tags can point to the same image
  • Tags are mutable - they can be moved to different images
  • For production, always use specific version tags, never latest

Creating Custom Images

Writing Dockerfiles

A Dockerfile is a text file with instructions for building an image:

FROM node:14-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .

EXPOSE 3000

CMD ["node", "index.js"]
Enter fullscreen mode Exit fullscreen mode

Common Dockerfile instructions:

  • FROM: Base image
  • WORKDIR: Set working directory
  • COPY/ADD: Copy files into the image
  • RUN: Execute commands during build
  • ENV: Set environment variables
  • EXPOSE: Document container ports
  • CMD/ENTRYPOINT: Default command when container starts

Best Practices for Dockerfile Creation

Through trial and error, I've learned these best practices:

Use specific base image tags

   # Good
   FROM node:14-alpine
   # Not good
   FROM node:latest
Enter fullscreen mode Exit fullscreen mode

Order instructions from least to most frequently changing

   # Dependencies change less often than source code
   COPY package.json .
   RUN npm install
   COPY . .
Enter fullscreen mode Exit fullscreen mode

Group related RUN commands to reduce layers

   RUN apt-get update && \
       apt-get install -y something && \
       apt-get clean
Enter fullscreen mode Exit fullscreen mode

Use .dockerignore to exclude unnecessary files

   node_modules
   npm-debug.log
   Dockerfile
   .git
Enter fullscreen mode Exit fullscreen mode

Run as non-root user for security

   RUN useradd -r appuser
   USER appuser
Enter fullscreen mode Exit fullscreen mode

Use multi-stage builds (more on this later)

Building Images with docker build

Once you have a Dockerfile, building an image is straightforward:

# Build with a tag
docker build -t my-app:1.0 .

# Build with a specific Dockerfile
docker build -f Dockerfile.prod -t my-app:1.0 .

# Build with build arguments
docker build --build-arg VERSION=1.0 -t my-app:1.0 .
Enter fullscreen mode Exit fullscreen mode

The . at the end specifies the build context - the directory containing your application files.

Understanding Image Layers and Caching

Docker images consist of layers, which are cached for efficiency:

  • Each instruction in a Dockerfile creates a layer
  • Layers are cached and reused when possible
  • If a layer changes, all downstream layers must be rebuilt
  • You can see layers with docker history image-name

This is why the order of instructions matters. For my Node.js apps, I always:

  1. Copy package.json first
  2. Install dependencies
  3. Copy application code

This way, if only my code changes (but not dependencies), Docker reuses the cached dependency layer.

Managing Images

Listing and Removing Images

Basic image management:

# List all images
docker images

# Remove an image
docker rmi image-id

# Remove all dangling images
docker image prune

# Remove all unused images
docker image prune -a

# Force remove all images
docker rmi $(docker images -q) -f
Enter fullscreen mode Exit fullscreen mode

Tagging and Pushing to Registries

To share your images, you need to tag and push them:

# Tag an image
docker tag my-app:1.0 username/my-app:1.0

# Log in to Docker Hub
docker login

# Push to Docker Hub
docker push username/my-app:1.0

# Push to another registry
docker login registry.example.com
docker tag my-app:1.0 registry.example.com/team/my-app:1.0
docker push registry.example.com/team/my-app:1.0
Enter fullscreen mode Exit fullscreen mode

Multi-Stage Builds for Smaller Images

This technique dramatically reduces image size by using multiple FROM statements:

# Stage 1: Build stage
FROM node:14 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# Stage 2: Production stage
FROM node:14-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
RUN npm install --only=production
EXPOSE 3000
CMD ["node", "dist/index.js"]
Enter fullscreen mode Exit fullscreen mode

Benefits I've seen with multi-stage builds:

  • Final images are much smaller
  • Build tools aren't included in production
  • Better security with smaller attack surface
  • Separation of build and runtime concerns

Image Size Optimization Techniques

These techniques have helped me reduce image sizes:

Use Alpine-based images

   FROM node:14-alpine  # Much smaller than node:14
Enter fullscreen mode Exit fullscreen mode

Clean up in the same RUN step

   RUN apt-get update && \
       apt-get install -y something && \
       apt-get clean && \
       rm -rf /var/lib/apt/lists/*
Enter fullscreen mode Exit fullscreen mode

Remove unnecessary files

   RUN npm install && \
       npm cache clean --force
Enter fullscreen mode Exit fullscreen mode

Don't install dev dependencies in production

   RUN npm install --only=production
Enter fullscreen mode Exit fullscreen mode

Consider distroless images for even smaller images

   FROM gcr.io/distroless/nodejs
Enter fullscreen mode Exit fullscreen mode

Use .dockerignore to exclude unnecessary files from the build context

Distroless Images

Distroless images are minimalist Docker images that contain:

  • Your application
  • Its runtime dependencies
  • Nothing else

Unlike regular base images like Ubuntu or Alpine, distroless images don't contain:

  • Package managers (apt, apk, npm)
  • Shells (bash, sh)
  • Standard Linux utilities (ls, cp, vim)

Example of a distroless Node.js image:

FROM node:14 AS builder
WORKDIR /app
COPY . .
RUN npm install && npm run build

FROM gcr.io/distroless/nodejs:14
COPY --from=builder /app/dist /app
WORKDIR /app
CMD ["index.js"]
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • Drastically smaller image size
  • Much smaller attack surface
  • Better security (no shell access)
  • Only what's needed to run the application

The downside is you can't easily debug inside these containers (no shell), but that's actually a security feature in production.

I use distroless images for production deployments when security is a priority and debugging inside the container isn't needed.

Conclusion

Working effectively with Docker images has made my development workflow much smoother. I can quickly create consistent environments, share them with teammates, and deploy them confidently.

The key lessons I've learned:

  • Use specific tags for consistency
  • Structure Dockerfiles for efficient caching
  • Leverage multi-stage builds for smaller images
  • Follow best practices for security and optimization

Next up, I'll cover Docker volumes and data persistence - a crucial topic for stateful applications.


Next up: "Docker Volumes and Data Persistence"

Top comments (0)