跪拜 Guibai
← Back to the summary

Docker from Zero: What Every Flag in `docker run` Actually Does

1. Introduction

Suppose you developed a Node.js service (index.js) on Windows, listening on port 1314 and returning "hello world". You also wrote an Nginx reverse proxy (nginx.conf) to forward requests from port 80 to port 1314.

The question is: How do you get Nginx running in an independent, portable way that doesn't pollute your local environment?

This is exactly the problem Docker solves. The command you execute is the core operation in the Docker world:

docker run --name my-nginx-demo -p 80:80 -v C:\...\nginx.conf:/etc/nginx/nginx.conf -d nginx

Let's use this command as a clue to break down Docker's basic concepts one by one.


2. What is Docker

Docker is a containerization platform that packages an application and all its dependencies into a standardized unit, which can run consistently on any machine with Docker installed.

Docker ≈ a lightweight virtual machine, but it shares the host operating system kernel, starts in seconds, and has extremely low resource consumption. A container runs only one process, not an entire operating system.

Docker's core value lies in three words: Package, Distribute, Run. You package an application into an image, distribute it to any machine, run it with the same command, and the result is completely consistent.

Docker Overall Architecture

Docker uses a Client-Server architecture:

┌──────────┐     REST API      ┌────────────┐      ┌──────────────┐
│  docker   │ ───────────────→ │  dockerd    │ ←──→ │  containerd  │
│  (CLI)    │                  │  (Daemon)   │      │  (Runtime)    │
└──────────┘                   └────────────┘      └──────────────┘
                                     │
                                     │ pull/push
                                     ▼
                              ┌──────────────┐
                              │  Registry     │
                              │  (Docker Hub) │
                              └──────────────┘

Note: On Windows, Docker actually runs inside a lightweight Linux virtual machine (WSL 2 or Hyper-V), so while the docker command appears to execute on Windows, the daemon runs inside Linux.

Docker vs Virtual Machines

Dimension Docker Container Virtual Machine
Startup Speed Seconds Minutes
Resource Usage MB level, shares host kernel GB level, each VM has its own OS
Isolation Process-level isolation, shared kernel Full isolation, independent kernel
Image Size Typically tens to hundreds of MB Typically several GB
Portability Any Linux host Limited by Hypervisor
Use Cases Microservices, CI/CD, development environments Strong multi-tenant isolation, running different OSes

One-sentence distinction: Virtual machines virtualize hardware; containers virtualize the operating system. Need to run Linux and Windows simultaneously on the same machine? Use a VM. Need to package a Node service into a unit that runs anywhere? Use a container.


3. Image

An image is a "template" or "snapshot" for a container, containing everything needed to run an application: code, runtime, system libraries, environment variables, configuration files. Images are read-only and cannot be modified.

In your command, nginx is an image name:

docker run ... nginx

Docker first checks if the nginx:latest image exists locally; if not, it pulls it from Docker Hub. The output you saw earlier from docker images is the list of images already on your machine:

IMAGE ID DISK USAGE Meaning
nginx:latest 8541484afbc9 241MB Full Nginx, based on Debian
nginx:alpine 4a73073bd557 93.6MB Slim Nginx, based on Alpine Linux
hello-world:latest 5dd0d3e6e255 25.9kB Official Docker test image, only 9.49kB of content

Tip: For production, nginx:alpine is recommended — it's only 1/3 the size of the full version and has a smaller attack surface. Your demo used nginx:latest (241MB); switching to alpine saves 150MB.

Image Tags and Version Management

The :latest and :alpine after the image name are tags, used to distinguish different versions or variants of the same image. If you don't specify a tag, Docker defaults to pulling :latest.

docker pull nginx                # Equivalent to nginx:latest
docker pull nginx:alpine         # Specify the alpine version
docker pull nginx:1.25.3         # Specify an exact version number
docker pull node:18-alpine       # Node 18 alpine version

In production, always specify an exact version number (e.g., nginx:1.25.3); don't rely on latest. The latest tag changes as new versions are released, which can cause your application to suddenly behave inconsistently.

Image Layering

An image is not a single large file but is built from multiple layers. Each layer represents a filesystem change (e.g., installing a package, copying a file). The benefits of this design are:


4. Container

A container is a running instance of an image. You can think of an image as a "Class" and a container as an "Instance". One image can start multiple containers, each isolated from the others.

In your command:

docker run --name my-nginx-demo ... nginx

--name my-nginx-demo gives the container a name for easier management:

Command Purpose
docker ps View running containers
docker ps -a View all containers (including stopped ones)
docker stop my-nginx-demo Stop a container
docker start my-nginx-demo Start a stopped container
docker rm my-nginx-demo Delete a container
docker rm -f my-nginx-demo Force delete (whether running or not)
docker logs my-nginx-demo View container logs
docker exec -it my-nginx-demo sh Enter the container to execute commands

