Featured image of post Docker 入门与实战:从容器到 Compose

Docker 入门与实战:从容器到 Compose

从运行第一个容器开始,用 Go 服务与 Caddy 反向代理串联镜像构建、数据持久化、容器网络和 Docker Compose,附日常命令与排障速查

从下面的 Nginx 实验开始:启动容器,访问页面,再停止并删除它。完成一次运行之后,再看镜像、数据卷和网络各自解决什么问题。

本文使用 Linux 容器、现代 docker compose 命令和 Bash/Zsh 语法。macOS、Windows 通常使用 Docker Desktop,Linux 可以安装 Docker Engine 与 Compose 插件;Windows 用户可在 WSL 中执行示例。示例假设 CLI 连接本机 Docker,首次拉取镜像需要网络。

阶段 要解决的问题 完成标志
1. 运行容器 怎样启动一个现成的服务? 浏览器显示 Nginx 欢迎页
2. 理解对象 镜像、容器和宿主机是什么关系? 分清创建、停止与删除
3. 构建镜像 怎样交付自己的应用? Go 服务返回主机名和健康状态
4. 管理数据与网络 数据放哪里,服务怎么互相访问? 新容器读到旧数据,通过名称访问应用
5. 使用 Compose 怎样用一份配置启动多个服务? 请求经 Caddy 转发到 Go 服务

顺序阅读可完成整套实验;日常使用时可直接跳到命令速查或常见问题。

先运行一个容器

检查环境

安装入口见 Docker 官方安装文档。安装后执行:

1
2
3
docker version
docker compose version
docker run --rm hello-world

docker version 应同时显示 Client 和 Server;hello-world 应输出 Hello from Docker!。如果无法连接 daemon,先启动 Docker Desktop 或 Linux 上的 Docker 服务。docker info 可以进一步查看服务端环境。

启动 Nginx

1
2
3
4
docker run --detach \
  --name docker-web \
  --publish 127.0.0.1:8080:80 \
  nginx:alpine

打开 http://127.0.0.1:8080,应看到 Nginx 欢迎页,也可以用下面的命令检查:

1
2
curl -I http://127.0.0.1:8080
docker ps --filter name=docker-web

预期 HTTP 状态为 200 OK,容器状态为 Up。如果宿主机的 8080 已被占用,把发布参数改成 127.0.0.1:18080:80,访问地址也改为 18080。后面的独立容器实验同样只修改宿主机端口,Compose 示例则通过 APP_PORT 调整。

片段 含义
docker run 根据镜像创建并启动一个新容器;本地没有镜像时自动拉取
--detach / -d 在后台运行
--name docker-web 为容器指定名称,后续命令可直接引用
--publish 127.0.0.1:8080:80 / -p 把宿主机本地的 8080 转发到容器的 80
nginx:alpine 镜像仓库名与标签

命令结构是 docker run [Docker 选项] 镜像 [命令及参数]。-p、--name 等 Docker 选项必须写在镜像名之前。

查看日志,再结束实验

1
2
3
docker logs --tail 20 docker-web
docker stop docker-web
docker ps -a --filter name=docker-web

此时容器仍然存在,只是状态变为 Exited。重新启动和删除是两个不同操作:

1
2
3
4
5
6
# 重新启动原来的容器
docker start docker-web

# 实验结束:先停止,再删除
docker stop docker-web
docker rm docker-web

删除后可以再次使用 docker-web 这个名称。镜像仍保留在本机,下次运行可直接复用。

理解镜像、容器与运行环境

Docker 用统一的工具构建、分发和运行容器化应用。它把应用、用户空间依赖和运行时打包成镜像,减少“换一台机器就无法启动”的环境差异;端口、数据和密钥等运行配置仍需要单独提供。

从构建到运行

  flowchart LR
    A[源代码与 Dockerfile] -->|docker build| B[镜像]
    B -->|docker push| C[镜像仓库 Registry]
    C -->|docker pull| D[另一台机器上的镜像]
    B -->|docker run| E[容器]
    D -->|docker run| F[容器]
