Understanding Docker Images, Containers, and Networking

Understanding Docker Images, Containers, and Networking

Introduction

As a software developer, you understand the importance of managing infrastructure efficiently and building robust, scalable systems. In Docker, two key concepts, images and containers, serve as the building blocks of containerized applications. Mastering them and understanding how Docker networking works will allow you to build, test, and deploy multi-service architectures seamlessly.

This blog will take you through the life cycle of Docker images, container management, and networking. We will cover how Docker builds images, handles container instances, and how networking is handled across these containers in various scenarios.


Docker Images

A Docker image is a lightweight, standalone, and executable package that includes everything needed to run a piece of software: code, runtime, libraries, environment variables, and configuration files. Docker images are created from Dockerfiles, and each change to an image creates a new layer, allowing for efficient storage and transfer.

Image Structure and Layers

Docker images are composed of multiple layers. Each layer represents a set of file changes or instructions in the Dockerfile. The layered approach has a few important benefits:

  • Efficiency: Docker reuses layers when building new images, reducing the build time and storage requirements.

  • Portability: The image can be shared and run on any platform that supports Docker.

For example, an image with a base layer of Ubuntu can have several additional layers like:

  1. Adding the necessary packages (e.g., Python).

  2. Copying source code into the container.

  3. Running additional commands like installing dependencies.

When Docker builds an image, it caches layers to speed up future builds by avoiding redundant steps. Each layer is immutable, meaning once it’s created, it can’t be modified. New changes are added as new layers on top of the previous ones.

Listing and Managing Images

To list all images on your system:

docker images

Output:

REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
nginx               latest              abc123              2 weeks ago         133MB
node                14-alpine           def456              3 days ago          85MB

You can also use docker inspect to get detailed information about an image:

docker inspect nginx

Building Your Own Images

Developers often create custom images using Dockerfiles. A Dockerfile is a script that defines the steps required to build a Docker image.

Example Dockerfile:

FROM node:14-alpine
WORKDIR /app
COPY . .
RUN npm install
EXPOSE 3000
CMD ["npm", "start"]

Key commands in the Dockerfile:

  • FROM: Specifies the base image.

  • WORKDIR: Sets the working directory inside the container.

  • COPY: Copies files from the local filesystem to the container.

  • RUN: Executes commands inside the container (e.g., installing dependencies).

  • CMD: Specifies the default command to run when a container starts.

To build an image from a Dockerfile:

docker build -t my-node-app .

This creates an image called my-node-app. The -t flag tags the image, and the . refers to the location of the Dockerfile (the current directory).

Optimizing Docker Images

One of the critical challenges in Docker is keeping images lightweight and efficient. Here are a few strategies:

  • Minimize the number of layers: Each instruction in a Dockerfile creates a new layer. Combine related instructions in a single RUN statement.

  • Use a smaller base image: For example, using node:14-alpine (a slim version of Node.js based on Alpine Linux) results in a much smaller image than node:14.

  • Leverage multi-stage builds: Multi-stage builds allow you to separate build dependencies from the final image, reducing the size of the production image.

Example:

# Build stage
FROM node:14-alpine AS build
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
RUN npm run build

# Production stage
FROM nginx:alpine
COPY --from=build /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Containers

A Docker container is an instance of a Docker image. Containers are designed to be ephemeral, meaning they can be created, modified, and destroyed easily.

Running and Managing Containers

To run a container from an image:

docker run -d -p 8080:80 nginx

This command creates and runs a detached NGINX container, mapping port 80 of the container to port 8080 on the host.

  • -d: Runs the container in detached mode (in the background).

  • -p: Maps the host’s port to the container’s port.

To list all running containers:

docker ps

To list all containers, including stopped ones:

docker ps -a

Example output:

CONTAINER ID  IMAGE  COMMAND                  CREATED       STATUS       PORTS                   NAMES
abc123        nginx  "/docker-entrypoint.…"   5 hours ago   Up 5 hours   0.0.0.0:8080->80/tcp    webserver

To stop and remove a container:

docker stop abc123
docker rm abc123

Container Lifecycle and Isolation

Containers are isolated from each other and the host system, providing security and stability. They have their own file system, network interfaces, and process trees, but they share the host kernel. This makes containers lightweight, with minimal overhead compared to traditional virtual machines.


Networking in Docker

Networking is a critical aspect of Docker, especially when working with multi-container applications. Docker provides several options for networking, each with different use cases depending on your needs.

1. Default Bridge Network

By default, Docker creates a bridge network when a container is started. This network allows communication between containers on the same Docker host, but containers cannot be accessed externally unless ports are explicitly exposed.

Example:

docker run -d --name app1 nginx
docker run -d --name app2 nginx

To inspect the network:

docker network inspect bridge

Containers can communicate with each other over the bridge network using their names. For example, inside app1, you could ping app2 by name:

ping app2

2. Custom Networks

You can create custom networks to provide better control over container communication. A custom bridge network allows containers to communicate by name, without needing to link them manually.

To create a custom network:

docker network create my-custom-network

To run containers within this network:

docker run -d --name app1 --network my-custom-network nginx
docker run -d --name app2 --network my-custom-network nginx

Now, containers app1 and app2 can communicate directly by name, and other containers that are not on the my-custom-network cannot access them.

3. Host Network

The host network allows a container to share the host’s network stack. In this mode, the container's ports are mapped directly to the host machine’s network interface, which can lead to better performance in some scenarios but sacrifices network isolation.

To run a container in the host network:

docker run --network host nginx

Use cases for the host network are rare, often involving high-performance requirements where minimal network overhead is critical.

4. Overlay Network

The overlay network is used for multi-host Docker deployments, such as Docker Swarm or Kubernetes. It allows containers on different Docker hosts to communicate securely.

To use the overlay network, you need to enable Docker Swarm:

docker swarm init
docker network create --driver overlay my-overlay-network

Now, containers on different Docker nodes can communicate via the overlay network.

5. Exposing Ports to the Host

To make a container’s service accessible from the host, you need to bind the container’s ports to the host machine. This is done using the -p option in docker run:

docker run -d -p 8080:80 nginx

Here, the container’s port 80 (NGINX’s default port) is mapped to port 8080 on the host machine, making NGINX accessible via http://localhost:8080.


Inspecting Networks and Containers

To inspect a container’s network settings:

docker inspect <container_id>

This provides a detailed JSON output, including IP addresses, port mappings, and network settings.

To inspect a Docker network:

docker network inspect <network_name>

You can see which containers are attached to a given network, along with their IP addresses and configurations.


Linking Containers

While custom networks are the preferred way to enable container communication, Docker previously used container links to allow direct interaction between containers. Although linking is now deprecated in favor of networks, it’s worth knowing how it worked.

Example of linking:

docker run -d --name app1 nginx
docker run -d --name app2 --link app1 nginx

app2 can now reference app1 by its name. However, the use of links is discouraged in favor of using Docker networks.


Conclusion

In this blog, we’ve explored Docker images and containers in depth, focusing on how they work under the hood and how they can be used effectively in development and production. We’ve also covered Docker’s networking model, explaining how containers communicate with each other and the outside world.

In upcoming blogs, we’ll dive into Docker Volumes and Persistent Storage and Orchestrating Multi-Container Applications with Docker Compose, enabling you to manage stateful applications and scale complex multi-service environments.

Did you find this article valuable?

Support Bit Fetch by becoming a sponsor. Any amount is appreciated!