Skip to main content

Command Palette

Search for a command to run...

Building Docker Images

Navigating the ccomplexities surrounding images and containers for beginners

Updated
16 min read
Building Docker Images

There is nothing you wan tell me...DevOps engineers are photographers. Baddest ones at it sef.

So, as a newbie in DevOps, one of the core concept you will learn after Virtualization is Containerization. Believe me, there are plenty "zations" to learn. At first, it looks like a very big concept but just like ladies that always form hard to get, Containerization is actually soft on the inside.

You might want to read this short introduction to containerization before you proceed: Read Here

The main reason for containerization is to ensure your applications runs without having to think about infrastuctural dependencies. You won't have to be thinking if your application will run on Linux or Windows. Neither will you be thinking of Setting up the environments needed to ensure the application runs optimally. Also, it makes scalling so smooth and fast. Read the article to gain more.

What is an Image?

Nah! I'm not going to be giving one yeye definition. We are all bored of that.

If you read the Virtualization article, you will realize that what we were doing is to just install an application (Hypervisor) on our machine that will split our machines into different machines with the specified Operating System we want. So, if you have a laptop that is running Windows OS, you can install VirtualBox (Hypervisor Type II) on it and create another instance of your Laptop which will be running Ubuntu (Linux) OS. Now you literally have two laptops.

Think of this in relation to Naruto's Shadow Clone Jutsu (Kage Bunshin no Jutsu) but in this case, you can ensure your clones are different characters. Ok, it's not helpful for those who sees Anime as shit.

So, your applications will be running on one/more of these clones (Virtual Machines). You will want to run applications that requires Windows Environment on a Windows Virtual Machine and that for Linux on Linux VM.

But Containerizations kicked this core dependency on environment away. It took the whole application deployment to a new level by ensuring our applications are stripped of all components that requires to speak to a specific environment. Now all we have are binaries that are free to run on any OS. This is just a water-down explanation. To ensure developers and ops are not fighting on "It works on my system, it didnt work on yours" issue, Containerization employs the use of Image where all your application code, commands needed to run it and all dependencies for smooth running are bundled into one single file called IMAGE. Take this image anywhere and run, you will always get the same thing.

One beautiful analogy for this image is our music industry. Instead of Wizkid to be coming to our various house to sing "Pakurumo", he recorded the song and shared it online. That way, whether i use spotify, youtube, or even download it on Naijaloaded dot com - the song will sound the same way.

Once you have your Image built from your codes, you can give anyone this image and they can deploy the application bundled in it anywhere with the same behaviour.

When you run your image, it will run as a container. You can stop the container, delete the container and recreate the entire deployment from the initial image within seconds. And this is what makes deployment so sweet with containerization unlike Virtualization.

In virtualization, deploying and managing applications on your VMs is tedious and not as smooth as this.

Creating the Image

Ok, enough of the plenty shalaye. Let's start what we came here for.

To Create an image of your application, you will need a good mirrorless Canon Camera. Oops! Don't look at me that way.

To create the image of your application, you will need to create a Dockerfile file in your application root directory. In this file, you will write the recipe for how your image should be built.

There are three principles I follow when creating an image. This is not a book information, rather it's just something that guides me personally in writing my Dockerfile.

Dockerfile is the recipe book where you write the steps on how your image should be built

  1. Understand how you will normally deploy the applications on the server./laptop This will help you in knowing the command needed to be put in your Dockerfile

  2. Prioritize Speed when building images. Image building is not one in a lifetime task. Your app will have versions and each versions will be built from this same Dockerfile.

  3. Handle dependencies efficiently. Your application might depend on the availability of another application for it to run. E.g. DB. In the example we will be using, our backend application requires the availability of Postgresql on port 5432. Without this, you won't be able to do DB migrations with Prisma. So, this have to be managed while building the image.

Example 1: Baby level

Let's try and create an image for our basic HTML and CSS application.

