Docker Basics: The Art of Containerization

Docker Basics: The Art of Containerization

·

18 min read

Problems before Docker:

At a software company, a new developer named Jake joined the team. Fresh out of college and eager to make an impact, Jake was assigned to work on a project called Webify, a web application that the team had been maintaining for months.

On his first day, Jake asked the lead developer, Sara, how to set up the project. Sara shared the steps:

  1. Install Nginx for serving the frontend.

  2. Install Python 3.8 for running the backend.

  3. Install PostgreSQL for the database.

  4. Use VS Code for development.

Jake enthusiastically followed the instructions but opted to install the latest versions of everything, thinking it would be better. After hours of setup, he confidently ran the application, but instead of a working app, he was greeted by a screen full of errors!

Jake double-checked the steps, but the errors persisted. Frustrated, he went to Sara.

“Hey, Sara,” Jake said. “I followed your steps, but the app isn’t running. I think there’s a bug in the project.”

Sara frowned. “That’s odd. It works fine on my machine.”

Curious, Sara sat at Jake’s desk, ran the same commands, and found the errors. Then she returned to her own machine, ran the app, and it worked perfectly.

After some investigation, Sara discovered the issue:

  • Jake had installed Nginx 1.25, but the project required Nginx 1.20.

  • Jake had installed Python 3.12, but the project relied on features only available in Python 3.8.

  • Jake's PostgreSQL setup wasn’t configured the same way as Sara’s.

The project was breaking because the environments didn’t match.


Enter Docker: The Game Changer!

“Jake,” she said, “this isn’t your fault. Setting up environments manually is tricky and error-prone. That’s why we use Docker!”

Sara explained:

“Docker lets us package the app, along with all its dependencies—like specific versions of Nginx, Python, and PostgreSQL—into a container. These containers run the same everywhere, whether it’s my laptop, your laptop, or a server in production.”


How Docker Solves the Problem:

  1. Consistency: Docker ensures everyone works in the same environment. Instead of installing software manually, Jake could just run a Docker container with the exact setup the app needed.

  2. Simplicity: Jake didn’t need to worry about version mismatches anymore. Docker used a Dockerfile to specify all the required versions.

  3. Portability: Once Jake’s app ran in Docker on his laptop, it would run the same way on Sara’s laptop or even in production.

  4. Quick Onboarding: Jake could’ve been up and running in minutes, without wrestling with installations and configurations.


What is Docker:

Docker is a containerization platform that enables developers to package applications along with all their dependencies (libraries, tools, configurations) into lightweight, portable containers. These containers ensure that the application runs consistently across different environments, whether it's your local machine, a testing server, or a production server.


Docker prerequisite terminologies:

  1. What is a Dockerfile?

    A Dockerfile is like a recipe book that Docker uses to build an image. It’s a text file with step-by-step instructions for creating a Docker image.

    • The Base Image to Use: What foundation to start with (e.g., Python, Ubuntu).

    • What to Add: Your app’s code, libraries, and dependencies.

    • How to Set It Up: Commands to run, environment variables, and file locations.

  2. What is an Image?

    A Docker image is like a recipe for creating a container.

    • It’s a blueprint that tells Docker what to put in the container and how to set it up.

    • When you run an image, Docker uses it to create a container.

  3. What is a Base Image?

    A base image is the starting point for your Docker image. It’s like the foundation of a house:

    • A base image provides the operating system and basic tools your app needs.

    • You build on top of it by adding your app and its specific requirements.

  4. What is a Container?

    A container is like a portable, self-contained box for your application. Inside this box:

    • Your application runs.

    • It has everything the app needs to work (libraries, tools, settings).

Hmm, a bit tricky to understand, right? Let's make it clear with a simple example:

You’ve built a web app called Lend Manager in Go. It helps users manage loans and repayments. Now, you want to containerize the app to make it easier to:

  1. Run it consistently across different environments (your laptop, servers, or your friend’s machine).

  2. Share it with your friend so they don’t have to manually install Go or dependencies to run it.

1. Create the Dockerfile

