1. 项目概述:从镜像到容器的核心操作闭环
在上一篇文章里,我们聊了Docker的基础概念和安装,算是把“地基”打好了。今天咱们直接上干货,聊聊每天都会用到的那些核心命令。如果把Docker比作一个现代化的物流仓库,那么镜像(Image)就是标准化的、封装好的货物模板,而容器(Container)就是根据这个模板实际跑起来的、一个个独立的“包裹”或“运行实例”。我们日常开发、测试、部署,绝大部分时间都在和“创建镜像”、“启动容器”、“管理容器”这几件事打交道。命令看似简单,但里面的门道和“坑”可不少,比如镜像构建的优化、容器启动的姿势、数据如何持久化、网络怎么配置,这些细节直接决定了你的应用跑得稳不稳、快不快。这篇文章,我就以一个过来人的身份,带你把这些常用命令掰开了、揉碎了讲清楚,不仅告诉你命令怎么写,更重点分享我踩过的坑和总结的最佳实践,让你真正能用起来,而不是仅仅停留在“知道”的层面。
2. 核心基石:镜像(Image)的创建与管理
镜像是容器运行的蓝图。一个优秀的镜像应该是轻量、安全、可复现的。创建镜像主要有两种方式:docker commit和Dockerfile。前者适合临时保存状态,后者才是生产环境的黄金标准。
2.1 通过Dockerfile构建镜像:最佳实践详解
Dockerfile是一个文本文件,包含了一系列构建镜像的指令。使用docker build命令是创建自定义镜像最主流、最推荐的方式。
基本命令格式:
docker build -t <镜像名:标签> <构建上下文路径>例如,在当前目录构建一个名为myapp,标签为v1.0的镜像:
docker build -t myapp:v1.0 .这里的.代表当前目录作为“构建上下文”,Docker守护进程会把这个目录下的所有文件(注意:受.dockerignore文件影响)打包发送给Docker引擎用于构建。
一个完整的、包含优化技巧的Dockerfile示例:
# 第一阶段:构建阶段 FROM golang:1.19-alpine AS builder WORKDIR /app # 复制go模块文件,利用Docker缓存层,避免依赖变更时重复下载 COPY go.mod go.sum ./ RUN go mod download COPY . . # 静态链接,减少运行时对基础镜像的依赖 RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . # 第二阶段:运行阶段 - 使用极简镜像 FROM alpine:latest RUN apk --no-cache add ca-certificates tzdata WORKDIR /root/ # 从builder阶段复制编译好的二进制文件 COPY --from=builder /app/main . # 创建一个非root用户运行应用,增强安全性 RUN addgroup -g 1001 -S appgroup && adduser -u 1001 -S appuser -G appgroup USER appuser EXPOSE 8080 CMD ["./main"]注意:构建上下文中不要包含不必要的文件(如
.git,node_modules, 日志文件),这会导致构建上下文过大,拖慢构建速度。务必使用.dockerignore文件来排除它们,其语法类似于.gitignore。
实操心得与避坑指南:
- 利用多阶段构建:如上例所示,多阶段构建能显著减小最终镜像的体积。你在第一阶段(
builder)可以安装完整的编译工具链,生成可执行文件。在第二阶段,你仅需一个极简的运行环境(如alpine,scratch)和那个可执行文件。最终镜像里不会有编译工具和中间文件。 - 理解镜像层缓存:Dockerfile中的每一条指令都会创建一个新的镜像层。Docker会缓存这些层。如果某条指令及其之前的指令没有变化,后续构建会直接使用缓存,极大加速构建。因此,通常将变化频率低的指令(如安装基础软件包)放在前面,变化频率高的指令(如复制应用代码)放在后面。
- 标签管理:始终为镜像打上有意义的标签,如
myapp:v1.0,myapp:latest。latest标签是默认标签,但生产环境应避免依赖它,因为它是流动的。推荐使用语义化版本号或Git提交哈希作为标签。
2.2 通过docker commit创建镜像:应急之选
docker commit命令将一个运行中或已停止的容器,提交为一个新的镜像。这通常用于保存容器的当前状态,或者从一个运行中的容器快速创建一个用于调试的镜像。
命令格式:
docker commit [选项] <容器名或ID> <新镜像名:标签>例如,将容器my-running-container提交为镜像my-snapshot:debug:
docker commit my-running-container my-snapshot:debug使用场景与严重警告:
- 场景:临时性调试。比如,你在一个基础镜像的容器里安装了一堆调试工具,配置了复杂环境,想把这个状态保存下来下次直接用。
- 警告:切勿将其作为创建生产环境镜像的常规手段!原因如下:
- 不可复现:你无法精确知道容器里到底被修改了什么,失去了Dockerfile带来的声明式和可复现性。
- 臃肿:提交的镜像包含了所有读写层,通常非常臃肿,包含了许多不必要的变更。
- “黑盒”镜像:其他人无法通过Dockerfile了解镜像的构建过程,不利于协作和维护。
2.3 镜像的拉取、查看与清理
- 拉取镜像:从仓库下载镜像到本地。
docker pull nginx:alpine - 列出镜像:
docker images # 或使用更强大的新命令 docker image ls - 删除镜像:删除本地的镜像。如果镜像有容器(无论是否运行)在使用它,需要先删除容器或使用
-f强制删除。docker rmi <镜像ID或镜像名:标签> # 例如 docker rmi myapp:v1.0 # 删除所有悬空镜像(未被任何镜像引用的中间层) docker image prune # 删除所有未被使用的镜像(谨慎!) docker image prune -a
3. 灵魂操作:容器(Container)的生命周期管理
容器是镜像的运行实例。管理容器的生命周期是Docker日常使用的核心。
3.1 启动容器:docker run的学问
docker run命令是功能最复杂的命令之一,它从镜像创建并启动一个新容器。
基础启动:
docker run -d --name my-nginx nginx:alpine-d:后台运行(detached mode)。--name:为容器指定一个易读的名称,否则Docker会分配一个随机名称。
高级启动与关键参数解析:一个更贴近生产需求的启动命令可能长这样:
docker run -d \ --name my-webapp \ --hostname app01 \ --restart unless-stopped \ -p 8080:80 \ -v /host/path/data:/container/path/data:rw \ -v /host/path/config:/container/path/config:ro \ --env-file ./prod.env \ --memory="512m" \ --cpus="1.0" \ --network my-bridge-network \ myapp:v1.0让我们拆解这些关键选项:
- 端口映射 (
-p):-p 8080:80将主机的8080端口映射到容器的80端口。格式是主机端口:容器端口。 - 数据卷挂载 (
-v): 实现数据持久化和主机-容器文件共享。-v /host/data:/container/data:rw:将主机目录/host/data挂载到容器的/container/data,权限为读写。-v /host/config:/container/config:ro:挂载为只读(ro),保护配置文件不被容器修改。- 强烈建议使用命名卷(Named Volume)或绑定挂载(Bind Mount)的明确路径,避免使用匿名卷,便于管理。
- 环境变量 (
-e,--env-file):-e KEY=VALUE:设置单个环境变量。--env-file ./prod.env:从文件加载所有环境变量,更安全便捷,避免在命令行中暴露敏感信息。
- 资源限制 (
--memory,--cpus): 为容器设置内存和CPU限制,防止单个容器耗尽主机资源,这是多租户和生产环境的基本要求。 - 重启策略 (
--restart):no:默认,容器退出时不重启。on-failure[:max-retries]:非正常退出时重启,可指定最大重试次数。always:总是重启(包括手动停止后,如果守护进程重启,它也会被拉起)。unless-stopped:总是重启,除非用户明确执行docker stop停止了它。这是生产环境最常用的策略,能保证服务高可用。
- 网络 (
--network): 指定容器加入的Docker网络,实现容器间隔离或通信。默认是bridge网络。
提示:使用
docker run --help可以查看所有选项的详细说明。对于复杂的配置,考虑使用Docker Compose来管理,更为清晰和可维护。
3.2 容器的查看、停止与删除
- 列出容器:
docker ps # 查看运行中的容器 docker ps -a # 查看所有容器(包括已停止的) - 停止容器:
优先使用docker stop <容器名或ID> # 发送SIGTERM信号,允许优雅退出 docker kill <容器名或ID> # 发送SIGKILL信号,强制立即终止docker stop,给应用预留清理资源的时间。 - 启动/重启已存在的容器:
docker start <容器名或ID> docker restart <容器名或ID> - 删除容器:
docker rm <容器名或ID> # 删除已停止的容器 docker rm -f <容器名或ID> # 强制删除运行中的容器(慎用) # 删除所有已停止的容器(清理空间常用) docker container prune
3.3 进入容器与执行命令:运维与调试的关键
当需要排查问题、查看日志或执行临时命令时,我们需要与运行中的容器交互。
- 附着到容器 (
docker attach): 连接到容器的主进程(PID 1)的标准输入、输出和错误。一个重要的警告:如果你在附着后输入Ctrl+C,会发送SIGINT信号给主进程,通常会导致容器停止。退出附着默认也是Ctrl+P, Ctrl+Q组合键,不太直观。因此,attach通常仅用于查看实时输出流,不用于交互。 - 在容器内执行命令 (
docker exec):这是最常用、最安全的交互方式。它会在运行中的容器内启动一个新的进程。# 在容器内启动一个交互式的bash shell docker exec -it my-nginx /bin/bash # 在容器内执行一条命令并查看结果 docker exec my-nginx ls -la /usr/share/nginx/html-i:保持标准输入打开,允许交互。-t:分配一个伪终端(pseudo-TTY),使shell会话看起来更自然。-it通常一起使用,用于启动交互式Shell。
- 查看容器日志 (
docker logs): 获取容器主进程的标准输出和错误,是调试的利器。docker logs my-nginx # 查看全部日志 docker logs -f my-nginx # 实时跟踪日志(类似 tail -f) docker logs --tail 50 my-nginx # 查看最后50行 docker logs --since 10m my-nginx # 查看最近10分钟的日志
实操心得:
- 调试首选
docker exec -it:需要进入容器排查时,永远优先考虑exec。它独立于主进程,即使你在这个Shell里误操作导致Shell崩溃,也不会影响容器主进程的运行。 - 日志驱动配置:默认的日志驱动是
json-file,日志会存储在主机上,可能占用大量磁盘。对于生产环境,可以考虑配置日志轮转(log-rotation)或使用其他驱动(如journald,syslog),并通过docker run的--log-opt参数进行控制。 - 使用
docker inspect:这个命令能获取容器(或镜像、网络等)的底层详细信息,包括配置、状态、网络设置、挂载点等,格式为JSON。当出现网络不通、挂载失败等复杂问题时,docker inspect <容器名>是你最好的朋友。
4. 实战演练:一个Web应用的完整生命周期
让我们通过一个简单的Node.js Web应用,串联起从构建镜像到运行、管理、最终清理的完整流程。
4.1 准备应用代码
创建一个项目目录,例如my-node-app。
package.json:{ "name": "my-node-app", "version": "1.0.0", "description": "A simple Node.js app", "main": "server.js", "scripts": { "start": "node server.js" }, "dependencies": { "express": "^4.18.2" } }server.js:const express = require('express'); const app = express(); const PORT = process.env.PORT || 3000; app.get('/', (req, res) => { res.send('Hello from Docker!'); }); app.listen(PORT, () => { console.log(`Server is running on port ${PORT}`); });.dockerignore:node_modules npm-debug.log .git .DS_Store
4.2 编写并优化 Dockerfile
在项目根目录创建Dockerfile:
# 使用官方Node.js运行时作为父镜像 FROM node:18-alpine AS builder WORKDIR /usr/src/app # 复制包管理文件 COPY package*.json ./ # 安装依赖(利用缓存层) RUN npm ci --only=production # 第二阶段:运行阶段 FROM node:18-alpine WORKDIR /usr/src/app # 从构建阶段复制node_modules和依赖 COPY --from=builder /usr/src/app/node_modules ./node_modules # 复制应用源代码 COPY . . # 声明运行时容器监听的端口 EXPOSE 3000 # 定义环境变量 ENV NODE_ENV=production # 运行应用 USER node CMD ["node", "server.js"]这个Dockerfile使用了多阶段构建,并且先复制package.json安装依赖,充分利用了Docker的层缓存机制。
4.3 构建、运行与管理
构建镜像:
docker build -t my-node-app:v1 .观察输出,理解层的构建和缓存使用。
运行容器:
docker run -d \ --name my-running-app \ --restart unless-stopped \ -p 3000:3000 \ -e PORT=3000 \ my-node-app:v1此时,在浏览器访问
http://localhost:3000应该能看到 “Hello from Docker!”。查看状态与日志:
docker ps # 确认容器正在运行 docker logs my-running-app # 查看启动日志 docker stats my-running-app # 实时查看容器资源占用(CPU、内存)进入容器调试:
docker exec -it my-running-app /bin/sh # 在容器内执行 # ls -la # ps aux # exit更新应用:修改
server.js中的返回信息,重新构建镜像(标签改为v2),停止旧容器并运行新容器。# 修改代码后... docker build -t my-node-app:v2 . docker stop my-running-app docker rm my-running-app docker run -d --name my-running-app-v2 -p 3000:3000 my-node-app:v2清理:
docker stop my-running-app-v2 docker rm my-running-app-v2 docker rmi my-node-app:v1 my-node-app:v2 docker image prune # 清理悬空镜像
5. 常见问题与排查技巧实录
在实际操作中,你一定会遇到各种问题。这里记录了几个最常见的问题和我的排查思路。
5.1 容器启动后立即退出
这是新手最常遇到的问题。容器的主进程(CMD或ENTRYPOINT指定的命令)一结束,容器就退出了。
排查步骤:
- 查看退出代码:
docker ps -a会显示容器的状态和退出代码。非0代码通常意味着进程出错。 - 查看日志:第一时间运行
docker logs <容器名>,主进程的错误输出会在这里。 - 常见原因:
- 应用本身启动失败:检查应用日志,可能是配置文件错误、依赖缺失、端口冲突等。
- CMD命令错误:Dockerfile中的CMD命令路径或参数不对。例如,
CMD ["node", "server.js"]但server.js文件不存在。 - 前台运行:确保你的应用是前台运行的。如果你启动的是一个像Nginx、Apache这样的服务,它们默认是前台进程。但如果你写了一个脚本,最后以
&放入后台,或者启动了像npm start(某些配置下可能不是严格前台),容器会立即退出。解决方案是确保启动的进程是PID 1,并且不会退出。
一个快速调试技巧:如果怀疑是应用启动问题,可以先用交互模式启动一个临时容器,手动执行命令看看。
# 使用-it并覆盖默认的CMD,启动一个bash docker run -it --rm my-image /bin/bash # 在容器内手动尝试启动你的应用命令 node server.js5.2 端口绑定失败:Bind for 0.0.0.0:8080 failed: port is already allocated
这个错误说明主机上的8080端口已经被其他进程(可能是另一个Docker容器,也可能是主机上的其他服务)占用了。
解决方案:
- 更换主机端口:将
-p 8080:80改为-p 8081:80。 - 找出占用端口的进程并停止它:
# Linux/Mac sudo lsof -i :8080 # 或 sudo netstat -tulpn | grep :8080 # Windows (在PowerShell或CMD中) netstat -ano | findstr :8080
5.3 容器内无法访问外部网络或其它容器
这通常与Docker的网络配置有关。
排查步骤:
- 检查容器网络模式:
docker inspect <容器名> | grep -A 10 "NetworkSettings"。确认它是否在预期的网络上。 - 检查防火墙:主机防火墙(如firewalld, ufw, Windows Defender防火墙)可能阻止了Docker网桥的流量。可能需要添加规则允许Docker网桥(通常是
docker0)或特定网段的流量。 - 容器间通信:如果两个容器需要通信,最简单的方式是将它们连接到同一个自定义的Docker网络。
在# 创建自定义网络 docker network create my-network # 启动容器时加入该网络 docker run -d --name app1 --network my-network myapp:v1 docker run -d --name app2 --network my-network myapp:v2app1中,现在可以通过容器名app2直接访问app2的服务(Docker内置了DNS解析)。
5.4 数据卷(Volume)权限问题
当你在Linux主机上运行Docker,并将一个主机目录挂载到容器内时,可能会遇到容器内进程没有权限读写该目录的问题。
问题现象:应用日志报“Permission denied”,无法在挂载的目录下创建文件。
原因:容器内进程通常以非root用户(如UID 1000)运行,而主机上的目录可能属于root或其他用户,UID/GID不匹配导致权限拒绝。
解决方案(按推荐度排序):
- 最佳实践:在容器内处理:在Dockerfile中,确保你的应用代码或启动脚本对数据目录有正确的所有权。例如,在启动前用
chown改变目录所有者。或者,使用一个已知的、固定的UID/GID来运行容器进程,并在主机上预先创建具有相同UID/GID的目录。 - 调整主机目录权限(开发环境):在主机上,将目录的权限放宽,例如
chmod 777 /host/data。这很不安全,仅用于本地开发。 - 使用命名卷(Named Volume):Docker管理的命名卷会自动处理权限问题,更适合生产环境的数据持久化。
docker volume create my-data-volume docker run -v my-data-volume:/container/path ...
5.5 镜像构建缓慢或构建上下文过大
现象:docker build命令执行很久,在Sending build context to Docker daemon这一步就卡住。
原因:构建上下文(通常是Dockerfile所在目录)中有大量文件(如node_modules,.git, 构建产物等),这些文件都被打包发送给Docker守护进程。
解决方案:
- 使用
.dockerignore文件:这是必须做的。在Dockerfile同级目录创建.dockerignore,列出需要排除的文件和目录。**/.git **/node_modules **/*.log **/dist **/.env - 优化Dockerfile:将变化最频繁的指令(如
COPY . .)放在最后,最大化利用缓存。 - 使用构建缓存:确保构建环境稳定,避免在构建过程中下载外部资源时因网络问题导致缓存失效。对于公司内部,可以搭建私有镜像仓库和缓存代理。
我个人在经历了多次深夜排查后,养成了一个习惯:任何容器启动异常,首先docker logs;任何构建问题,先看.dockerignore和构建上下文的文件大小;任何网络问题,先docker network ls和docker inspect看网络配置。这些命令组合拳打下来,大部分问题都能快速定位。