In this project, we will be using Nginx to host our static website. Since we are talking about Docker, we will be deploying Nginx in docker also.

We will be following the following steps

  1. Clone the repository here:

  2. Understand how this application works and how I would have run it on my laptop

  3. Now replicate the same in our Dockerfile

Now, let's dive in.

Clone the repo. The html file we need is in my repository

git clone https://github.com/KodenOps/basic_website_sample.git

This will create the "basic_website_sample" folder in your directory. Now navigate into the directory.

cd basic_website_sample

If you use "ls" to list the files in this directory, you should see three files

a. images.jpeg

b. index.html

c. README.md

Understand how this application works and how I would have run it on my laptop

What we want to do is simple.

  1. We will run the nginx as docker
sudo docker run -d --name nginx -p 8000:80 nginx
  1. Let's see the status of the new container
sudo docker ps

You should see something like this. Look under the NAMES column for our Nginx container. Now we have our Nginx running now. You can access this Nginx on your browser via: http://localhost:8000/

You should see the default NGINX page.

  1. The next thing we want to do is to change the default NGINX page into our own static page. You know the "index.html" we have in the repo we cloned from git. How are we doing it? we will just go into the folder that has the default index.html and replace it with our own.

a. Let's enter into our container: As you've known, the entire package needed to run an image as container will always be bundled into the image. These are unpacked into the container when you run it. So, we will need to enter the container (the way you would have ssh into a VM in virtualization)

sudo docker exec -it nginx /bin/bash

b. The directory our "Index.html" is in " /usr/share/nginx/html "

cd /usr/share/nginx/html

If you "ls" you will see the files. Now you can exit the container and go back to your main terminal

exit

c. Now let's move our files into the "/usr/share/nginx/html" directory in our NGINX container. To do this, we will use the "DOCKER CP" command. This is used in copying files from/to our container.

# syntax to copy file to a docker container
sudo docker cp <source dir> CONTAINER NAME:<destination dir>

# syntax to copy file from a docker container
sudo docker cp CONTAINER NAME:<source dir> <destination dir>

So, let's use this command to copy our files into the container.

# copy the index.html

sudo docker cp ./index.html nginx:/usr/share/nginx/html

# copy the images.jpeg also

sudo docker cp ./images.jpeg nginx:/usr/share/nginx/html

And that's all! You can refresh your page now: http://localhost:8000/

You should see this page now

Now we know the core steps to follow

  1. Setup NGINX and ensure the port of the internal NGINX (on port 80) is mapped externally on port 8000

  2. Move my own index.html into the /usr/share/nginx/html.

  3. Access your app on http://localhost:8000/

That's all. This will be your basis in creating your image

Replicate this in creating your Docker Image

Now that you've understood how you would have setup your application on the server, it is easier to replicate same inside your Dockerfile. You just have to folow same process

  1. create a file inside your " basic_website_sample " called Dockerfile (no extension)

  2. Since our application will be running on NGINX, that is where we will start from nginx:alpine. This is a lightweight version of nginx.

So the way this file works is this. You need a base image. That is the main engine powering your application. Since you know that our application (index.html) is running on nginx, this will be our base image. The dockerfile can be seen as an empty box. The base image is more like the type of the box (leather, wooden, paper, etc). We have set the type of our box, but it is still empty. Now we need to move something into the box. So what we will do next is to pick our index.html from our directory and put into our box (Dockerfile). The path we will be putting it is /usr/share/nginx/html. We also need to add a connection socket to our box, so that anybody using this box in the future can be connecting to the socket. So, we will be exposing the default NGINX port 80. That's all.

# 
FROM nginx:alpine

COPY . /usr/share/nginx/html

EXPOSE 80
  1. Now build your app. Ensure you are in the " basic_website_sample " directory when you run the command below
sudo docker build -t my-special-app:latest .
  1. Check your new image using the command below
sudo docker images

# to show only this new image

