Skip to main content

Command Palette

Search for a command to run...

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

Updated
6 min read
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