You start by writing a Dockerfile to define how your app should be containerized.

Here’s an example Dockerfile for your Go app:

# Use an official Go image as the base
FROM golang:1.21

# Set the working directory inside the container
WORKDIR /app

# Copy the Go code into the container
COPY . .

# Build the Go app inside the container
RUN go build -o lend-manager

# Expose port 8080 for the app
EXPOSE 8080

# Command to run the app
CMD ["./lend-manager"]

Why Write This Dockerfile?

  • FROM: Starts with a pre-installed Go environment (no need to install Go manually on different machines).

  • WORKDIR: Sets up a clean workspace inside the container.

  • COPY: Copies your code into the container so it can be built.

  • RUN: Compiles your Go app into an executable (lend-manager).

  • EXPOSE: Opens port 8080 to make the app accessible.

  • CMD: Defines how to start the app (./lend-manager).

2. Build the Docker Image

Now, you build the Docker image using the Dockerfile.

Run this command in your terminal:

docker build -t lend-manager-app .
  • What happens: Docker follows the instructions in the Dockerfile and creates an image called lend-manager-app.

  • This image contains the entire environment (OS, Go runtime, app code, etc.) needed to run your app.

3. Run the Docker Container

You run the app as a container from the image.

Run this command:

docker run -p 8080:8080 lend-manager-app

What happens:

4. Share the App with Your Friend

You want your friend to easily run the app without setting up Go or manually building the code.

  • Share the Docker Image via a Registry

    You can upload the image to Docker Hub or a private registry:

  •   docker tag lend-manager-app your-dockerhub-username/lend-manager-app:latest
      docker push your-dockerhub-username/lend-manager-app:latest
    

    Your friend can pull the image using:

  •   docker pull your-dockerhub-username/lend-manager-app:latest
      docker run -p 8080:8080 your-dockerhub-username/lend-manager-app:latest
    

    Step 1: Running the Image

    When your friend pulls and runs the Docker image, this happens:

    • Pulling the Image
      If the image isn’t already on your friend’s system, Docker will download it from the registry (e.g., Docker Hub).
      The image contains:

      • The app’s code.

      • All the dependencies (libraries, tools, etc.) needed for the app.

      • A pre-configured environment to run the app (e.g., the Go runtime).

Your friend doesn’t have to manually install or configure anything!

  • Step 2: Running the Container

    When your friend runs the Docker container from the image:

    1. The App Runs

      • The app starts inside an isolated environment (the container).

      • The app behaves exactly as it did on your system since the environment is the same.

    2. The App is Accessible

      • By exposing port 8080 (as defined in your Dockerfile), the app becomes available on http://localhost:8080.

      • Your friend can open this URL in their browser and use the app.


Basic Docker commands:

CommandDescription
docker run <image_name>Run a container from the specified image
docker psList all running containers
docker ps -aList all containers (running and stopped)
docker stop <container_name>Stop a running container
docker rm <container>Remove a stopped container
docker imagesList all images available locally
docker rmi <image>Remove an image from the local system
docker pull <image>Download an image from a registry (like Docker Hub)
docker run -d <image>Run the container in detached mode (in the background)
docker run -it <image>Run the container in interactive mode (use terminal inside)
docker exec <container_id>Execute a command inside a running container