sudo docker images | grep my-special-app
  1. Now let's run our application image. Remember our NGINX is currently running our app on http://localhost:8000/. So we will be using a different port to host our new app image
sudo docker run -d --name mystatic_app -p 5000:80 my-special-app
  1. Now access your app on http://localhost:8000/. Now you can keep creating this particular application with that single "Docker Run" command. You don't have to worry about all the copying of "index.html" or exec-ing into the container again.

Example 2: Multi-app Project

In this project, we will be working on an application that has a frontend built in NextJS and also a backend built on Node, Prisma ( a database toolkit to interact with database) and PostGre SQL Database.

We will be creating two images here.

  1. Frontend image

  2. Backend image

Our PostGre will be independent. For the sake of this quick example, we will be setting up our DB on Docker also. Although, your Database will surely not run on Docker in a full production setting. Just cut me small slack here.

Remember our three golden guidelines?

  1. Clone the repository here:

  2. Understand how this application works and how I would have run it on my laptop

  3. Now replicate the same in our Dockerfile

Clone Repo

# clone the frontend

git clone https://github.com/DevOps-Bootcamp-2026/devops-bootcamp-linker-frontend.git

# clone the backend

git clone https://github.com/DevOps-Bootcamp-2026/devops-bootcamp-linker-backend.git

Codebase is the sole property of Achebe Okechukwu Peter

Learn How to setup the application normally

DB SETUP:

  • Let's run our DB container. We will be seting the our container name to be "db", password to be "mysecretissecret", and expose the port on "5432". We will be using the "postgres" image from dockerhub
sudo docker run --name db -e POSTGRES_PASSWORD=mysecretissecret -p 5432:5432 -d postgres

# check the status of your container
sudo docker ps | grep db

# check the logs of our db
sudo docker logs db -f

# you should see at this "database system is ready to accept connections" end of the line

That's all you need for now.

BACKEND SETUP:

  • Navigate into "devops-bootcamp-linker-backend" directory
cd devops-bootcamp-linker-backend
  • Install the dependencies needed for the node app as highlighted in the package.json file
npm install
  • The environment variable in the folder is labeled as .env.local.example. We need to rename it to .env. But the way we will do this is to just copy the file into a new file called .env.
cp .env.local.example .env.local
  • Edit the .env file to insert the db username (default is postgres) and password (the mysecretissecret you put when setting up db). The link localhost:5432 will remain the same since your db container is running on the same server as this backend code
# .env file

# Database
DATABASE_URL="postgresql://postgres:mysecretissecret@localhost:5432/DATABASE?schema=public"
PORT=3001
# NextAuth
NEXTAUTH_SECRET="changeme"
NEXTAUTH_URL="http://localhost:3000"

# Cloudinary (keep your existing values)
CLOUDINARY_CLOUD_NAME="changeme"
CLOUDINARY_API_KEY="changeme"
CLOUDINARY_API_SECRET="changeme"
  • Now run the db migration so that Prisma can connect to your Postgres Database
npx prisma migrate dev
  • Now start your backend
npm run dev

FRONTEND SETUP:

  • Navigate into "devops-bootcamp-linker-frontend" directory
cd devops-bootcamp-linker-frontend
  • Install the dependencies needed for the NextJS as highlighted in the package.json file
npm install
  • The environment variable in the folder is labeled as .env.local.example. We need to rename it to .env. But the way we will do this is to just copy the file into a new file called .env.
cp .env.local.example .env
  • Now run the application in Dev mode
npm run dev

Now we have an idea of what we need to put in our Dockerfile. We will literally replicate each of these steps (frontend and backend) in our dockerfile

Setup Dockerfile

Frontend

we already know this is a NextJS app that literally run on NodeJS. So this will be our base image. This is the type of our empty Box. Always use alpine tag for your image because they are lightweight

FROM node:25-alpine

Now since this is a bigger app, we need to separate our concern by creating a directory to put our app inside the container (when you run the image). So we will have to create a partition in our box and call it "/app".