对象 保存什么 生命周期
Dockerfile 基础镜像、复制文件和构建命令等配方 随源代码维护
镜像(image) 应用、依赖、只读文件系统层与默认配置 可用于创建多个容器
容器(container) 运行配置、进程状态和独立可写层 停止后仍存在,删除时可写层消失
数据卷(volume) 需要跨容器保留的数据 命名数据卷可独立于容器存在
网络(network) 容器之间的连接、地址与服务发现 可供多个容器加入

Registry 是存储和分发镜像的服务,例如 Docker Hub 或私有镜像仓库。日常命令由 Docker CLI 发出,经 API 交给 Docker daemon;daemon 管理这些对象,并通过 containerd、OCI runtime 等组件启动容器进程。

镜像不会随着容器一起改变

容器运行时,在镜像的只读层之上使用自己的可写层。写入这个可写层的文件不会修改原镜像,也不会自动进入其他容器。

操作 实际发生的事
docker run IMAGE 创建一个新容器并启动
docker stop CONTAINER 停止容器进程,保留容器及其可写层
docker start CONTAINER 再次启动已有容器
docker rm CONTAINER 删除容器及其可写层;命名数据卷通常保留
docker pull IMAGE 下载镜像,不会替换已经运行的容器

更新应用需要使用新镜像重新创建容器。 单纯拉取镜像或重启旧容器,不会把新镜像内容应用进去。

nginx:alpine 中的 alpine 是标签。标签可以被发布者重新指向另一个镜像;省略标签时默认使用 latest,它也只是普通标签,不保证最新或最稳定。本文使用便于练习的版本或变体标签,正式交付应选择经过测试的版本;严格复现时使用 digest,并安排后续更新。

容器与虚拟机的边界

维度 Linux 容器 虚拟机
内核 共享宿主 Linux 内核 每台虚拟机运行自己的内核
隔离方式 进程与资源隔离 硬件虚拟化
启动与开销 通常较快、较轻 通常需要启动完整操作系统
常见用途 应用交付、开发环境、CI/CD 不同内核、完整系统环境、较强隔离

Linux 通过 namespaces 隔离进程、网络和挂载等资源视图,通过 cgroups 统计和限制资源,结合 capabilities、seccomp 等机制约束权限。容器默认不会自动获得合适的 CPU、内存限制。

在 macOS 和 Windows 的 Docker Desktop 中,Linux 容器通常运行在其管理的 Linux 虚拟机里。镜像仍受操作系统、CPU 架构和内核能力约束;linux/amd64 与 linux/arm64 之间的运行需要兼容架构或模拟支持。

用 Dockerfile 构建自己的服务

这一节创建一个只依赖 Go 标准库的 HTTP 服务。编译器放在构建镜像中,本机不需要安装 Go。

准备应用文件

创建目录,后续构建和 Compose 命令都在这里执行:

1
2
mkdir -p docker-demo
cd docker-demo

目录中先准备四个文件:

1
2
3
4
5
docker-demo/
├── .dockerignore
├── Dockerfile
├── go.mod
└── main.go

go.mod:

1
2
3
module example.com/docker-demo

go 1.25

main.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import (
	"fmt"
	"log"
	"net/http"
	"os"
	"time"
)

func main() {
	hostname, err := os.Hostname()
	if err != nil {
		hostname = "unknown"
	}
	message := os.Getenv("APP_MESSAGE")
	if message == "" {
		message = "hello"
	}

	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
		fmt.Fprintf(w, "%s from %s\n", message, hostname)
	})
	mux.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) {
		fmt.Fprintln(w, "ok")
	})

	server := &http.Server{
		Addr:              ":8080",
		Handler:           mux,
		ReadHeaderTimeout: 5 * time.Second,
	}
	log.Println("listening on :8080")
	log.Fatal(server.ListenAndServe())
}

/ 返回问候语和容器主机名,/health 返回 ok。程序监听 :8080,可通过容器网络接口访问;如果只监听容器内的 127.0.0.1,端口映射通常无法访问它。

编写多阶段 Dockerfile

Dockerfile:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# syntax=docker/dockerfile:1

