← 返回知识宇宙
基础课程·入门

Docker 演示教程:一次打包,到处运行

面向初学者的 Docker 实操教程,10 分钟演示镜像与容器概念、Dockerfile 构建、Docker Compose 多服务编排、常用排错命令,并通过 Node.js + nginx + PostgreSQL 博客项目实战完成容器化部署。

知识编号22
关系轨道3
学习难度入门
Docker容器化镜像容器DockerfileDocker Compose部署DevOps命令行Node.js
抖音

视频讲解

Docker 演示教程:一次打包,到处运行

开场 Hook

Screen

黑底白字大字幕快速闪现:Mac、Windows、Linux 三台电脑屏幕依次出现"环境报错"的红字。镜头切到终端,敲下 docker run nginx,3 秒后浏览器里跑出网站。三台电脑同时显示成功页面——画面定格。

Narration

Docker 是全球开发者都在用的容器化工具。它解决了一个老问题:在你电脑上能跑,到别人电脑上就报错。开发、测试、部署,用同一套环境。Docker 不难,但概念容易混。今天十分钟,让你从零到能上手。咱们直接开干。


第一章 — Docker 到底是什么

本章引入

Screen

打开浏览器,访问 https://hub.docker.com,滚动到 nginx 镜像页面。回到终端,准备敲命令。

Narration

学 Docker,第一件事就是搞懂两个词:镜像和容器。我打个比方——镜像就是饼干模具,容器就是用模具扣出来的饼干。一个模具可以扣无数块饼干,一个镜像可以启动无数个容器。模具不能改,饼干用完就扔。这两件事搞明白,后面的命令都是顺水推舟。

Demo 1.1:拉取并运行一个镜像

Screen

打开 iTerm(macOS)/ Terminal(Linux)/ PowerShell(Windows)。输入 docker --version 确认安装。

Command

docker --version
docker pull nginx
docker images

Output

Docker version 24.0.7, build afdd53b
Using default tag: latest
latest: Pulling from library/nginx
a2abf6c4d29d: Pull complete
...
Status: Downloaded newer image for nginx:latest
REPOSITORY   TAG       IMAGE ID       CREATED       SIZE
nginx        latest    a2abf6c4d29d   2 weeks ago   187MB

Narration

先看一眼 Docker 装上没有。然后 docker pull nginx,从官方仓库拉一个 nginx 镜像下来。注意看进度条——一层一层下载,跟 Git 提交一层一层很像。docker images 看本地有什么镜像。这就是我们的"模具",还只是文件,没跑起来。

Demo 1.2:用镜像启动一个容器

Screen

终端输入启动命令,浏览器切到 http://localhost:8080。

Command

docker run -d --name my-web -p 8080:80 nginx
docker ps

Output

a1b2c3d4e5f6...   nginx:latest   "/docker-entrypoint.…"   2 seconds ago   Up 1 second   0.0.0.0:8080->80/tcp   my-web

CONTAINER ID   IMAGE     COMMAND                  STATUS         PORTS                  NAMES
a1b2c3d4e5f6   nginx     "/docker-entrypoint.…"   Up 3 seconds   0.0.0.0:8080->80/tcp   my-web

Narration

docker run 就是用模具扣饼干。-d 让它在后台跑,--name my-web 给它起个名字,-p 8080:80 把主机的 8080 端口映射到容器的 80 端口。docker ps 看哪些容器在跑。现在切到浏览器,访问 localhost:8080——nginx 欢迎页出来了。三条命令,一个网站跑起来,不需要装 nginx、不需要配环境,这就是 Docker。

Demo 1.3:停止并删除容器

Screen

终端操作,浏览器刷新页面看效果。

Command

docker stop my-web
docker ps -a
docker rm my-web
docker run -d --name my-web-2 -p 8080:80 nginx
curl -I http://localhost:8080

Output

my-web
CONTAINER ID   IMAGE     STATUS                     NAMES
a1b2c3d4e5f6   nginx     Exited (0) 3 seconds ago   my-web
my-web
a2b3c4d5e6f7...   nginx:latest   Up 1 second   0.0.0.0:8080->80/tcp   my-web-2

HTTP/1.1 200 OK
Server: nginx/1.25.3
...

Narration