FROM node:25-alpine

WORKDIR /app

From our setup initially, we are meant to run "npm install" but for it to run, we need an important file (package.json and package-lock.json). So we need to handpicked these files and drop it inside the box.

FROM node:25-alpine

WORKDIR /app

# we use * as a wildcard to select everything that matches. This will pick package.json andpackage-lock.json  
 
COPY package*.json ./

We can now run our "npm install". But here is a catch, there is a faster command which do the same thing. I will use "npm ci" which work for containers.

FROM node:25-alpine

WORKDIR /app 
 
COPY package*.json ./

RUN npm ci

Remember that the only files we have inside our box is package.json, package-lock.json and the node_modules folder that "npm ci" just created. Our main application is not even here yet. So we need to copy everything in our directory into our box now

FROM node:25-alpine

WORKDIR /app 
 
COPY package*.json ./

RUN npm ci

COPY . .

Now we have everything we need for our app. It now remain the .env thingy we modify during the setup.

FROM node:25-alpine

WORKDIR /app 
 
COPY package*.json ./

RUN npm ci

COPY . .

RUN cp .env.local.example .env

One last check, let's add our connection socket to the box

FROM node:25-alpine

WORKDIR /app 
 
COPY package*.json ./

RUN npm ci

COPY . .

RUN cp .env.local.example .env

EXPOSE 3000

Finally, let's run our application in dev mode

ROM node:25-alpine

WORKDIR /app 
 
COPY package*.json ./

RUN npm ci

COPY . .

RUN cp .env.local.example .env

EXPOSE 3000

CMD ["npm", "dev"]

You see, now our box is the same as how we initially set up our application. We can now proceed to build our application image. But one last thing before we build it. Because we do not want to copy unnecessary folders when we do (COPY . .), we will need to handle what we want to ignore. One key folder we want to ignore is the (node_modules) folder that was created when we ran (npm install) the other time. To do this, create a new file and call it .dockerignore. Now paste the following code into the file

node_modules
.git
.gitignore
Dockerfile
.dockerignore

we will be ignoring all the files we listed in our file including our Dockerfile and .dockerignore files.

Now that we have that sorted, lets build our image

sudo docker build -t devops-bootcamp-linker-frontend:latest .

Then check the image

sudo docker images | grep devops-bootcamp-linker-frontend

Congratulation, you now have your frontend image.

Backend

If you understood the process for Frontend, you should be able to create for backend also.

Check the process of starting the Backend on our server normally and try to replicate.

The backend is a node application which is also powered by NodeJS. So our base application is still node:25-alpine.

We will also need to set the partition in our box also and call it /app where the application will run in the container.

Then we will copy our package.json and package-lock.json files into the box and then run our npm install (npm ci).

Then we will copy the remaining files in our directory into our box

Finally, we will create a .env file from our .env.local.example

FROM node:25-alpine

WORKDIR /app 
 
COPY package*.json ./

RUN npm ci

COPY . .

RUN cp .env.example .env

There is a little problem with the next thing we are meant to do. From our initial setup, we need to run "npx prisma migrate dev" but if we do this directly, it will throw error since our db is not running currently. So, let's create a shell file and call it run_prisma.sh

#!/bin/bash
# This script is used to run the shell in the container

echo "Running database Migration..."
npx prisma migrate dev

echo "Starting the server..."
npm run dev

These are our last commands.

Now we can reference this shell file in the Dockerfile. We need to first give it an "executable" permission and then run it. Create the shell file in the same backend directory. This way it will be part of the files to be copied when you do (COPY . .)

FROM node:25-alpine

WORKDIR /app 
 
COPY package*.json ./

RUN npm ci

COPY . .

RUN cp .env.example .env && chmod +x runshell.sh

CMD ["./runshell.sh"]

Now we can build our image now.

sudo docker build -t devops-bootcamp-linker-backend:latest .

Then check the image

sudo docker images | grep devops-bootcamp-linker-backend