FROM golang:1.25-alpine AS build
WORKDIR /src
COPY go.mod ./
RUN go mod download
COPY main.go ./
RUN CGO_ENABLED=0 GOOS=linux go build \
    -trimpath \
    -ldflags="-s -w" \
    -o /out/server .

FROM alpine:3.22 AS runtime
RUN addgroup -S app && adduser -S -G app app
WORKDIR /app
COPY --from=build /out/server ./server
USER app
EXPOSE 8080
ENTRYPOINT ["./server"]

build 阶段包含编译器和源码;runtime 阶段只复制编译结果,并以非 root 用户运行。这个纯 Go 示例关闭了 CGO,避免依赖构建阶段的动态链接库。使用 CGO 的应用需要另行匹配运行时依赖。

先理解构建时执行的五条指令:

指令 作用
FROM 选择基础镜像,也可以开始一个新阶段
WORKDIR 设置后续指令的工作目录
COPY 复制构建上下文中的文件;--from 可从其他阶段复制
RUN 在构建过程中执行命令
ARG 声明构建参数,不会自动成为容器环境变量

再区分会影响运行默认值的指令:

指令 作用
ENV 设置环境变量,容器启动时可覆盖
USER 设置后续构建指令和容器进程的用户
EXPOSE 记录应用端口,不会自动发布宿主机端口
ENTRYPOINT 设置容器入口程序
CMD 提供默认命令;配合 ENTRYPOINT 时通常提供默认参数

这里的 exec form ENTRYPOINT ["./server"] 让服务直接接收容器信号。应用仍需自行处理信号,才能完成连接排空等优雅退出逻辑。

控制构建上下文与缓存

.dockerignore:

1
2
3
4
5
6
7
8
.git
.env
.env.*
secrets/
backup/
*.log
tmp/
bin/

执行 docker build ... . 时,最后的 . 指定构建上下文。普通 COPY 从上下文中取文件,不能越过它读取父目录;.dockerignore 排除的文件也不能被普通 COPY 复制。

Dockerfile 先复制 go.mod 并下载依赖,再复制 main.go。这样只修改源码时,通常可以复用依赖下载层。当前示例没有第三方依赖,因此没有 go.sum;实际项目应一起复制并维护 go.mod 和 go.sum。

BuildKit 还支持缓存挂载。需要加速 Go 编译时,可以把构建指令替换为:

1
2
3
RUN --mount=type=cache,target=/go/pkg/mod \
    --mount=type=cache,target=/root/.cache/go-build \
    CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/server .

缓存挂载用于复用下载或编译结果,其目录内容不会自动写入镜像层。构建凭据应使用 secret mount,避免通过 ARG、ENV 或“复制后删除”留在镜像配置或历史层中。

构建、运行并验证

确认第一节的 Nginx 已停止,释放 8080 端口:

1
2
3
4
5
6
7
8
docker build --pull --tag docker-demo:1.0 .
docker run --detach --rm \
  --name docker-demo-app \
  --publish 127.0.0.1:8080:8080 \
  docker-demo:1.0

curl --fail http://127.0.0.1:8080/
curl --fail http://127.0.0.1:8080/health

预期分别返回 hello from <容器主机名> 和 ok。--pull 会检查基础镜像更新;它不等于禁用构建缓存。这里使用 --rm,停止后会自动删除实验容器:

1
docker stop docker-demo-app

把数据与网络接到容器上

验证数据卷的生命周期

停止容器会保留可写层,删除容器才会删除可写层。需要跨容器保存的数据,应放到挂载中。

挂载方式 数据在哪里 典型用途
命名数据卷(volume) 由 Docker 管理,独立于容器 数据库、上传文件
绑定挂载(bind mount) Docker 宿主机上的指定路径 开发源码、配置文件
tmpfs Linux 宿主机内存,停止后消失 临时文件;仍可能受交换空间配置影响

先用一个容器写入数据,再用另一个容器读取:

1
2
3
4
5
6
7
8
9
docker volume create docker-demo-data

docker run --rm \
  --mount type=volume,src=docker-demo-data,dst=/data \
  alpine:3.22 sh -c 'echo persisted > /data/message.txt'