容器是临时的。停掉、删掉,随时可以再扣一块。"容器坏了就删掉重建,不要修它"——这是黄金法则。修出来的东西没法复现,叫"雪花服务器"。

本章回顾

Key Takeaway

镜像 = 模具(只读模板),容器 = 用模具做出的产品(运行中的实例)。一个镜像可以启动无数个容器,容器删除重建只需一两秒。

Practical Use Case

任何需要"环境一致"的场景都用得上:开发环境、CI/CD、测试环境、生产部署。今天用的 nginx 镜像就是 Docker Hub 上的现成模具,5 亿+ 次下载——绝大多数中间件都有人帮你做好了。

Transition

镜像跑起来了,但怎么跑自己的应用?下一章,我们写 Dockerfile,构建自己的镜像。


第二章 — 写 Dockerfile,构建你的第一个镜像

本章引入

Screen

新建一个项目文件夹,打开 VSCode。

Narration

光跑别人的镜像没意思。我们要跑自己的应用。Dockerfile 就是菜谱,告诉 Docker 怎么一步步做出你需要的镜像。每条指令是一步操作,写好菜谱,Docker 负责烹饪。

Demo 2.1:创建项目结构

Screen

VSCode 新建文件夹 hello-docker,打开终端。

Command

mkdir hello-docker && cd hello-docker
touch index.js Dockerfile .dockerignore

Output

hello-docker/
├── .dockerignore
├── Dockerfile
└── index.js

Narration

项目根目录建三个文件:index.js 是应用代码,Dockerfile 是菜谱,.dockerignore 排除不需要的文件。结构简单,咱们一行一行写。

Demo 2.2:写应用代码

Screen

VSCode 打开 index.js,输入代码。

Command

const http = require("http");
const server = http.createServer((req, res) => {
  res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
  res.end("Hello Docker! 这是我的第一个容器化应用。\n");
});
server.listen(3000, () => {
  console.log("Server running on http://localhost:3000");
});

Output

(文件保存,无终端输出)

Narration

一个最简单的 Node.js HTTP 服务器,监听 3000 端口,返回一句话。咱们在本地不跑它——直接在 Docker 里跑。

Demo 2.3:写 Dockerfile

Screen

VSCode 打开 Dockerfile,逐行输入。

Command

FROM node:20-alpine
WORKDIR /app
COPY package.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["node", "index.js"]

Output

(文件保存,无终端输出)

Narration

每行都是一个步骤:FROM 选基础镜像——这里用 alpine 版本,只有 120MB,比默认镜像小 6 倍。WORKDIR 设工作目录。COPY package.json 先拷依赖清单,RUN npm install 装依赖——这一步很关键。COPY . . 最后再拷代码。这样改代码时,依赖这步可以复用缓存,构建从 5 秒搞定,不用每次都装一遍。EXPOSE 声明端口,CMD 启动命令。

Demo 2.4:写 .dockerignore

Screen

VSCode 打开 .dockerignore 文件。

Command

node_modules
.git
.env
*.log

Output

(文件保存,无终端输出)

Narration

.dockerignore 是给 Docker 的"黑名单"。node_modules 本机装的不算数,容器里要重装;.git、.env 不应该进镜像。这一步让构建更快、镜像更小、更安全。

Demo 2.5:补一个 package.json

Screen

VSCode 新建 package.json 文件。

Command

{
  "name": "hello-docker",
  "version": "1.0.0",
  "main": "index.js"
}

Output

(文件保存,无终端输出)

Narration

因为我们 COPY package.json 了,这个文件得存在。没有依赖没关系——npm install 会很快跑完。但有了它,将来加依赖也方便。

Demo 2.6:构建镜像

Screen

VSCode 终端,定位到项目目录,执行 build。

Command

docker build -t hello-docker:v1 .

Output

[+] Building 15.2s (10/10) FINISHED
 => [internal] load build definition from Dockerfile
 => [internal] load .dockerignore
 => [1/5] FROM docker.io/library/node:20-alpine
 => [2/5] WORKDIR /app
 => [3/5] COPY package.json ./
 => [4/5] RUN npm install
 => [5/5] COPY . .
 => exporting to image
 => => naming to docker.io/library/hello-docker:v1

Narration