Note: Containers are stateless — deleting a container destroys all data inside it. If data needs to persist, use mount volumes (see Section 7).


5. Complete Breakdown of the docker run Command

Returning to your command, let's analyze it piece by piece:

docker run --name my-nginx-demo -p 80:80 -v C:\...\nginx.conf:/etc/nginx/nginx.conf -d nginx
Parameter Meaning
docker run Create and start a new container
--name my-nginx-demo Name the container my-nginx-demo
-p 80:80 Port mapping: Host 80 → Container 80
-v C:\...\nginx.conf:/etc/nginx/nginx.conf Mount volume: Inject local config file into the container
-d Run in background (detached mode)
nginx The image name to use

Now let's focus on the most critical parameters: -p, -v, and -e.

Supplement: Environment Variables (-e)

Containers often need environment variables to pass runtime parameters, such as database connection addresses, API keys, etc.:

# Single environment variable
docker run -e NODE_ENV=production nginx

# Multiple environment variables
docker run -e MYSQL_ROOT_PASSWORD=123456 -e MYSQL_DATABASE=myapp mysql:8

# Load environment variables from a file (recommended, avoids passwords in shell history)
docker run --env-file .env my-app
Parameter Meaning
-e KEY=VALUE Set a single environment variable
--env-file .env Load all environment variables from a file

Security reminder: Don't write sensitive information like passwords directly in the docker run command; they will remain in your shell history. Use --env-file to load from a .env file, and ensure .env is added to .gitignore.


6. Port Mapping (-p)

Containers have their own independent network stack. Inside the container, Nginx listens on port 80, but that's port 80 inside the container, which the host cannot access by default. You need -p to "expose" the container port to the host.

-p host_port:container_port
-p 80:80    ← Accessing localhost:80 equals accessing port 80 inside the container

This is why visiting http://localhost shows the Nginx page — traffic to host port 80 is forwarded by Docker to port 80 inside the container.

Common port mapping formats:

Syntax Meaning
-p 8080:80 Host 8080 → Container 80
-p 80:80 Host 80 → Container 80 (ports can be the same)
-p 127.0.0.1:80:80 Bind only to the loopback address, inaccessible externally
-p 80:80/udp Specify UDP protocol (default is TCP)

7. Mount Volumes (-v / --volume)

Mount volumes allow you to "map" files or directories from the host into the container. Modifications inside the container sync to the host, and vice versa.

-v host_path:container_path
-v C:\...\nginx.conf:/etc/nginx/nginx.conf

In your scenario, the purpose of this mount is:

Replace the default Nginx configuration inside the container with your local nginx.conf. This way, modifying the local file and recreating the container takes effect without needing to enter the container to edit.

The pitfalls you encountered are also here:

Common uses for mount volumes:

Usage Scenario
Mount config files Like your demo, injecting nginx.conf
Mount code directories Sync code in real-time during development; changes take effect immediately
Mount data directories Persist database data files to the host; deleting the container doesn't lose data

Three Types of Data Volumes

Docker provides three persistence methods; the -v you used belongs to the first type:

Type Syntax Storage Location Use Case
Bind Mount -v /host/path:/container/path Any path on the host Development environments, injecting config files
Volume -v volume_name:/container/path Docker-managed /var/lib/docker/volumes/ Production environments, database persistence
tmpfs --tmpfs /container/path Host memory Temporary sensitive data, caches

Bind Mount (the type you used):

-v C:\...\nginx.conf:/etc/nginx/nginx.conf

Pros: Direct access to host files, convenient for development and debugging. Cons: Path depends on the host; may be incompatible when switching machines.

Volume (Docker-managed):

docker volume create my-data
docker run -v my-data:/var/lib/mysql mysql:8

Pros: Unified management by Docker, cross-platform consistency, easy backup and restore. Cons: Cannot browse directly like regular files.

Use Volume for production, Bind Mount for development.


8. Container Networking: host.docker.internal

Your nginx.conf contains this line:

proxy_pass http://host.docker.internal:1314;

This involves a key issue in Docker networking: Writing localhost inside a container refers to the container itself, not the host.

In your architecture:

If proxy_pass were written as http://localhost:1314, the container would look for port 1314 inside itself, but there's no Node service inside the container, so the request would fail.

Solution: Docker Desktop (Windows / macOS) provides a special hostname host.docker.internal, through which containers can directly access the host. Linux doesn't have this built-in hostname; you need to add it manually with --add-host, or use 172.17.0.1 (the default bridge gateway).

Docker provides three default network modes:

Network Mode Characteristics Use Case
bridge (default) Container has an independent IP, accesses the external network via host NAT Most scenarios
host Container directly uses the host network stack, no isolation High-performance needs, not supported on Windows/Mac
none Container has no network Pure computation tasks

9. Dockerfile: Building Your Own Image

So far, you've used the ready-made nginx image. But what about your Node.js service? You can use a Dockerfile to package your own application into an image, place it on the same Docker network as Nginx, and then you won't need host.docker.internal.