docker run --rm \
  --mount type=volume,src=docker-demo-data,dst=/data,readonly \
  alpine:3.22 cat /data/message.txt

看到 persisted 就说明数据不依赖第一个容器。--rm 不会删除显式命名的数据卷。

绑定挂载适合把已有文件交给容器,例如只读查看当前目录:

1
2
3
docker run --rm \
  --mount type=bind,src="$(pwd)",dst=/workspace,readonly \
  alpine:3.22 ls -la /workspace

--mount 默认会在绑定挂载源路径不存在时报错;-v 则可能自动创建目录,掩盖路径拼写错误。挂载会遮蔽目标路径的原有内容;首次挂载空数据卷时,Docker 默认还可能把目标目录原有内容复制进卷。绑定挂载需要额外处理宿主机路径与 UID/GID 权限,Docker Desktop 还受目录共享设置影响。

练习完成后,只删除本节创建的卷;这会永久删除其中的实验数据:

1
docker volume rm docker-demo-data

数据卷本身不是备份。数据库应优先使用自身的备份工具并验证恢复;复制数据文件时需要保证写入一致性。

通过容器名称访问服务

先建立用户自定义桥接网络,再把第三节的 Go 镜像接进去:

1
2
3
4
5
6
7
8
9
docker network create docker-demo-net
docker run --detach --rm \
  --name docker-demo-app \
  --network docker-demo-net \
  docker-demo:1.0

docker run --rm \
  --network docker-demo-net \
  alpine:3.22 wget -qO- http://docker-demo-app:8080/health

预期输出 ok。这次没有 -p:临时客户端与应用在同一用户自定义网络中,可以通过 Docker DNS 解析容器名,直接访问容器端口。默认 bridge 网络不提供相同的容器名自动解析体验。

谁访问谁 应使用的地址
宿主机访问发布后的服务 127.0.0.1:宿主机端口
同一网络中的容器访问应用 docker-demo-app:8080;Compose 中使用服务名
容器访问自己 127.0.0.1:容器端口
容器访问 Docker Desktop 宿主机 通常使用 host.docker.internal:服务端口

Linux Engine 上访问宿主机时,可在 docker run 中添加 --add-host host.docker.internal:host-gateway;宿主机服务也必须监听容器可达的接口并允许访问。容器内的 localhost 不会指向另一个容器或宿主机。

发布端口时,-p 8080:80 默认通常绑定宿主机所有接口;-p 127.0.0.1:8080:80 用于本机开发。服务间通信通常只需要共享网络,不必给每个服务发布端口。

结束本节实验,下一节交给 Compose 管理网络:

1
2
docker stop docker-demo-app
docker network rm docker-demo-net

用 Compose 组织完整应用

现在把 Go 服务放在 Caddy 反向代理后面,由一份 compose.yaml 声明构建、网络、挂载和启动依赖。现代 Compose 使用 docker compose 命令并遵循 Compose Specification,顶层不需要 version: "3"。

  flowchart LR
    A[浏览器] -->|127.0.0.1:8080| B[proxy:80 / Caddy]
    B -->|app:8080 / 项目网络| C[app / Go 服务]
    D[Caddyfile] -.只读绑定挂载.-> B
    E[Caddy 命名数据卷] --- B

添加 Caddy 与 Compose 配置

在第三节的 docker-demo 目录里增加 Caddyfile 和 compose.yaml:

1
2
3
4
5
6
7
docker-demo/
├── .dockerignore
├── Caddyfile
├── Dockerfile
├── compose.yaml
├── go.mod
└── main.go

Caddyfile:

1
2
3
:80 {
	reverse_proxy app:8080
}

compose.yaml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
name: docker-demo