docker build 是按菜谱做菜的过程。-t hello-docker:v1 给镜像起名打标签,方便以后引用。结尾的 . 表示"当前目录找 Dockerfile"。每一步的输出对应 Dockerfile 里的一行。15 秒构建完成。

Demo 2.7:运行容器并测试

Screen

终端启动容器,切到浏览器。

Command

docker run -d -p 3000:3000 --name hello-app hello-docker:v1
docker logs hello-app
curl http://localhost:3000

Output

b2c3d4e5f6a7...
Server running on http://localhost:3000

Hello Docker! 这是我的第一个容器化应用。

Narration

启动命令跟第一章很像,只是换了端口和镜像名。docker logs 看容器在说什么——"Server running on http://localhost:3000"。`curl` 测一下,浏览器也访问一下——我们的应用跑起来了。代码、环境、依赖,全都封装在这个叫 hello-docker:v1 的模具里。换台机器,docker run 一行命令,立刻就能跑。

Demo 2.8:验证镜像可移植性

Screen

终端查看镜像信息,导出再导入试一下。

Command

docker images hello-docker
docker history hello-docker:v1
docker stop hello-app && docker rm hello-app

Output

REPOSITORY     TAG       IMAGE ID       CREATED       SIZE
hello-docker   v1        f8e7d6c5b4a3   2 min ago     145MB

IMAGE          CREATED         CREATED BY                                      SIZE
f8e7d6c5b4a3   2 minutes ago   CMD ["node" "index.js"]                          0B
<missing>      2 minutes ago   COPY . .                                         4B
<missing>      3 minutes ago   RUN npm install                                  18MB
...

Narration

docker history 看镜像每一层是什么——每层对应 Dockerfile 一行。这就是 Dockerfile 的核心价值:你写的每一行,最终都变成了镜像里的一层,改动可追溯,构建可复现。

本章回顾

Key Takeaway

Dockerfile 是菜谱,镜像按菜谱做,容器用镜像启动。 关键技巧:先拷依赖清单、装依赖,最后才拷代码——这样改代码不会触发依赖重装。

Practical Use Case

任何后端服务(Node、Python、Go、Java)都可以这样打包。Dockerfile 进 Git,团队任何人、任何机器 docker build 出来的镜像都是一致的。

Transition

单个容器跑起来了。但真实项目往往是多个服务:前端、后端、数据库。下一章,我们用 Docker Compose 一键启动整个应用。


第三章 — Docker Compose:多容器编排

本章引入

Screen

打开浏览器,访问 Docker 官方文档的 Compose 页面。回到 VSCode,新建 multi-service-demo 文件夹。

Narration

真实项目至少三件套:前端、后端、数据库。纯用命令启动——创建网络、等数据库就绪、起后端、起前端——敲一堆命令,容易搞错顺序。Docker Compose 让所有服务写在一个文件里,一条命令全启动。这个文件进 Git,换台电脑一键还原。

Demo 3.1:搭建项目骨架

Screen

VSCode 新建目录,创建三个子目录。

Command

mkdir multi-service-demo && cd multi-service-demo
mkdir web api db
touch docker-compose.yml

Output

multi-service-demo/
├── api/
├── db/
├── docker-compose.yml
└── web/

Narration

三个目录代表三个服务。docker-compose.yml 是入口文件,统一描述整个应用。

Demo 3.2:写后端 API

Screen

VSCode 打开 api/server.js,输入代码。

Command

const http = require("http");
const server = http.createServer((req, res) => {
  res.writeHead(200, { "Content-Type": "application/json" });
  res.end(JSON.stringify({
    message: "Hello from API",
    time: new Date().toISOString()
  }));
});
server.listen(4000, () => console.log("API on :4000"));

Output

(文件保存,无终端输出)

Narration

API 服务跑在 4000 端口,返回 JSON。注意:接下来在 Compose 文件里我们直接用 api 这个服务名连它,不写 IP。

Demo 3.3:写前端页面

Screen

VSCode 打开 web/index.html。

Command

<!DOCTYPE html>
<html>
<body>
  <h1>Multi-Service Demo</h1>
  <div id="out">Loading...</div>
  <script>
    fetch("http://localhost:4000/")
      .then(r => r.json())
      .then(d => document.getElementById("out").innerText =
        JSON.stringify(d));
  </script>
</body>
</html>

Output

(文件保存,无终端输出)

Narration

