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) │
└──────────────┘
- docker CLI — Where you type commands, e.g.,
docker run - dockerd (daemon) — Background service responsible for managing images, containers, networks, and storage
- containerd — The low-level runtime that actually creates and runs containers
- Registry — Image repository; Docker Hub is the default public registry, but you can also set up a private one
Note: On Windows, Docker actually runs inside a lightweight Linux virtual machine (WSL 2 or Hyper-V), so while the
dockercommand 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:alpineis recommended — it's only 1/3 the size of the full version and has a smaller attack surface. Your demo usednginx: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 onlatest. Thelatesttag 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:
- Reuse: Multiple images can share the same base layers, saving disk space
- Incremental builds: After modifying a Dockerfile, only the changed layers need to be rebuilt
- Fast distribution: When pulling an image, only the missing layers need to be downloaded
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 runcommand; they will remain in your shell history. Use--env-fileto load from a.envfile, and ensure.envis 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:
- The path must be correct: The default Nginx config path inside the container is
/etc/nginx/nginx.conf, not/usr/etc/.... Writing the wrong path mounts to the wrong location, Nginx still uses the default config, so you saw the Welcome page. - Windows paths: Absolute paths on Windows need backslashes or forward slashes in PowerShell; Docker requires forward slashes:
C:/Users/liaoh/Desktop/...
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:
- Nginx runs inside a Docker container
- Node.js service runs on the host (Windows), listening on port 1314
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 use172.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+CMDcombination is best practice:ENTRYPOINTfixes the main program,CMDprovides 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, includingnode_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
- Image —
nginx:latest, the application template, layered storage, version management via tags - Container —
my-nginx-demo, a running instance of an image, stateless, destroyed upon deletion - Port Mapping —
-p 80:80, connecting host and container networks - Mount Volumes —
-v nginx.conf:/etc/nginx/nginx.conf, injecting config files; distinguish Bind Mount / Volume / tmpfs - Environment Variables —
-e/--env-file, passing runtime configuration to containers - Container Networking —
host.docker.internal, container accessing the host; bridge / host / none three modes - Dockerfile — Packaging your own application into an image, CMD vs ENTRYPOINT
- .dockerignore — Excluding irrelevant files, reducing image size
- Multi-stage Builds — Separating build and runtime environments, further slimming images
- 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.