🐳 Dockerfile Explained – How to Build Your Own Docker Images Step by Step

KUNTAL MAITYKUNTAL MAITY
9 min read

Do you want to containerize your app but not sure how to write a Dockerfile? This guide will walk you through everything you need to know about Dockerfile – from what it is, to writing your own optimized one for Node.js, React, Python, or any other project.


📦 What is a Dockerfile?

A Dockerfile is a plain text file that contains a set of instructions Docker uses to build a custom image.

Think of it as a recipe for baking a cake – it tells Docker how to:

  • Set up the environment

  • Copy project files

  • Install dependencies

  • Set configuration

  • Run the app


⚙️ How Dockerfile Works – Behind the Scenes

When you run:

docker build -t myapp .

Docker reads the Dockerfile line by line:

  1. Starts with a base image

  2. Copies your source code

  3. Installs packages

  4. Defines the default command

Each line in the Dockerfile creates a new layer in the final image (Docker uses caching to speed up builds).


📖 Why Use Dockerfile?

  1. Reproducibility: Ensures consistent environments across different machines.

  2. Version Control: Dockerfiles can be versioned alongside your code.

  3. Automation: Simplifies the build process with a single command.

  4. Isolation: Keeps your app and its dependencies contained.

  5. Portability: Run your app anywhere Docker is available.

  6. Scalability: Easily scale your app by running multiple containers.

  7. Efficiency: Leverages Docker's layer caching to speed up builds.

  8. Security: Isolate applications and their dependencies from the host system.

  9. Collaboration: Share your Dockerfile with teammates for consistent setups.

  10. Testing: Easily test your application in a controlled environment.


🧰 All Dockerfile Commands – Explained with Use Cases & Examples

Whether you're a beginner or advanced Docker user, understanding each Dockerfile instruction helps you write cleaner, faster, and production-ready images.

Here’s a complete list of Dockerfile instructions with explanations and examples.


🔹 FROM

✅ What it does:

Specifies the base image from which you are building your container.

🛠 How to use:

FROM node:18

📌 Notes:

  • Must be the first command in most cases.

  • Supports multi-stage builds:

FROM node:18 as builder
FROM nginx:alpine

🔹 RUN

✅ What it does:

Executes commands in the container during image build.

🛠 How to use:

# RUN <command>
RUN apt-get update && apt-get install -y curl

📌 Notes:

  • Creates a new layer.

  • Use && to combine commands and reduce layers.


🔹 CMD

✅ What it does:

Specifies the default command to run when a container starts.

🛠 How to use:

# CMD ["<executable>", "<param1>", "<param2>"]
CMD ["node", "index.js"]

📌 Notes:

  • Only one CMD allowed — the last one overrides previous.

  • Use CMD for default behavior, but override with command line if needed.


🔹 ENTRYPOINT

✅ What it does:

Like CMD, but always executed — cannot be overridden easily.

🛠 How to use:

# ENTRYPOINT ["executable", "param1", "param2"]
ENTRYPOINT ["npm", "start"]

📌 Notes:

  • Use when your container is built for a specific task (e.g., CLI tools).

  • Combine with CMD to pass default args:

ENTRYPOINT ["python3"]
CMD ["app.py"]

🔹 WORKDIR

✅ What it does:

Sets the working directory for RUN, CMD, ENTRYPOINT, COPY, etc.

🛠 How to use:

# WORKDIR <container-path>
WORKDIR /usr/src/app

📌 Notes:

  • Sets the working directory for subsequent instructions inside the container

  • If the directory does not exist, it will be created.


🔹 COPY

✅ What it does:

Copies files from your host system into the container image.

🛠 How to use:

# COPY <host-path> <container-path>
COPY package.json ./
COPY . .

📌 Notes:

  • Simpler and faster than ADD.

  • Use .dockerignore to avoid copying unwanted files.


🔹 ADD

✅ What it does:

Like COPY, but with extra features:

  • Extracts local .tar.gz archives

  • Downloads from remote URLs (not recommended)

🛠 How to use:

# ADD <source> <destination>
ADD source.tar.gz /app/

📌 Notes:

  • Prefer COPY unless you need archive extraction or remote fetch.

🔹 EXPOSE

✅ What it does:

Documents the port(s) the container will listen on.

🛠 How to use:

# EXPOSE <port>
EXPOSE 3000

📌 Notes:

  • Informational only. To publish, use -p with docker run.

🔹 ENV

✅ What it does:

Sets an environment variable inside the container.

🛠 How to use:

# ENV <key>=<value>
ENV NODE_ENV=production

📌 Notes:

  • Used for configs, secrets (avoid hardcoding), or service ports.

🔹 ARG

✅ What it does:

Defines a build-time variable.

🛠 How to use:

# ARG <name>[=<default>]
ARG APP_VERSION
RUN echo $APP_VERSION

📌 Notes:

  • Use with --build-arg when running docker build.

🔹 LABEL

✅ What it does:

Adds metadata to the image.

🛠 How to use:

LABEL maintainer="kuntal@example.com"
LABEL version="1.0"

📌 Notes:

Useful for automation, image tracking, etc.


🔹 VOLUME

✅ What it does:

Declares a mount point for persistent or shared data.

🛠 How to use:

# Mounts an anonymous volume within the container
VOLUME <container-path> # VOLUME /app/data

# Mounts a host path to the container
VOLUME <[<host-path>]:<container-path>> # VOLUME /host/data:/app/data

# Mounts a named volume to the container
VOLUME <[<volume-name>]:<container-path>> # VOLUME my-volume:/app/data

📌 Notes:

  • Container will use this path for external volume mounting.

🔹 USER

✅ What it does:

Sets the user (and optionally group) under which the container runs.

🛠 How to use:

# USER <username>[:<groupname>]
USER node

📌 Notes:

  • Avoid running as root in production.

🔹 ONBUILD

✅ What it does:

Defines a trigger instruction to run when the image is used as a base for another build.

🛠 How to use:

ONBUILD COPY . /app
ONBUILD RUN npm install

📌 Notes:

Used for base images intended for other builds (not commonly used today).


🔹 HEALTHCHECK

✅ What it does:

Tells Docker how to check if the container is still healthy.

🛠 How to use:

# HEALTHCHECK CMD <command>
HEALTHCHECK CMD curl --fail http://localhost:3000 || exit 1

📌 Notes:

  • Helps in container orchestration (like Docker Swarm, Kubernetes).

🔹 SHELL

✅ What it does:

Changes the default shell used in RUN.

🛠 How to use:

# SHELL ["<shell>", "<option1>", "<option2>"]
SHELL ["/bin/bash", "-c"]

📌 Notes:

Useful when using bash-specific features or Windows containers.


🔹 STOPSIGNAL

✅ What it does:

Sets the system call signal to send for stopping the container.

🛠 How to use:

# STOPSIGNAL <signal>
STOPSIGNAL SIGINT

📌 Notes:

Useful for graceful shutdowns.


🧠 Pro Tips

  • Use COPY over ADD unless you really need its extras.

  • Always keep Dockerfile minimal and cached effectively (group layers logically).

  • Combine CMD and ENTRYPOINT wisely for CLI-based containers.

  • Use .dockerignore to improve performance and avoid bloated images.


✍️ How to Write a Dockerfile – Step-by-Step Instructions

Let’s break down a basic example.

🧪 Example: Node.js Express App

📁 Folder Structure:

my-app/
├── Dockerfile
├── package.json
├── package-lock.json
└── index.js

📄 The Dockerfile

# 1️⃣ Use an official Node.js base image
FROM node:18

# 2️⃣ Set working directory
WORKDIR /app

# 3️⃣ Copy dependency files first
COPY package*.json ./

# 4️⃣ Install dependencies
RUN npm install

# 5️⃣ Copy all source files
COPY . .

# 6️⃣ Expose a port (if your app runs on a port)
EXPOSE 3000

# 7️⃣ Default command to run your app
CMD [ "node", "index.js" ]

🧠 Explanation of Each Instruction

InstructionPurpose
FROMSets the base image (here: official Node.js)
WORKDIRSets the working directory inside the container
COPYCopies files from host to container
RUNExecutes a command (e.g. install packages)
EXPOSEInforms Docker what port the container listens to
CMDDefault command to run when container starts

🔥 Best Practices for Writing Dockerfile

Use slim base images Example: node:18-slim instead of node:18

Leverage Docker cache Copy package.json before other files to cache dependencies

Avoid installing dev dependencies in production Use npm ci --only=production

Use .dockerignore Just like .gitignore, exclude node_modules, logs, etc.

Use multi-stage builds (for smaller image sizes, especially for production)


🧪 Dockerfile for React App (Multi-stage Build)

# First stage: build the app
FROM node:18 as build

WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# Second stage: serve the app using Nginx
FROM nginx:alpine

COPY --from=build /app/build /usr/share/nginx/html

EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

🧊 Result: You get a lightweight production image with just the compiled app.


🧪 Dockerfile for Python App

FROM python:3.10-slim

WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .

EXPOSE 5000
CMD ["python", "app.py"]

🚀 Build and Run Docker Image

🏗️ Build your Docker image:

docker build -t my-node-app .

🏃‍♂️ Run the container:

docker run -p 3000:3000 my-node-app

Now visit http://localhost:3000 in your browser.


🧼 Add .dockerignore File

Create a .dockerignore file in the root of your project to reduce image size and avoid copying unwanted files:

node_modules
npm-debug.log
.env
.git

🛠 Common Dockerfile Errors

❌ Problem✅ Solution
COPY failsEnsure paths are correct and case-sensitive
Dependencies missingMake sure RUN npm install is present
App not startingCheck CMD or ENTRYPOINT syntax
Big image sizeUse slim base images and multi-stage builds

📚 TL;DR – Cheat Sheet

FROM node:18-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "index.js"]

🎯 Final Thoughts

Learning to write a clean and efficient Dockerfile is essential for any modern developer or DevOps engineer. Whether you're deploying a Node.js API, a React frontend, or a Python script — Dockerfiles allow you to package your entire environment and run it anywhere.

🔥 Start simple, optimize later. First get it working, then improve caching, layer order, and size.


✅ What's Next?

  • Learn about docker-compose.yml

  • Use .env files with Docker

  • Set up CI/CD to build images automatically


📝 Additional Resources

✍️ Author’s Note

This guide is a living document. If you find any errors or have suggestions for improvements, please let me know! I’m always looking to make this resource better for everyone.


💬 Have Questions or Suggestions?

Drop a comment below or connect with me on LinkedIn or GitHub. Let’s make apps faster together! 🚀

0
Subscribe to my newsletter

Read articles from KUNTAL MAITY directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

KUNTAL MAITY
KUNTAL MAITY

I’m a passionate Full-Stack Developer who loves building performance-driven web and mobile applications. I work primarily with JavaScript, React, Next.js, Node.js, and MongoDB, and I enjoy simplifying complex concepts into developer-friendly tutorials. On this blog, you'll find hands-on guides, real-world projects, and developer insights—all aimed at helping you level up your coding skills and build production-ready apps. Whether you're into backend performance, frontend polish, or full-stack architecture, there's something here for you! Let’s learn, build, and grow together. 💻✨