这一节只做一件事:把“镜像 → 容器 → 输出”这条链路跑通。我们先运行一个执行完就退出的容器,再启动一个持续提供网页的容器。
docker run 实际做了什么下面这条命令看起来只是“运行”,实际包含四个动作:
docker run 等价于常用场景下的 docker create 加 docker start,但一次完成更方便。
Docker 不会为了保持容器存在而额外启动一套完整系统。容器的生命周期跟它的主进程绑定:主进程开始,容器进入运行状态;主进程结束,容器进入退出状态。
容器里的主进程通常是 PID 1。它有两个值得提前知道的职责:接收 Docker 转发的终止信号,以及回收子进程。如果镜像用一层不必要的 shell 包住应用,而 shell 又没有正确转发信号,docker stop 可能无法让应用优雅退出。后面编写 Dockerfile 时会使用 exec 形式的启动配置,减少这类问题。
镜像可以带默认启动命令。alpine:3.22 默认可以运行 shell,而本章在镜像名后写了 echo ...,这会把要执行的命令替换为 echo。因此:
docker run [Docker 参数] 镜像名 [容器命令及其参数]镜像名前的 --rm、-d、-p 由 Docker 解释;镜像名后的 echo 及字符串交给容器进程。参数放错位置时,错误可能来自完全不同的组件。
--rm 表示进程退出后自动删除容器。镜像仍会保留,下一次运行不必重新下载。
$ docker run --rm alpine:3.22 echo "Docker 已经可以工作"
Docker 已经可以工作这行中文不是 Docker 客户端打印的,而是 Alpine 容器里的 echo 进程输出的。命令结束后查看全部容器:
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES列表为空,说明 --rm 生效。再看镜像:
$ docker image ls alpine:3.22
REPOSITORY TAG IMAGE ID CREATED SIZE
alpine 3.22 9234e8fb04c4 ... 8.31MB镜像 ID 和大小可能不同,但应能看到 alpine 与 3.22。
还可以故意运行一个返回非零状态的进程,观察退出码如何成为诊断证据。先理解:Unix 程序通常用 0 表示成功,用非零值表示失败;Docker 记录主进程的最终退出码。
$ docker run --name exit-demo alpine:3.22 sh -c 'echo "准备退出"; exit 7'
准备退出
$ docker inspect --format 'status={{.State.Status}} exit={{.State.ExitCode}}' exit-demo
status=exited exit=7
$ docker rm exit-demo
exit-demo这次没有使用 --rm,因为我们需要保留退出后的容器对象才能检查状态。证据读完再删除,是排错时很重要的顺序。
网页服务需要持续运行。-d 让容器进入后台,--name 给容器一个稳定名字,-p 8080:80 把开发设备的 8080 端口转发到容器的 80 端口。
端口参数的顺序始终是:
外部访问端口:容器监听端口EXPOSE 80 只是镜像元数据,不会自动把端口开放出来。真正的发布动作来自 -p 或 Compose 的 ports。
前台运行时,当前终端会连接容器的标准输入、标准输出和标准错误;按 Ctrl+C 往往会把中断信号发送给容器主进程。后台运行时,客户端取得容器 ID 后返回,容器仍由 Docker 管理,输出可以通过 docker logs 读取。
-d 不会把一个短命令变成长服务。下面的容器即使后台启动,也会立即退出,因为 echo 已经完成:
$ docker run -d --name short-job alpine:3.22 echo done
...
$ docker ps -a --filter name=short-job --format '{{.Status}}'
Exited (0) ...
$ docker rm short-job
short-jobnginx 在容器的 network namespace 中监听 80。访问者连接的是外部网络空间中的 8080。Docker 配置转发规则,把进入已发布端口的流量送到容器。二者不要求数字相同,8080:80、9000:80 都可以指向同一个容器端口。
默认写成 -p 8080:80 时,端口通常会绑定到所有接口;如果服务只应供当前设备访问,可以显式限制为回环地址:
$ docker run -d --name private-web -p 127.0.0.1:8081:80 nginx:alpine这时当前设备可访问 127.0.0.1:8081,其他设备不能直接通过该映射进入。安全边界不能只靠应用“应该不会被访问”,而要在发布端口时明确。
$ docker run -d --name course-web -p 8080:80 nginx:alpine
8c8f3d7a8f2a...Docker 返回容器 ID。现在查看正在运行的容器:
$ docker ps --filter name=course-web
CONTAINER ID IMAGE STATUS PORTS NAMES
8c8f3d7a8f2a nginx:alpine Up 5 seconds 0.0.0.0:8080->80/tcp course-web请求网页:
$ curl -I http://localhost:8080
HTTP/1.1 200 OK
Server: nginx
Content-Type: text/html200 OK 证明请求经过了四段路径:curl → 8080 端口 → 容器 80 端口 → nginx。如果浏览器打开 http://localhost:8080,也会看到 nginx 欢迎页。
nginx 会把访问记录写到标准输出,因此可以直接查看:
$ docker logs course-web
... "HEAD / HTTP/1.1" 200 ...停止容器,再删除它:
$ docker stop course-web
course-web
$ docker rm course-web
course-web检查结果:
$ docker ps -a --filter name=course-web
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESdocker stop 先向主进程发送终止信号,并给它留出优雅退出时间。docker kill 默认直接发送强制终止信号,通常只在进程无法正常退出时使用。
stop 背后的信号过程对 Linux 容器而言,docker stop 通常先向 PID 1 发送 SIGTERM,等待宽限期;如果进程仍未退出,再发送 SIGKILL。前者可以被应用捕获,用来停止接收请求、刷新缓冲区和关闭连接;后者不能被捕获,会立刻终止进程。
因此生产应用需要处理终止信号,Dockerfile 也要避免信号被错误的 shell 层截断。看到 Exited (137) 时,还要考虑进程是否收到 SIGKILL,而不能只说“Docker 自己崩了”。退出码与信号的对应关系会在排错章节继续展开。
最后一条尤其重要:端口转发存在,不等于服务存在。如果 curl 得到连接重置或超时,应继续检查容器状态、日志以及容器内进程监听的地址和端口。
完成本章后,你应该能解释一次 200 OK 的完整证据链:容器主进程仍在运行,nginx 监听容器端口 80,Docker 发布外部端口 8080,请求被转发后由 nginx 返回响应。