A Dockerfile for your index.js:

# Based on the official Node.js image
FROM node:18-alpine

# Set the working directory inside the container
WORKDIR /app

# Copy package.json and install dependencies
COPY package.json .
RUN npm install

# Copy source code
COPY index.js .

# Expose the port (documentation purpose; actual mapping still requires -p)
EXPOSE 1314

# Startup command
CMD ["node", "index.js"]

Core instruction explanations:

Instruction Purpose
FROM Specifies the base image; every Dockerfile must start with FROM
WORKDIR Sets the working directory; subsequent instructions execute in this directory
COPY Copies host files into the image
RUN Executes commands during image build (e.g., installing dependencies)
EXPOSE Declares the port the container listens on (documentation; actual mapping still needs -p)
CMD The default command executed when the container starts

CMD vs ENTRYPOINT

These are the two most easily confused instructions in a Dockerfile:

Instruction Behavior Can be overridden by docker run?
CMD Defines the default command and parameters Can be completely overridden
ENTRYPOINT Defines the container's main process entry point Not overridden; parameters are appended
# CMD mode: docker run my-app echo hello → outputs hello
CMD ["node", "index.js"]

# ENTRYPOINT mode: docker run my-app index2.js → equivalent to node index2.js
ENTRYPOINT ["node"]
CMD ["index.js"]

The ENTRYPOINT + CMD combination is best practice: ENTRYPOINT fixes the main program, CMD provides default parameters, allowing flexible parameter substitution at runtime.

Building and running:

# Build the image
docker build -t my-node-app .

# Run the container
docker run --name my-node-demo -p 1314:1314 -d my-node-app

.dockerignore

Similar to .gitignore, .dockerignore tells Docker which files to ignore during the build, preventing node_modules, logs, .env, and other irrelevant files from being packed into the image:

# .dockerignore
node_modules
.git
.env
*.log
dist

Without .dockerignore, COPY . . copies the entire project directory into the image, including node_modules (which may be incompatible with the system inside the container) and sensitive files, causing the image size to balloon or even fail to build.

Multi-stage Build

The Dockerfile above has a problem: node_modules contains build tools and development dependencies that aren't needed in production, yet they take up hundreds of MB. Multi-stage builds solve this:

# ===== Stage 1: Build =====
FROM node:18-alpine AS builder
WORKDIR /app
COPY package.json .
RUN npm install --production   # Only install production dependencies
COPY index.js .

# ===== Stage 2: Run =====
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app .     # Only copy artifacts from the build stage
EXPOSE 1314
CMD ["node", "index.js"]

This way, the final image contains only runtime code and production dependencies, making it much smaller than a single-stage build. For large projects (Go, Java, frontend), multi-stage builds are practically standard.

Advanced: Docker Compose

When you have multiple containers that need to work together (e.g., Nginx + Node), starting them one by one with docker run is cumbersome. Use Docker Compose to orchestrate all services at once:

# docker-compose.yml
version: "3.8"
services:
  node:
    build: .
    ports:
      - "1314:1314"

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
    depends_on:
      - node

Then start everything with a single command:

docker compose up -d

Note that now proxy_pass can directly use the service name node:1314, because Compose automatically creates an internal network where containers can access each other by service name.


10. Common Command Quick Reference

Command Purpose
docker images List local images
docker pull nginx:alpine Pull an image
docker build -t name . Build an image
docker rmi nginx:latest Delete an image
docker ps View running containers
docker ps -a View all containers
docker run ... Create and start a container
docker stop my-nginx-demo Stop a container
docker start my-nginx-demo Start an existing container
docker restart my-nginx-demo Restart a container
docker rm -f my-nginx-demo Force delete a container
docker logs my-nginx-demo View logs
docker exec -it my-nginx-demo sh Enter a container
docker compose up -d Compose: start all services
docker compose down Compose: stop and delete all services

11. Summary

  1. Imagenginx:latest, the application template, layered storage, version management via tags
  2. Containermy-nginx-demo, a running instance of an image, stateless, destroyed upon deletion
  3. Port Mapping-p 80:80, connecting host and container networks
  4. Mount Volumes-v nginx.conf:/etc/nginx/nginx.conf, injecting config files; distinguish Bind Mount / Volume / tmpfs
  5. Environment Variables-e / --env-file, passing runtime configuration to containers
  6. Container Networkinghost.docker.internal, container accessing the host; bridge / host / none three modes
  7. Dockerfile — Packaging your own application into an image, CMD vs ENTRYPOINT
  8. .dockerignore — Excluding irrelevant files, reducing image size
  9. Multi-stage Builds — Separating build and runtime environments, further slimming images
  10. Docker Compose — Orchestrating multiple containers to work together, starting all services with one command

One-sentence summary: Docker lets you package your application and its environment together, running consistently on any machine. Your Node service + Nginx reverse proxy is just two Docker commands, and the result is exactly the same on another computer.