前端通过 localhost:4000 调后端(开发阶段浏览器直连,生产环境会换种方式)。

Demo 3.4:写 docker-compose.yml

Screen

VSCode 打开 docker-compose.yml,逐行输入。

Command

services:
  api:
    image: node:20-alpine
    working_dir: /app
    volumes:
      - ./api:/app
    command: sh -c "npm init -y >/dev/null && npm install -g http-proxy && node server.js"
    ports:
      - "4000:4000"

  web:
    image: nginx:alpine
    volumes:
      - ./web:/usr/share/nginx/html:ro
    ports:
      - "8080:80"
    depends_on:
      - api

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: mypass
      POSTGRES_DB: appdb
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:

Output

(文件保存,无终端输出)

Narration

这是 Compose 的核心——一个文件,三服务。api 用 node 镜像跑后端;web 用 nginx 镜像托管前端静态文件;db 用 postgres 镜像跑数据库。三个关键点:volumes 把本地目录挂进容器(改代码不用重建);depends_on 控制启动顺序;最下方的 volumes: pgdata 给数据库做持久化——容器删了数据还在。

Demo 3.5:一键启动所有服务

Screen

终端执行 docker compose up -d,切换到浏览器。

Command

docker compose up -d
docker compose ps
sleep 3 && curl -s http://localhost:4000/

Output

[+] Running 4/4
 ✔ Network multi-service-demo_default  Created
 ✔ Volume multi-service-demo_pgdata    Created
 ✔ Container multi-service-demo-api-1  Started
 ✔ Container multi-service-demo-web-1  Started
 ✔ Container multi-service-demo-db-1   Started

NAME                          SERVICE   STATUS          PORTS
multi-service-demo-api-1      api       Up 5 seconds    0.0.0.0:4000->4000/tcp
multi-service-demo-web-1      web       Up 4 seconds    0.0.0.0:8080->80/tcp
multi-service-demo-db-1       db        Up 4 seconds    5432/tcp

{"message":"Hello from API","time":"2026-07-06T10:23:45.123Z"}

Narration

一条 docker compose up -d,三个容器全起来。docker compose ps 看每个服务的状态。curl 测一下后端——返回 JSON。前端访问 localhost:8080,浏览器会调用 4000 的 API,看到 API 的输出。这就是多服务编排。

Demo 3.6:服务名就是地址

Screen

终端进入 API 容器,用服务名连数据库。

Command

docker compose exec api sh
# 在容器内
apk add --no-cache postgresql-client >/dev/null && \
psql -h db -U postgres -d appdb -c "SELECT version();"
exit

Output

# 进入容器后
/app # psql -h db -U postgres -d appdb -c "SELECT version();"
                                                version
PostgreSQL 16.2 on x86_64-pc-linux-musl, compiled by gcc...

Narration

看仔细——我们连数据库用的是 -h db,不是 IP。Compose 自动给所有服务做了 DNS,在同一个网络里,服务名就是主机名。这就是 Compose 帮你省掉的功夫——不用记 IP、不用配 /etc/hosts。

Demo 3.7:查看日志和停止

Screen

终端操作演示日志查看和一键停止。

Command

docker compose logs --tail 10 api
docker compose logs -f db &
sleep 2 && kill %1 2>/dev/null
docker compose down
docker compose ps
docker compose down -v

Output

api-1  | Server running on :4000
db-1   | PostgreSQL init process complete; ready for start up.

NAME   SERVICE   STATUS   PORTS

Removing network multi-service-demo_default
Removing volume multi-service-demo_pgdata

Narration

docker compose logs 看日志,不用记容器全名。docker compose down 停服务、删容器、删网络。注意——这不会删数据卷。数据卷要单独用 -v 才删,平时千万别加,否则数据库就没了。

本章回顾

Key Takeaway

Compose 让多服务用一个 YAML 文件描述,一条命令启停。 同一网络里服务名就是主机名;数据库一定要加 volume,否则容器删了数据全没。

Practical Use Case

本地开发环境:前端 + 后端 + 数据库 + Redis + 消息队列,全装进 Compose。CI/CD 测试环境:每个 PR 启动一组完整服务跑测试,跑完销毁。

Transition

服务跑起来了,总会遇到问题——容器启动就退出、连不上数据库。下一章,我们讲常用命令和排错技巧。