Examples :

  1. docker pull ubuntu:

    Downloads the latest Ubuntu image from Docker Hub to your local system.

  2. docker run ubuntu:

    Creates and runs a container from the Ubuntu image interactively.

  3. docker ps:

    Lists all currently running containers.

  4. docker ps -a:

    Lists all containers, including stopped ones.

  5. docker stop ubuntu:

    Stops a running container named or with the ID ubuntu (use container name/ID, not image name).

  6. docker images:

    Displays all downloaded Docker images on your system.

  7. docker rm ecstatic_mendeleev:

    Removes the stopped container named ecstatic_mendeleev.

  8. docker rmi ubuntu:

    Deletes the Ubuntu image from your local system.

  9. docker run -d ubuntu:

    Runs an Ubuntu container in detached mode (in the background).

  10. docker run -it ubuntu:

    Runs an Ubuntu container interactively with a terminal session.

  11. docker exec 929a9d568f88 cat /etc/*release*:

    Executes the cat /etc/*release* command in the running container with ID 929a9d568f88 to display its OS release information.


Run - tags

  1. docker pull redis:4.0

    Runs a container using a specific version of Redis image for instance version 4.0.

  2. docker run -i

    Runs a container in interactive mode, keeping STDIN open for input (commonly used with -t).

  3. docker run -it

    Runs a container interactively with a terminal session (combines -i and -t for interactive input and terminal emulation).


Port Mapping:

Port mapping in Docker connects a port on your host machine (your computer) to a port inside a running Docker container. It allows you to access applications running inside the container from outside the container.

Why it is Done:

Containers are isolated from your computer’s network by default. Port mapping makes services (e.g., web servers or databases) in a container accessible to the host machine or external users.

Imagine you a developer working on a new web app. You’ve decided to use NGINX as a web server, running it in a Docker container. Here’s how port mapping helps:

  1. You create a container using the nginx image. By default, the web server listens on port 80 inside the container.

  2. But when you try to access http://localhost on your browser, nothing happens. Why? Because the container’s internal port 80 isn’t connected to your computer’s network.

  3. You realize you need to map a port on your computer to the container’s port 80.

So, you run this command:

docker run -d -p 8080:80 nginx
  • 8080 is the host port (your computer).

  • 80 is the container port.

  • Docker creates a connection between these two ports.

Now, when you open http://localhost:8080 in your browser, it connects to port 8080 on your computer, which forwards the request to port 80 in the container. Voilà! You see the NGINX welcome page.


RUN - Volume Mapping:

Volume mapping is a way to link a directory on your computer (host) to a directory inside a Docker container. This ensures that data persists even if the container is stopped or deleted, overcoming the temporary nature of container file systems.

You are working on a blog application and decide to use MySQL to store user data. You don’t want to install MySQL on your computer, so you run a MySQL Docker container:

docker run --name mysql -e MYSQL_ROOT_PASSWORD=root -d mysql

Everything works fine. You connect the application to the MySQL database, create a table, and starts adding data.

The Problem: Data Loss

Later, you run:

docker stop mysql
docker rm mysql

When you restart the container:

docker run --name mysql -e MYSQL_ROOT_PASSWORD=root -d mysql

You check for the data and discover it's all gone! Why? Because:

  1. Containers have an independent file system.

  2. When the container was removed, so was all the data stored inside it.

You realize you need a way to persist the data, so it stays even if the container is deleted.

The Solution: Volume Mapping

To prevent this problem, you use volume mapping. You link a directory on your computer (/opt/mydir) to the directory where MySQL stores its data inside the container (/var/lib/mysql):

docker run -v /opt/mydir:/var/lib/mysql mysql

Here’s what happens:

  • /opt/mydir is the host directory where data will be stored.

  • /var/lib/mysql is the container directory where MySQL writes its data.

  • Docker ensures any changes in /var/lib/mysql are synchronized with /opt/mydir.

Now, even if you stop and remove the container, the data remains in /opt/mydir. When you start a new container, the new container finds the same data because it’s mapped to /opt/mydir.


Inspect Container:

  1. docker inspect <container_name/container_id>

    Provides detailed information about containers, images, volumes, and networks in Docker. It returns information in JSON format about the specified object, such as configuration details, networking settings, environment variables, and more.


Container Logs

  1. docker logs<container_name/container_id>

    Container logs refer to the output generated by a containerized application running inside a Docker container. These logs provide information about the container’s activities, errors, and the status of processes inside the container. Container logs are crucial for debugging, monitoring, and understanding the behavior of applications in production.


How to write a Dockerfile?

We will understand it with the help of a tutorial. In this tutorial, we’ll walk through the steps to containerize a simple Go application using Docker. The Go application will be a basic web server that serves "Hello, World!" when accessed via a browser. We'll cover everything from writing the Go code to building and running it inside a Docker container.

Prerequisites

Before we start, make sure you have the following tools installed on your machine:

  • Go (Golang): We’ll be using Go 1.23.1 for this tutorial. You can download it from golang.org.

  • Docker: You’ll need Docker installed on your machine to create containers. You can download Docker Desktop from docker.com.


Step 1: Write a Simple Go Application

Let's start by creating a basic Go application that starts an HTTP server and displays a "Hello, World!" message.

1.1 Create the Project Directory

Open a terminal and create a directory for your Go application:

mkdir GoDockerized
cd GoDockerized

1.2 Write the Go Code (main.go)

Inside the GoDockerized directory, create a file named main.go and add the following Go code:

// main.go
package main

import (
    "fmt"
    "net/http"
)

// handler function will respond with "Hello, World!"
func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, World!")
}

func main() {
    // Register the handler for the root URL "/"
    http.HandleFunc("/", handler)

    // Print a message indicating the server is starting
    fmt.Println("Server starting on port 8080...")

    // Start the server on port 8080
    http.ListenAndServe(":8080", nil)
}

Step 2: Create a Dockerfile

Now that we have our Go application, we need to create a Dockerfile to containerize it. A Dockerfile is a set of instructions that Docker uses to build a container image.

2.1 Write the Dockerfile

In the same GoDockerized directory, create a new file named Dockerfile (without any extension). The contents of the Dockerfile are as follows:

# Step 1: Use the official Go image for Go 1.23 based on Alpine
FROM golang:1.23-alpine

# Step 2: Set the working directory inside the container
WORKDIR /app

# Step 3: Copy the Go source code into the container
COPY . .

# Step 4: Build the Go application
RUN go build -o main .

# Step 5: Expose the port that the app will run on
EXPOSE 8080

# Step 6: Define the default command to run the app
CMD ["./main"]

Explanation of the Dockerfile:

  1. FROM golang:1.23-alpine:

    • We start with the official Go 1.23 image, based on Alpine Linux. Alpine is a lightweight Linux distribution that minimizes the size of our Docker image.

    • The Go 1.23 version ensures we are using the latest Go release for our application.

  2. WORKDIR /app:

    • This sets the working directory inside the Docker container to /app. All subsequent commands (like COPY, RUN, CMD) will execute inside this directory.
  3. COPY . .:

    • This copies the contents of the current directory on your host machine (your Go app) into the /app directory inside the container.
  4. RUN go build -o main .:

    • This command builds the Go application inside the container. The -o main flag tells Go to output the binary file as main, which will be the executable we run inside the container.
  5. EXPOSE 8080:

    • This tells Docker that the container will use port 8080 for communication. This is the port where our Go application will listen for HTTP requests.
  6. CMD ["./main"]:

    • This defines the default command that runs when the container starts. It tells Docker to execute the mainbinary that was built earlier, starting the Go application.

Step 3: Build the Docker Image

Now that we have both the Go application (main.go) and the Dockerfile, we can build the Docker image.

3.1 Build the Image

In the terminal, run the following command in the GoDockerized directory to build the Docker image:

docker build -t godockerized .

Here’s a breakdown of the command:

  • docker build: This command is used to build a Docker image from the Dockerfile.

  • -t godockerized: The -t flag tags the image with a name (godockerized), which will be useful for running or referencing this image later.

  • .: The dot indicates the current directory, meaning Docker should use the Dockerfile and other files in this directory to build the image.

Once the image build process is complete, Docker will output the image layers and their respective build statuses. The build should take a few seconds to a minute, depending on your system.

Step 4: Run the Docker Container

Now that we’ve built the Docker image, we can run it in a container.

4.1 Run the Container

To run the Go application inside a container, use the following command:

docker run -p 8080:8080 godockerized

This command does a few things:

  • docker run: Tells Docker to run a container from the specified image.

  • -p 8080:8080: Maps port 8080 on your local machine to port 8080 inside the Docker container. This allows you to access the Go web server through your local machine’s browser.

  • godockerized: This is the name of the image we created earlier.

After running the command, Docker will start a container based on the godockerized image, and you should see output similar to this in your terminal:

Server starting on port 8080...

his confirms that the Go application is running and listening for HTTP requests on port 8080 inside the container.

4.2 Verify the Application is Running

Now, open a web browser and visit http://localhost:8080. You should see the message:

Hello, World!

This confirms that your Go application is running successfully inside the Docker container, and you’re able to access it via the browser.


Pushing the image to Docker Hub:

To push the godockerized image to Docker Hub, you'll need to follow a few steps. This involves logging into Docker Hub, tagging the image correctly, and then pushing it to your Docker Hub repository.

Step 1: Log in to Docker Hub from the Command Line

Before you can push your image to Docker Hub, you need to log in using your Docker Hub credentials.

Run this command in your terminal:

docker login

You will be prompted to enter your Docker Hub username and password. After successfully logging in, you’ll be able to push images to your Docker Hub account.

Step 2: Tag the Docker Image for Docker Hub

Docker images should be tagged with the Docker Hub username (or organization name) and the repository name.

For example, if your Docker Hub username is yourusername, you need to tag the godockerized image as follows:

docker tag godockerized yourusername/godockerized:latest

In this command:

  • godockerized is the name of the local image you built.

  • yourusername/godockerized is the new tag with your Docker Hub username and the repository name you want to create (e.g., godockerized).

  • latest is the tag for the image version (you can use other tags like v1, v2, etc., if you prefer).

Step 3: Push the Image to Docker Hub

Now, you can push the tagged image to Docker Hub:

docker push yourusername/godockerized:latest

Pulling the Image from Docker Hub

To verify that the image was successfully pushed, you can try pulling it from Docker Hub to any machine that has Docker installed.

Run the following command:

docker pull yourusername/godockerized:latest

If you want to directly pull the image that I have created, run the following command:

docker pull nsahil992/godockerized:latest

How Applications are Containerized in Docker:

1. Docker CLI (Command Line Interface)

  • What it does: The user interacts with Docker via the CLI, using commands like docker build or docker run.

  • Action:

    • For example, when you run docker run, the CLI sends this command to the Docker Daemon.

2. Docker Daemon

  • What it does: The Daemon is the core engine of Docker. It listens to commands from the CLI or REST API and manages everything, including:

    • Building images.

    • Running containers.

    • Managing networks, storage, and other resources.

  • Communication:

    • The Daemon receives instructions from the CLI or REST API and executes them.

    • For example, if you ask to run a container, the Daemon will fetch the image, create the container, and allocate resources.


3. REST API

  • What it does: Allows external programs to interact with the Docker Daemon programmatically.

  • Action:

    • Tools like Docker Compose or Kubernetes use this API to talk to the Docker Daemon.

    • Instead of using the CLI, they send HTTP requests to the Daemon.


4. Container Creation Process

When you run docker run (or an equivalent API call), the following steps occur:

  1. Check for the Image:

    • The Daemon checks if the requested image exists locally.

    • If not, it downloads it from a registry like Docker Hub.

  2. Set Up Isolation (Namespaces):

    • Docker uses namespaces to isolate the container’s processes, network, and file system from the host.

    • For example:

      • A new PID namespace ensures container processes don’t see or interfere with host processes.

      • A new network namespace gives the container its own virtual network.

  3. Allocate Resources (Cgroups):

    • The Daemon uses cgroups to manage how much CPU, memory, or other resources the container can use.
  4. File System Setup (Union File System):

    • Docker sets up a layered file system:

      • The base layers are read-only (OS, libraries, etc.).

      • A writable layer is added on top for runtime changes.

  5. Start the Container:

    • The Daemon starts the container and runs the specified command (e.g., python app.py).

    • The container now operates independently, with its own isolated environment.


Resources:

Docker course [Kodekloud] : https://learn.kodekloud.com/user/courses/docker-training-course-for-the-absolute-beginner

GitHub small project : https://github.com/nsahil992/GoDockerized