Day 31 of #90DaysOfDevOps: Building My First Custom Docker Images with Dockerfile

After spending the last two days learning Docker Images, Containers, and the Container Lifecycle, today I finally learned the most important Docker skill:
Writing Dockerfiles and building custom Docker images.
Running existing images from Docker Hub is useful, but real-world DevOps engineers rarely deploy applications directly from public images. Instead, they create custom images tailored to their applications.
Today's goal was simple:
Learn how Docker images are built and create my own custom Docker images from scratch.
What is a Dockerfile?
A Dockerfile is a text file containing instructions that Docker uses to build an image.
Think of it as a recipe.
Just like a cooking recipe tells you how to prepare a dish, a Dockerfile tells Docker how to create an image.
Example:
FROM nginx:alpine
COPY index.html /usr/share/nginx/html/index.html
EXPOSE 80
CMD ["nginx","-g","daemon off;"]
Each instruction creates a new layer in the image.
My First Dockerfile
I started with a simple Dockerfile using Ubuntu as the base image.
FROM ubuntu
RUN apt-get update && apt-get install -y curl
CMD ["echo","Hello from my custom image!"]
Building the Image
docker build -t my-ubuntu:v1 .
Running the Container
docker run my-ubuntu:v1
Output:
Hello from my custom image!
This was my first custom Docker image successfully built from scratch.
Understanding Dockerfile Instructions
To understand Dockerfiles better, I created another image using several common instructions.
FROM ubuntu
RUN apt-get update && apt-get install -y curl
WORKDIR /app
COPY app.txt .
EXPOSE 8080
CMD ["cat","app.txt"]
Let's break this down.
FROM
FROM ubuntu
Defines the base image.
Every Docker image starts from another image.
RUN
RUN apt-get update && apt-get install -y curl
Executes commands during image build.
Used for:
Installing packages
Updating repositories
Configuring software
WORKDIR
WORKDIR /app
Sets the working directory inside the container.
Equivalent to:
cd /app
COPY
COPY app.txt .
Copies files from the host machine into the image.
EXPOSE
EXPOSE 8080
Documents which port the application uses.
CMD
CMD ["cat","app.txt"]
Defines the default command executed when the container starts.
CMD vs ENTRYPOINT
One concept that initially confused me was the difference between CMD and ENTRYPOINT.
Both define what happens when a container starts.
However, they behave differently.
CMD Example
Dockerfile:
FROM alpine
CMD ["echo","hello"]
Run:
docker run cmd-demo
Output:
hello
Now override the command:
docker run cmd-demo ls
Output:
bin
dev
etc
home
Observation:
The custom command completely replaced CMD.
ENTRYPOINT Example
Dockerfile:
FROM alpine
ENTRYPOINT ["echo"]
Run:
docker run entrypoint-demo hello docker
Output:
hello docker
Observation:
The arguments were appended to ENTRYPOINT.
CMD vs ENTRYPOINT
| CMD | ENTRYPOINT |
|---|---|
| Provides default command | Provides fixed executable |
| Can be overridden | Cannot be easily replaced |
| More flexible | More restrictive |
| Good for utility containers | Good for applications |
My Rule of Thumb
Use:
CMD when users may want to override behavior.
ENTRYPOINT when the container should always execute a specific program.
Building My First Website Container
This was the most exciting part of today's challenge.
I created a custom HTML page and deployed it inside an Nginx container.
Dockerfile
FROM nginx:latest
COPY index.html /usr/share/nginx/html/index.html
EXPOSE 80
CMD ["nginx","-g","daemon off;"]
Build Image
docker build -t server:v1 .
Run Container
docker run -d -p 80:80 server:v1
Docker successfully started the container.
Verify Running Containers
docker ps
Output:
0.0.0.0:80->80/tcp
This confirmed that port 80 on my EC2 instance was mapped to port 80 inside the container.
Accessing the Website
Using my EC2 public IP:
http://3.80.75.104
I was able to access my custom webpage running inside Docker.
Seeing a website served from my own Docker image was one of the most satisfying moments in my Docker learning journey so far.
Using .dockerignore
I also learned how Docker decides which files are included during builds.
I created:
.dockerignore
Contents:
node_modules
.git
*.md
.env
Benefits:
Faster builds
Smaller images
Better security
Reduced build context size
Docker Build Cache
One of Docker's most powerful features is caching.
When I rebuilt my image without changing earlier layers, Docker displayed:
CACHED
instead of rebuilding everything.
This significantly improves build speed.
Why Layer Order Matters
Bad:
COPY . .
RUN npm install
Every code change forces package installation again.
Good:
COPY package.json .
RUN npm install
COPY . .
Now dependencies remain cached until package.json changes.
This is a major optimization used in production Dockerfiles.
Key Commands Learned Today
docker build -t image:tag .
docker run image
docker run -d -p 80:80 image
docker images
docker ps
docker exec -it container bash
What I Learned
✅ How Dockerfiles build images
✅ Purpose of FROM, RUN, COPY, WORKDIR, EXPOSE, and CMD
✅ Difference between CMD and ENTRYPOINT
✅ Building and running custom Docker images
✅ Serving a static website using Nginx
✅ Using .dockerignore
✅ Docker build cache optimization
Final Thoughts
Today was the day Docker truly started making sense.
Before Day 31, I was mostly running existing images from Docker Hub.
Now I can:
Create my own Docker images
Package applications
Deploy custom websites
Optimize image builds
Understand how Dockerfiles work internally
This is a major milestone because Dockerfiles are the foundation of modern containerized applications and DevOps workflows.
One step closer to mastering Docker. 🐳🚀
#Docker #DevOps #Containerization #Nginx #Linux #CloudComputing #90DaysOfDevOps