第四章 — 常用命令与排错技巧

本章引入

Screen

终端准备操作,浏览器打开 Docker 官方命令参考。

Narration

容器不出问题是不可能的。掌握三个命令能解决 80% 的问题:docker ps -a 看状态、docker logs 看日志、docker exec -it 进容器。配合常用命令速查表,大部分问题自己就能搞定。

Demo 4.1:排错三步法演示

Screen

终端模拟一个"启动失败"的场景。

Command

# 故意写错入口文件
docker run -d --name broken-app hello-docker:v1
sleep 2
docker ps -a
docker logs broken-app
docker rm -f broken-app

Output

CONTAINER ID   IMAGE              STATUS                     NAMES
b1c2d3e4f5a6   hello-docker:v1    Exited (1) 2 seconds ago   broken-app

docker: Error response from daemon: failed to create task ...:
invalid working directory "/app": directory not found

# 因为当前目录没 COPY . .? 故意测试

Narration

容器启动后立刻退出——STATUS 显示 Exited (1)。docker logs 看一眼,立刻告诉你"工作目录不存在"或者"找不到模块"。日志就是答案,这就是为什么"先看日志"是排错的第一步。

Demo 4.2:进入容器内部调试

Screen

启动一个 alpine 容器并进入交互式 shell。

Command

docker run -d --name debug-demo alpine sleep 3600
docker exec -it debug-demo sh
# 在容器内
ls /
cat /etc/os-release
exit

Output

/ # ls /
bin   dev   etc   home  lib   media  mnt   opt   proc  root  run   sbin  srv   sys   tmp   usr   var

/ # cat /etc/os-release
NAME="Alpine Linux"
VERSION_ID=3.19.4
...

Narration

docker exec -it 像 SSH 进服务器。-i 保持输入,-t 分配终端。进去之后 ls、cat、ps,跟普通 Linux 命令一模一样。但记住——这是调试用,不要在容器里手动改东西然后保存。改了配置要改 Dockerfile,重新 build。

Demo 4.3:清理无用资源

Screen

终端演示清理命令。

Command

docker system df
docker system prune -f
docker image prune -f
docker volume prune -f

Output

TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          8         3         1.2GB     800MB (66%)
Containers      5         2         50MB      30MB (60%)
Local Volumes   3         1         200MB     150MB (75%)
Build Cache     15        0         500MB     500MB (100%)

Deleted Containers:
...
Total reclaimed space: 1.4GB

Narration

构建几次后,docker images 里全是 <none>:<none> 的废弃镜像。docker system prune 一键清理——停掉的容器、无用的网络、悬空镜像、构建缓存。注意:它不会删正在用的容器和数据卷。

Demo 4.4:常用命令速查演练

Screen

终端逐条演示速查表命令。

Command

docker ps                    # 运行中的容器
docker ps -a                 # 全部容器(含已停)
docker logs --tail 20 web    # 容器最后 20 行日志
docker stop web              # 停止容器(发送 SIGTERM)
docker kill web              # 强制停止(SIGKILL)
docker rm web                # 删除已停的容器
docker rm -f web             # 强制删除(运行中也可)
docker images                # 镜像列表
docker rmi nginx:latest      # 删除镜像
docker volume ls             # 数据卷列表
docker network ls            # 网络列表

Output

CONTAINER ID   IMAGE     STATUS         PORTS     NAMES
(具体输出按当前环境而定)

REPOSITORY   TAG       IMAGE ID   CREATED   SIZE

Narration

这张表打印出来贴显示器旁边。前五条 ps / logs / exec / stop / rm,加上 prune,足够应付日常 90% 的场景。

本章回顾

Key Takeaway

排错三步法:ps 看状态 → logs 看日志 → exec 进容器。 容器是临时的,配置改 Dockerfile,不在容器里手动保存。

Practical Use Case

CI/CD 跑测试:测试容器失败,立刻 docker logs 看错误;本地开发:服务起不来,先看 STATUS 是 Exited 还是 Restarting,再决定看哪里的日志。

Transition

四章基础都讲完了。咱们来一个真实的综合实战——把今天所有东西串起来。


最终实战项目 — 用 Docker Compose 启动一个真实博客应用

Screen

终端新建项目目录 blog-stack,全程在 VSCode 里操作。