services:
  app:
    build:
      context: .
      target: runtime
    environment:
      APP_MESSAGE: "${APP_MESSAGE:-hello}"
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/health"]
      interval: 10s
      timeout: 3s
      retries: 3
      start_period: 5s

  proxy:
    image: caddy:2-alpine
    restart: unless-stopped
    ports:
      - "127.0.0.1:${APP_PORT:-8080}:80"
    volumes:
      - type: bind
        source: ./Caddyfile
        target: /etc/caddy/Caddyfile
        read_only: true
        bind:
          create_host_path: false
      - caddy-data:/data
      - caddy-config:/config
    depends_on:
      app:
        condition: service_healthy

volumes:
  caddy-data:
  caddy-config:

这里使用 Compose 自动创建的项目默认网络,proxy 可以通过服务名 app 找到 Go 服务,无需硬编码 IP 或设置 container_name。只有 proxy 向宿主机发布端口,app 通过项目网络接收请求。

Caddyfile 以只读方式挂载,源文件不存在时直接报错。/data 和 /config 使用命名数据卷保存 Caddy 的持久状态;本例只配置 HTTP,不涉及域名与自动 HTTPS 证书签发。

app 的健康检查在它自己的容器中执行,因此使用 127.0.0.1:8080。运行镜像 Alpine 自带的 BusyBox wget 可用于本例检查;换成 distroless 等镜像时,必须同步调整检查方式。

启动并检查整条请求链路

1
2
3
4
5
6
7
8
# 验证配置,再构建并等待服务就绪
docker compose config --quiet
docker compose up --detach --build --wait --wait-timeout 120

# 查看状态,验证经过 Caddy 的请求
docker compose ps
curl --fail http://127.0.0.1:8080/
curl --fail http://127.0.0.1:8080/health

预期 app 显示 healthy、proxy 处于运行状态,请求返回 hello from <容器主机名> 与 ok。可用 docker compose logs --tail 50 排查启动问题。

depends_on: condition: service_healthy 让 Caddy 在创建启动时等待应用健康;普通 depends_on 只保证依赖先启动,不保证可接受请求。--wait 对有健康检查的服务等待健康,对没有健康检查的服务只等待运行,因此仍需用 curl 验证入口。

健康检查失败不会单独触发 restart: unless-stopped。重启策略主要处理进程退出,启动依赖也不会持续修复运行期间的依赖故障;实际应用仍需实现超时、重试和恢复。

修改配置:区分插值与容器环境变量

在同一目录新建可选的 .env:

1
2
APP_PORT=18080
APP_MESSAGE=hello-compose

保持 compose.yaml 使用前面的完整配置,然后执行:

1
2
3
docker compose config
docker compose up --detach --wait --wait-timeout 120
curl --fail http://127.0.0.1:18080/

预期返回 hello-compose from <容器主机名>。后续从宿主机验证服务时,使用新端口 18080。

配置机制 发生在哪一步 本例中的作用
Shell 环境或 .env 中的变量 Compose 解析配置时 替换 ${APP_PORT:-8080} 等表达式;Shell 同名变量优先
environment 创建容器时 将展开后的 APP_MESSAGE 注入应用容器
服务级 env_file 创建容器时 把指定文件中的变量传给该服务容器

.env 被 Compose 读取,不代表其中所有变量自动进入容器。 本例的 APP_PORT 只改变宿主机发布端口,Go 服务仍监听容器端口 8080。APP_MESSAGE 能被 Go 程序读取,是因为配置中写了 environment。

docker compose config 会显示展开后的配置,不应把包含真实密钥的输出直接贴入公共日志。密钥应使用未提交到 Git 的文件或专门的密钥管理系统;Compose secrets 可将文件挂到 /run/secrets/<名称>,但本地源文件仍是明文。

更新与停止

需求 命令
修改源码或 Dockerfile 后更新 docker compose up -d --build --wait
修改环境变量或 Compose 服务配置后更新 docker compose up -d --wait
拉取 Caddy 镜像更新并应用 docker compose pull --ignore-buildable,再执行 docker compose up -d --wait
暂停环境,保留容器 docker compose stop;恢复用 docker compose start
结束实验,删除容器与项目网络 docker compose down;默认保留命名数据卷

docker compose restart 只重启已有容器,不会应用新的环境变量、镜像或服务配置。若只修改了绑定挂载的 Caddyfile 内容,可执行 docker compose exec proxy caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile,让 Caddy 重新加载配置。

