Containers & DevOps Read time: 9 min Updated: October 2026

Practical Docker Guide: Container Fundamentals, Networking & Dockerfile Best Practices

In modern software engineering, one of the most frustrating challenges is the classic "it works on my machine" dilemma. A service functions perfectly in a developer's local development environment, but crashes on staging or production due to differing shared libraries, compiler versions, or operating system quirks. Docker reduces environment differences by isolating applications and their complete runtime dependencies inside lightweight, standardized units known as containers.[1]

This hands-on technical tutorial walks through container mechanics, compares containers with traditional virtual machines, covers everyday container management commands, demonstrates how to write production-grade Dockerfiles, and explains persistent data management with volumes.[1][2]

1. Containers vs. Virtual Machines

While both containers and virtual machines provide workload isolation, their operational architectures are fundamentally different:

  • Virtual Machines (VMs): Each virtual machine includes a complete guest operating system, virtual device drivers, and dedicated memory allocations managed by a hypervisor (such as KVM, ESXi, or Hyper-V). A separate guest kernel adds overhead; boot time and memory use depend on the image and workload.
  • Docker Containers: Containers run directly as isolated processes on the host Linux kernel using native kernel features such as namespaces (for process, mount, and network isolation) and cgroups (for CPU and memory boundaries). Containers share the host kernel. Startup time and memory use depend on the application; large containers can still consume substantial resources.[1]

2. Core Docker Concepts

Mastering Docker requires understanding four foundational concepts described in Docker's architecture documentation:[1]

  • Docker Image: An immutable, read-only template that packages your application binary, runtime, environment variables, and configuration files. Images are built in layers.
  • Docker Container: A runnable, isolated instance of an image. A thin read-write layer is placed on top of the underlying immutable image layers.
  • Docker Daemon (dockerd): The background service managing images, containers, networks, and storage volumes on the host system.
  • Docker Registry: A centralized repository for storing and distributing images, such as Docker Hub or private cloud registries.

3. Essential CLI Commands and First Container Run

Verify that Docker Engine is running on your machine by checking the installed version:[1]

docker --version

Execute your first test container by running the official test image from Docker Hub:

docker run hello-world

When you execute this command, Docker checks whether the image exists locally. If not, it downloads the image from Docker Hub, initializes the container process, streams the confirmation output to your terminal, and terminates cleanly.

4. Running a Web Server with Port Forwarding

Now let us run a real web server using the official Nginx image in detached mode with port publishing:[1]

docker run -d -p 8080:80 --name my-webserver nginx:alpine

Understanding the flags in this command:

  • -d (Detached): Runs the container process in the background, freeing your terminal prompt.
  • -p 8080:80 (Port Publishing): Maps host port 8080 to container internal port 80. Navigating to http://localhost:8080 connects directly to the Nginx instance.
  • --name my-webserver: Assigns a memorable, human-readable name for inspection and lifecycle operations.
  • nginx:alpine: Specifies the image tag, using the minimal Alpine Linux base distribution to minimize image size.
Port Mapping Syntax: The syntax is always host_port:container_port. If you specify 3000:80, external traffic hitting port 3000 on the host reaches port 80 inside the container.

5. Inspecting, Stopping, and Removing Containers

Inspect running containers using the docker ps command:

docker ps

To view all containers, including stopped ones, pass the -a flag:

docker ps -a

View live stdout/stderr log output from a container process:

docker logs --tail 50 -f my-webserver

When finished, gracefully terminate the container process and remove its filesystem layer:

docker stop my-webserver
docker rm my-webserver

6. Writing a Production-Ready Dockerfile

A Dockerfile contains the instructions Docker uses to build custom container images. Here is a production-optimized example for a Node.js application utilizing layer caching and non-root execution:[2]

# Stage 1: Build image from minimal official base
FROM node:24-alpine AS base

# Set working directory inside container
WORKDIR /usr/src/app

# Copy dependency manifests first to leverage Docker layer caching
COPY package*.json ./

# Install production dependencies only
RUN npm ci --omit=dev

# Copy application source code
COPY . .

# Avoid running as the root superuser inside the container
USER node

# Document the exposed port
EXPOSE 3000

# Specify execution command
CMD ["node", "server.js"]

Build your custom image using docker build:[2]

# The period indicates build context is the current working directory
docker build -t my-node-service:1.0.0 .

7. Managing Persistent Data with Volumes

Containers are ephemeral by default: when a container is removed, all filesystem modifications made inside its writable layer are permanently erased. For databases and file uploads, persistent storage is required. Docker provides named volumes to decouple persistent data from the container lifecycle:[1]

# Create an isolated Docker volume managed by the daemon
docker volume create db_storage

# Mount the volume into a database container
docker run -d \
  --name app-database \
  -v db_storage:/var/lib/postgresql/data \
  -e POSTGRES_PASSWORD=secret \
  postgres:16-alpine

If the app-database container is stopped, upgraded, or destroyed, the data stored in db_storage remains intact on the host disk and can be immediately reattached to a new container.

Summary of Core Practices

  1. Favor lightweight base images such as Alpine or slim Debian variants.[1]
  2. Copy dependency specifications before source code to optimize build caching.[2]
  3. Never run container workloads as root in production environments.
  4. Always decouple stateful data into managed Docker volumes.

Primary Sources & Documentation