Narration

最后来一个完整的实战:搭一个博客系统,包含 Node 后端 + nginx 前端 + postgres 数据库,全部用 Docker Compose 编排,5 分钟跑起来。

Demo 5.1:搭建项目骨架

Screen

VSCode 新建项目目录,创建子目录结构。

Command

mkdir blog-stack && cd blog-stack
mkdir -p api web
touch docker-compose.yml api/package.json

Output

blog-stack/
├── api/
│   └── package.json
├── docker-compose.yml
└── web/

Narration

项目结构简单:一个 docker-compose.yml,一个后端目录,一个前端目录。咱们从后端开始。

Demo 5.2:写后端 + 自己的 Dockerfile

Screen

VSCode 创建 api/server.js、api/Dockerfile。

Command

// api/server.js
const http = require("http");
const { Client } = require("pg");

const client = new Client({
  host: process.env.DB_HOST || "db",
  user: "postgres",
  password: "mypass",
  database: "blogdb"
});

async function start() {
  await client.connect();
  await client.query(`
    CREATE TABLE IF NOT EXISTS posts (
      id SERIAL PRIMARY KEY,
      title TEXT NOT NULL,
      created_at TIMESTAMPTZ DEFAULT NOW()
    )
  `);
  await client.query(
    "INSERT INTO posts (title) VALUES ($1) ON CONFLICT DO NOTHING",
    ["Welcome to Docker!"]
  );

  http.createServer(async (req, res) => {
    res.writeHead(200, { "Content-Type": "application/json" });
    const { rows } = await client.query(
      "SELECT id, title, created_at FROM posts ORDER BY id DESC"
    );
    res.end(JSON.stringify(rows));
  }).listen(4000, () => console.log("Blog API on :4000"));
}

start().catch(e => { console.error(e); process.exit(1); });

Command

// api/package.json
{
  "name": "blog-api",
  "version": "1.0.0",
  "main": "server.js",
  "dependencies": { "pg": "^8.11.0" }
}

Command

# api/Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package.json .
RUN npm install --production
COPY server.js .
EXPOSE 4000
CMD ["node", "server.js"]

Output

(文件保存,无终端输出)

Narration

后端连数据库(用服务名 db)、自动建表、插入演示数据、暴露 JSON API。注意 Dockerfile 还是那个套路:先拷 package.json,再 npm install,最后拷代码。

Demo 5.3:写前端页面

Screen

VSCode 打开 web/index.html。

Command

<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>Blog</title></head>
<body>
  <h1>📝 My Docker Blog</h1>
  <ul id="posts"></ul>
  <script>
    fetch("/api/posts")
      .then(r => r.json())
      .then(posts => {
        document.getElementById("posts").innerHTML =
          posts.map(p =>
            `<li><b>${p.title}</b> — ${new Date(p.created_at).toLocaleString()}</li>`
          ).join("");
      });
  </script>
</body>
</html>

Output

(文件保存,无终端输出)

Narration

前端通过相对路径 /api/posts 调后端——nginx 接下来会做反向代理。

Demo 5.4:写 docker-compose.yml

Screen

VSCode 打开 docker-compose.yml,输入完整编排。

Command

services:
  api:
    build: ./api
    environment:
      DB_HOST: db
    depends_on:
      - db

  web:
    image: nginx:alpine
    volumes:
      - ./web:/usr/share/nginx/html:ro
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
    ports:
      - "8080:80"
    depends_on:
      - api

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: mypass
      POSTGRES_DB: blogdb
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:

Output

(文件保存,无终端输出)

Narration

三个服务:api 自己 build(用 api/Dockerfile),web 用官方 nginx 镜像,db 用 postgres。depends_on 控制顺序。api 用环境变量 DB_HOST=db 连数据库。

Demo 5.5:写 nginx 反向代理配置

Screen

VSCode 新建 nginx.conf。

Command

server {
  listen 80;
  location / {
    root /usr/share/nginx/html;
    index index.html;
  }
  location /api/ {
    proxy_pass http://api:4000/;
  }
}

Output

(文件保存,无终端输出)

Narration

nginx 把 /api/ 路径代理到 api 服务。看到没——http://api:4000/——这就是 Compose 的 DNS 魔法:服务名当主机名用,零配置。