完整重置本例时,docker compose down --volumes 还会删除声明的非 external 命名数据卷和附属匿名卷。它会永久删除卷内数据,且不会额外询问确认,应与日常 down 区分使用。

命令速查

下面的 IMAGE、CONTAINER、NETWORK 和 registry.example.com/team 都是占位值,执行前需要替换。这些是按需查阅的命令,不是一段需要顺序执行的脚本。

镜像与分发

目的 命令
拉取、列出镜像 docker pull IMAGE / docker image ls(或 docker images)
检查配置与构建历史 docker image inspect IMAGE / docker image history IMAGE
添加引用、推送镜像 docker tag IMAGE registry.example.com/team/app:1.0 / docker push registry.example.com/team/app:1.0
离线导出与载入 docker save -o app.tar IMAGE / docker load -i app.tar
删除本地镜像引用 docker image rm IMAGE(或 docker rmi IMAGE)

推送前使用 docker login registry.example.com 登录;自动化中使用 --password-stdin 从标准输入接收凭据,避免把令牌直接写在命令参数中。

save/load 保存镜像配置和镜像层,不包含数据卷或容器可写层。export/import 处理容器文件系统,无法完整保留原镜像的分层、历史与配置,也不会备份挂载卷,不能与镜像归档或数据备份混用。

需要发布多架构镜像时,可使用 Buildx:

1
2
3
4
5
6
7
8
# 先核对 builder 支持的平台
docker buildx inspect --bootstrap

# 替换为自己的仓库地址;--push 会实际发布镜像
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --tag registry.example.com/team/app:1.0 \
  --push .

跨架构 RUN 需要原生 worker 或 QEMU/binfmt 模拟支持。非默认 builder 的单平台本地构建通常需要 --load 才会载入本地镜像列表,多平台结果通常直接推送到仓库。

容器与日志

目的 命令
查看全部容器及状态 docker ps -a
跟踪最近日志 docker logs --follow --tail 100 CONTAINER
在已有容器中执行命令 docker exec -it CONTAINER sh
检查配置与端口 docker inspect CONTAINER / docker port CONTAINER
查看资源与进程 docker stats CONTAINER / docker top CONTAINER

logs --follow 和 stats 会持续运行,按 Ctrl+C 退出查看;exec 启动的 shell 用 exit 退出。精简镜像可能没有 Bash,distroless 或 scratch 镜像甚至没有 shell,应通过日志、指标或专门的调试工具排查。

需要复制文件时,使用 docker cp ./file CONTAINER:/path 或反向复制;需要查看可写层变化时,使用 docker diff CONTAINER。这些操作不会把修改自动写回 Dockerfile。

Compose 与资源限制

目的 命令
检查配置与状态 docker compose config / docker compose ps
跟踪一个服务的日志 docker compose logs --follow --tail 100 app
在应用容器内执行命令 docker compose exec app sh
创建一次性容器并覆盖入口 docker compose run --rm --entrypoint sh app -c 'id'
查看项目使用的镜像 docker compose images

给独立容器设置资源上限的示例,使用第三节已经构建的镜像:

1
2
3
4
5
docker run --rm \
  --memory 512m \
  --cpus 1.0 \
  --pids-limit 200 \
  docker-demo:1.0

命令在前台运行,按 Ctrl+C 结束。Compose 服务可以配置对应的 mem_limit、cpus、pids_limit,再用 up -d 应用,并通过 docker inspect、docker stats 核对效果。

磁盘占用与清理

先用 docker system df --verbose 确认空间用在哪里,再选择清理范围:

命令 删除范围
docker container prune 所有已停止的容器及其可写层
docker image prune 悬空镜像;加 --all 扩大到未被容器引用的镜像
docker builder prune 未使用的构建缓存,默认主要清理悬空缓存
docker network prune 未被容器使用的网络
docker volume prune 现代 Engine 默认仅清理未被容器引用的匿名本地卷;--all 包括命名卷

