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

Table of contents
- 📦 What is a Dockerfile?
- ⚙️ How Dockerfile Works – Behind the Scenes
- 📖 Why Use Dockerfile?
- 🧰 All Dockerfile Commands – Explained with Use Cases & Examples
- ✍️ How to Write a Dockerfile – Step-by-Step Instructions
- 🔥 Best Practices for Writing Dockerfile
- 🧪 Dockerfile for React App (Multi-stage Build)
- 🧪 Dockerfile for Python App
- 🚀 Build and Run Docker Image
- 🧼 Add .dockerignore File
- 🛠 Common Dockerfile Errors
- 📚 TL;DR – Cheat Sheet
- 🎯 Final Thoughts
- ✅ What's Next?
- 📝 Additional Resources
- ✍️ Author’s Note
- 💬 Have Questions or Suggestions?

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:
Starts with a base image
Copies your source code
Installs packages
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?
Reproducibility: Ensures consistent environments across different machines.
Version Control: Dockerfiles can be versioned alongside your code.
Automation: Simplifies the build process with a single command.
Isolation: Keeps your app and its dependencies contained.
Portability: Run your app anywhere Docker is available.
Scalability: Easily scale your app by running multiple containers.
Efficiency: Leverages Docker's layer caching to speed up builds.
Security: Isolate applications and their dependencies from the host system.
Collaboration: Share your Dockerfile with teammates for consistent setups.
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
archivesDownloads 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
withdocker 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 runningdocker 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
overADD
unless you really need its extras.Always keep Dockerfile minimal and cached effectively (group layers logically).
Combine
CMD
andENTRYPOINT
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
Instruction | Purpose |
FROM | Sets the base image (here: official Node.js) |
WORKDIR | Sets the working directory inside the container |
COPY | Copies files from host to container |
RUN | Executes a command (e.g. install packages) |
EXPOSE | Informs Docker what port the container listens to |
CMD | Default 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 fails | Ensure paths are correct and case-sensitive |
Dependencies missing | Make sure RUN npm install is present |
App not starting | Check CMD or ENTRYPOINT syntax |
Big image size | Use 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 DockerSet 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! 🚀
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. 💻✨