Demo 5.6:一键启动

Screen

终端执行启动命令,等待几秒后测试。

Command

docker compose up -d --build
docker compose ps
sleep 5
curl -s http://localhost:8080/api/posts
curl -s http://localhost:8080/ | head -20

Output

[+] Running 4/4
 ✔ Network blog-stack_default   Created
 ✔ Volume  blog-stack_pgdata     Created
 ✔ Container blog-stack-api-1    Started
 ✔ Container blog-stack-web-1    Started
 ✔ Container blog-stack-db-1     Started

NAME                  SERVICE   STATUS          PORTS
blog-stack-api-1      api       Up 8 seconds
blog-stack-web-1      web       Up 7 seconds    0.0.0.0:8080->80/tcp
blog-stack-db-1       db        Up 7 seconds     5432/tcp

[{"id":1,"title":"Welcome to Docker!","created_at":"2026-07-06T10:30:00.000Z"}]
<!DOCTYPE html>
<html>
<head>...

Narration

docker compose up -d --build——既构建又启动。三服务起来后,curl localhost:8080/api/posts 拿到数据库里的文章;curl localhost:8080/ 拿到前端页面。打开浏览器访问 localhost:8080——看到博客首页,标题"Welcone to Docker!"显示出来了。

Demo 5.7:验证持久化

Screen

终端演示重启数据不丢。

Command

# 在数据库里手动插入一条数据
docker compose exec db psql -U postgres -d blogdb \
  -c "INSERT INTO posts (title) VALUES ('数据持久化测试');"
# 删掉整个栈
docker compose down
# 重启
docker compose up -d
sleep 5
curl -s http://localhost:8080/api/posts

Output

INSERT 0 1

[{"id":2,"title":"数据持久化测试","created_at":"..."},
 {"id":1,"title":"Welcome to Docker!","created_at":"..."}]

Narration

手插一条数据,docker compose down 删容器,重启——数据还在。这就是 volumes: pgdata 的价值——容器是临时的,卷是持久的。如果上一章没加 volume,这一步数据就没了。

Demo 5.8:清理

Screen

终端操作,演示收尾。

Command

docker compose down              # 停服务、删容器、保留数据
docker compose down -v           # 删数据卷(演示用)
docker compose down --rmi all    # 连镜像一起删

Output

[+] Running 4/0
 ✔ Container blog-stack-api-1  Removed
 ✔ Container blog-stack-web-1  Removed
 ✔ Container blog-stack-db-1   Removed
 ✔ Network blog-stack_default  Removed

Narration

三种收尾方式:down 保留卷,down -v 连数据一起删(演示完用),down --rmi all 把自定义镜像也清掉。日常开发:down 即可;想完全重置:down -v。


课程收尾

Screen

回到终端黑屏,文字浮现课程总结。

Narration

十分钟到了。咱们一路干了这些事:

你现在能做什么:把任何 Node、Python、Go、Java 项目 Docker 化;用 Compose 拉起一套完整本地开发环境;遇到问题自己排。

下一步学什么:

Docker 不难,难的是"看不看日志"。只要遇到问题先看日志,大部分都能自己解决。代码写得再好,不会 Docker 也只能在自己的机器上跑——现在你可以让它跑到世界上任何一台机器上。

去试一下,把你现在正在做的项目 Docker 化。下一期见。


速查附录(可直接复制到收藏夹)

镜像与容器

docker pull nginx                          # 拉镜像
docker images                              # 看镜像
docker rmi nginx                           # 删镜像
docker run -d -p 8080:80 --name web nginx  # 跑容器
docker ps / docker ps -a                   # 看容器
docker stop web / docker rm web            # 停/删
docker logs -f web                         # 跟日志
docker exec -it web sh                     # 进容器

Dockerfile 模板

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .
EXPOSE 3000
CMD ["node", "index.js"]

Compose 模板

services:
  app:
    build: .
    ports: ["3000:3000"]
    depends_on: [db]
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: mypass
    volumes: [pgdata:/var/lib/postgresql/data]
volumes:
  pgdata:

排错流程

docker ps -a           # 1. 状态?
docker logs app        # 2. 日志?
docker exec -it app sh # 3. 进容器看
docker system prune    # 4. 清理无用资源

实践示例

引用此知识点

更多基础课程知识