未被引用,不代表数据已经没有价值。 prune 的确认提示主要说明删除范围,并不是完整的待删除对象预览。先查看 docker ps -a、docker volume ls 和相关 inspect 输出,确认后再清理;能明确定位的对象,优先按名称删除。

docker system prune --all --volumes 会合并清理停止的容器、未使用网络、未被容器引用的镜像、构建缓存与未使用匿名卷,影响范围很大,不适合作为日常实验的收尾命令。

常见问题

容器启动后立即退出

容器主进程退出,容器就会停止。先查退出码和日志:

1
2
3
docker ps -a
docker inspect --format '{{.State.ExitCode}}' CONTAINER
docker logs CONTAINER

检查入口命令、缺失配置与应用错误。服务应以前台进程运行;在容器里把服务放到后台后退出入口脚本,会导致容器结束。排障期间可暂时省略 --rm,保留退出后的状态与日志。

端口已经发布,却无法访问

  1. 用 docker ps -a 和日志确认服务没有退出。
  2. 用 docker port CONTAINER 核对“宿主机端口 → 容器端口”的映射。
  3. 确认应用监听容器网络接口,而不是只监听容器内的 127.0.0.1。
  4. 如果从另一台机器访问,确认端口没有仅绑定宿主机 127.0.0.1,并检查防火墙与安全组。

如果报 port is already allocated,是宿主机端口冲突:单容器修改 -p 左侧端口,Compose 示例修改 APP_PORT。容器内部的监听端口不必跟着改变。

服务名解析失败,或代理返回 502

先用 docker compose ps 确认两个服务都存在,再检查 docker compose logs --tail 100 app proxy。同一网络内使用服务名与容器端口,不能用宿主机映射端口或把 localhost 当作其他服务。

对本文示例,可从 Caddy 容器中直接请求应用:

1
docker compose exec proxy wget -qO- http://app:8080/health

返回 ok 说明代理到应用的网络路径正常,再检查 Caddy 配置与宿主机入口。独立容器使用 docker network inspect NETWORK 核对网络成员。

修改代码或配置后没有生效

修改内容 应执行的操作
main.go、Dockerfile 或构建依赖 docker compose up -d --build --wait
.env、environment 或端口等服务配置 docker compose up -d --wait
绑定挂载的 Caddyfile 在 Caddy 容器中执行 caddy reload

先检查构建上下文、.dockerignore 和 COPY 是否包含目标文件。只有仍怀疑缓存时,才用 docker compose build --no-cache app 后重新 up -d;--no-cache 不需要用于每次构建,也不会自动拉取更新的基础镜像。

重建容器后数据不见了

先确认数据是否曾写入命名卷,以及新容器是否挂载了同一个卷。docker inspect CONTAINER 的 Mounts 和 docker volume inspect VOLUME 可帮助核对。

Compose 默认把项目名作为卷名的一部分,例如本例的 docker-demo_caddy-data。改变项目名可能创建一组新卷,看起来像“数据丢失”,旧卷可能仍在。不要先运行清理命令;如果已执行 down --volumes,则需要从备份恢复。

用到真实项目时

本文示例用于本地练习。投入实际服务前,补齐与应用相关的五件事:

  1. 管理交付版本:使用可信基础镜像,固定经过测试的版本或 digest,持续更新与扫描漏洞。
  2. 限制运行权限:使用非 root 用户,按需配置资源上限,谨慎开放 Docker socket、宿主机目录或 --privileged。
  3. 分开配置与密钥:普通配置可以版本化,真实密钥放到独立的密钥管理流程中,只发布必要端口。
  4. 验证服务恢复:实现依赖超时与重试、优雅退出,接入健康监测和日志。
  5. 验证数据恢复:为持久数据建立备份,实际演练恢复,确认重建容器后仍能读取原数据。

默认以 root 运行的 Linux Docker daemon 具有很高的宿主机权限,访问其 socket 或加入 docker 用户组通常也意味着接近 root 的能力。共享容器网络只提供连接与一定的隔离,不会自动提供服务认证或流量加密。

参考资料

comments powered by Disqus
使用 Hugo 构建
主题 Stack 由 Jimmy 设计