刚接触 Docker 那阵子,我最常干的事就是把docker run的长参数串反复复制到笔记里,等真到要创建容器时才发现,要么端口映射漏了、要么数据目录没挂载、要么容器一重启数据全没。这篇笔记就是围绕“Docker 创建容器”这件事,把我自己踩过的坑和常用命令整理成一套完整思路:先搞清楚镜像和容器的关系,再弄明白创建容器的几种方式,然后把参数逐项拆开讲,最后给几个高频实战场景和常见报错的排查方法。适合刚装好 Docker 但还不知道怎么把镜像跑成容器的人,也适合已经会docker run但想系统补全参数细节的人。
1. 从核心思路说起:为什么创建容器要先搞懂“镜像与容器”的关系
1.1 docker run 到底干了什么
很多人第一次用docker run nginx看到镜像被下载、容器直接能访问时,会觉得 Docker 就是个“一键装软件”的工具。但实际创建容器的过程,比你看到的要复杂一点。
一条docker run命令背后,Docker 会做四件事:
- 检查本地有没有指定镜像,没有就自动去仓库拉取
- 基于镜像创建一个可写的容器层,相当于把镜像变成“一个正在运行的实例”
- 分配网络、挂载卷、设置环境变量等运行时配置
- 执行镜像里预设的启动命令,比如 MySQL 镜像里的
mysqld
理解了这四步,你再看创建容器的命令,就不会把参数当成“照抄的咒语”了。比如-p 3306:3306是网络层面的配置,-v /data:/var/lib/mysql是存储层面的配置,-e MYSQL_ROOT_PASSWORD=123456是环境变量层面的配置,它们分别在容器的不同生命周期阶段生效。
1.2 创建容器的四种常见方式
我在实际使用中,创建容器基本只用到四种方式,各有适用场景:
docker create:只创建容器但不启动,适合先准备好容器、稍后再启动,或者需要批量创建的场景docker run:创建并立即启动,这是用得最多的方式docker compose up:通过 YAML 文件创建并启动多个容器,适合多服务编排- 先
docker create再docker start:适合调试阶段,创建后先检查配置,确认没问题再启动
这四种方式本质都是“由镜像生成容器”,区别只在启动时机和管理粒度。单容器调试用docker run,多服务部署用 compose,需要精细控制启动时机就用docker create加docker start。
2. 实战准备:Docker 安装与基础环境检查
2.1 安装 Docker Desktop 还是纯 Docker Engine
创建容器之前,你得先有一个能用的 Docker 环境。现在主流两种选择:Windows/Mac 上用 Docker Desktop,Linux 上装 Docker Engine。
Docker Desktop 自带图形界面,还能在设置里方便地调整资源配额,比如给 Docker 分配多少内存和 CPU。但新手最容易卡在 Docker Desktop 的启动上,因为它依赖系统的虚拟化支持。如果你在 Windows 上遇到 “Docker Desktop failed to start because virtualisation support wasn't detected” 这类报错,通常不是 Docker 本身的问题,而是 BIOS 里的虚拟化没开。
我的建议是:Windows 用户优先用 Docker Desktop,遇到启动失败先开 BIOS 的虚拟化;Linux 用户直接装 Docker Engine,命令方式更纯粹,也更好排查问题。
Linux 上安装 Docker Engine 最简单的方式是用官方脚本:
curl -fsSL https://get.docker.com -o get-docker.sh sudo sh get-docker.sh脚本会帮你配好软件源、把 Docker 装好并设置开机自启。装完后可以用docker --version验证。Windows 上则直接下载 Docker Desktop 安装包,按提示走完向导,装完一般需要重启或至少退出重进一次,让 Docker 的虚拟化服务生效。
2.2 基础状态检查:别一上来就 run
我见过不少新手,Docker 刚装完就急着docker run,结果报一堆错,分不清是镜像问题还是环境问题。正确的顺序是先做三个基础检查:
# 检查 Docker 版本 docker --version # 检查 Docker 服务是否正常,能显示出系统信息的输出 docker info # 检查当前有哪些容器在跑 docker psdocker info很值得养成习惯。如果这里就报错,docker run大概率也会挂。常见的失败情况有:当前用户不在 docker 用户组里(Linux 下会提示权限不足)、Docker Desktop 虚拟机没启动、磁盘空间不够。
2.3 镜像准备:先看本地有没有,再考虑拉取
创建容器前,确认镜像是否存在是个好习惯。我用 Docker 这些年总结出一条经验:尽量显式指定镜像版本,别默认用 latest。
# 查看本地已有的镜像 docker images # 拉取指定版本镜像,比如 MySQL 8.0 docker pull mysql:8.0 # 拉取最新版 docker pull nginx:latest为啥我强调指定版本?latest是个移动标签,今天拉到的镜像和三个月后拉到的可能完全不同。你写博文、做项目、部署生产环境时,版本不固定等于给自己埋雷。比如安装 MySQL 8.0 时,我通常直接docker pull mysql:8.0,这样后面创建容器时用的是确定的版本,排查问题也有据可依。
3. 创建容器的核心命令详解
3.1 docker create:只建不跑的场景
docker create字面意思就是“创建容器”,但执行完容器处于 created 状态,不会启动。我第一次用这个命令时觉得有点鸡肋,后来发现它在两个场景下特别好用:
- 需要预先分配一个固定名称或固定 IP 的容器,但不希望它马上运行
- 想先创建容器、检查配置再启动,方便调试
命令格式如下:
docker create --name my-nginx -p 8080:80 nginx:latest后面接的参数和docker run完全一样,区别就是最后不自动启动。创建完可以用docker start my-nginx把容器跑起来。如果你拿不准一个容器怎么配参数,用docker create先创建,再用docker inspect检查配置,比直接docker run翻车了再改要安全。
3.2 docker run:最常用的一步到位
docker run是所有创建容器命令里最核心的。基本语法是:
docker run [OPTIONS] IMAGE [COMMAND] [ARG...]一个最朴素的示例:
docker run --name web -d -p 8080:80 nginx:latest这条命令的拆解是:容器名叫 web,后台运行(-d),把宿主机的 8080 端口映射到容器内的 80 端口,用 nginx 镜像创建并启动。
这里有个容易忽略的细节:[COMMAND]这部分可以覆盖镜像默认的启动命令。比如镜像里默认启动 nginx,你想只启动一个 bash 做调试,就可以这样:
docker run -it --name debug nginx:latest bash-it是-i和-t的组合,-i保持标准输入打开,-t分配一个伪终端。组合起来你就能像 SSH 到一台机器一样直接进入容器的 shell。注意这种方式进去后,容器里只是个 bash,nginx 服务并不会启动,适合排查问题,不适合正式跑服务。
3.3 常用参数逐项拆解
创建容器时,参数选得对不对,直接决定你后面用起来顺不顺。我把高频参数按用途分成了几组。
资源限制相关的:
-m或--memory:限制容器最大内存,比如-m 512m--cpus:限制容器可用 CPU 数,比如--cpus=1.5--restart:容器退出时的重启策略,always表示只要 Docker 服务在,容器挂了就自动拉起
网络相关的:
-p 宿主机端口:容器端口:端口映射-P:随机映射容器内所有暴露的端口--network:指定网络模式,比如--network host直接用宿主机网络--ip:指定容器 IP,需要配合自定义网络使用
存储相关的:
-v 宿主机目录:容器目录:数据卷挂载--mount type=bind,source=宿主机目录,target=容器目录:另一种挂载语法,更喜欢用这个,因为语义更清晰
环境与交互相关的:
-e或--env:设置环境变量--env-file:从一个文件读取环境变量-it:交互式终端-d:后台运行--name:指定容器名称
这么多参数,新手最容易混乱的是-v的挂载写法。它的核心逻辑是:宿主机目录在冒号左边,容器目录在冒号右边。比如把宿主机的/home/myapp/data挂到容器的/var/lib/mysql,就写-v /home/myapp/data:/var/lib/mysql。
3.4 端口映射、数据卷、网络这三个关键点
创建容器为什么要把这三样单独拎出来?因为它们分别对应容器使用时的三个老大难问题:怎么访问、数据怎么保存、容器之间怎么通信。
端口映射的问题在于:宿主机端口不能重复。你想创建两个 nginx 容器,宿主机 80 端口只能给一个用,另一个就得换8080或者别的。养成“端口冲突先查docker ps看看自己开了哪些端口”的习惯,能少踩很多坑。
数据卷的问题在于:容器删除后,容器层的数据就没了。如果你跑一个 MySQL 容器,不加-v挂载数据目录,容器一删,数据库就没了。我见过很多次这种惨案。解决方案就是挂载数据卷,让数据存在宿主机上,和容器生命周期解耦。
网络的问题在于:多个容器之间怎么互相访问。最简单的方式是用--link,但那是老古董了,现在推荐先创建一个自定义网络,然后让容器加入同一个网络,互相用容器名通信:
docker network create mynet docker run --name mysql8 --network mynet -e MYSQL_ROOT_PASSWORD=123456 -d mysql:8.0 docker run --name myapp --network mynet -d my-webapp:v1这样myapp容器里访问数据库,直接用mysql8:3306就行,不需要关心 MySQL 容器的 IP 是多少。
4. 容器创建后的生命周期管理
4.1 启动、停止、重启和查看状态
创建完容器,后续管理基本围绕生命周期操作展开。常用命令不多,但每个都很重要:
# 查看运行中的容器 docker ps # 查看所有容器,包括已经停止的 docker ps -a # 启动一个已存在的容器 docker start 容器名或ID # 停止容器 docker stop 容器名或ID # 重启容器 docker restart 容器名或ID这里我多说一句:docker stop是优雅停止,会先给容器发 SIGTERM 信号,等一段时间再发 SIGKILL。如果你想让容器立刻死掉,就用docker kill。日常操作里,stop基本够用,kill一般只在容器卡死时用。
docker ps -a这个命令很重要。你刚才用docker run创建了容器,但是docker ps看不到,多半是容器启动后立刻退出了。这时候docker ps -a就能看到这个容器的状态是 Exited,再配合docker logs查看退出原因。
4.2 进入容器、执行命令、查看日志
进入容器有两种常见方式,我都经常用:
# 方式一:直接在容器里开一个交互式终端 docker exec -it 容器名 bash # 方式二:执行单条命令,不进入交互环境 docker exec 容器名 mysql --versionexec和run的-it区别很多人会混。docker exec -it 容器名 bash是在一个已经运行的容器里开新进程,而docker run -it是创建新容器并进入。前者你进入的是“当前这台运行中的机器”,后者你是“从镜像重新开一台机器”。
容器日志查看也有固定套路:
# 查看全部日志 docker logs 容器名 # 实时跟踪日志输出,按 Ctrl+C 退出 docker logs -f 容器名 # 看最后 100 行日志 docker logs --tail 100 容器名我排查问题时,几乎都是docker logs 容器名 | tail -50这种组合,先把最近报错捞出来,再逐行分析。日志是容器给外界的唯一“发言渠道”,很多启动失败的原因都藏在日志尾部。
4.3 删除容器与清理
删除容器这个操作,一定要和数据卷搞明白关系。
# 删除一个停止的容器 docker rm 容器名 # 强制删除运行中的容器 docker rm -f 容器名 # 删除容器时同时删除它的匿名数据卷 docker rm -v 容器名如果你用了-v显式挂载宿主机目录,容器删除后宿主机目录里的数据还在。但如果你创建容器时没挂载数据卷,容器写入的数据会存在匿名数据卷里,docker rm默认不会删除这些卷,不手动清理会一直占磁盘。
常用的清理命令:
# 清理所有停止状态的容器 docker container prune # 清理所有悬空镜像(没有标签且没有被容器引用的镜像) docker image prune # 一键清理容器、网络、悬空镜像、构建缓存 docker system prunedocker system prune很猛,会把你不要的镜像缓存全部清掉。我第一次跑这个命令时手一抖把本地一堆没用但以后可能用到的镜像都清了,后来学乖了:先docker system prune -a前看清楚提示,别盲目加-a。
5. 实操案例:MySQL 8.0、Redis 主从、青龙依赖管理
5.1 MySQL 8.0 创建容器全流程
MySQL 容器是我创建最多的容器类型之一。网络热搜词里“docker安装mysql失败”“docker安装mysql8.0并使用”出现频率很高,说明很多人卡在创建这一步。我完整走一遍流程。
第一步,拉取镜像并创建数据目录:
docker pull mysql:8.0 mkdir -p /data/mysql第二步,创建容器:
docker run --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=MyPass@123 \ -v /data/mysql:/var/lib/mysql \ -v /data/mysql-conf:/etc/mysql/conf.d \ --restart=always \ -d mysql:8.0这一步有几个关键点:
MYSQL_ROOT_PASSWORD是初始化时设置 root 密码的环境变量/var/lib/mysql是 MySQL 的数据目录,必须挂载出来,否则删容器等于删数据库/etc/mysql/conf.d挂载配置文件目录,这样可以把自己写的my.cnf放进去,比如修改字符集
第三步,验证容器和数据库是否正常:
docker ps docker logs mysql8 | tail -20 docker exec -it mysql8 mysql -uroot -p如果提示Can't connect to local MySQL server through socket这类错误,多半是mysqld没起来或初始化没完成。docker logs mysql8里能看到“ready for connections”说明初始化成功了。初始化过程一般需要几十秒,刚创建完别急着连。
第四步,访问容器内的 MySQL。如果是在宿主机上连,直接mysql -h127.0.0.1 -P3306 -uroot -p就行。如果是在另一个容器里访问,就要像前面说的,把两个容器放在同一个自定义网络里,用容器名做主机名。
我踩过的坑:MySQL 8.0 默认认证插件是caching_sha2_password,老客户端工具可能连不上,会报认证错误。如果你非要用老客户端,得在创建容器时加一条参数--default-authentication-plugin=mysql_native_password,但这种方式在新版本 MySQL 里已经逐渐移除了,更推荐升级客户端去兼容新插件。
5.2 Redis 主从复制容器创建
Redis 做主从,Docker 创建容器时最方便的方式是:先启动一个主节点,再启动从节点并指定主节点地址。
主节点:
docker run --name redis-master \ -p 6379:6379 \ -v /data/redis-master:/data \ -d redis:6.0 \ redis-server --appendonly yes从节点:
docker run --name redis-slave \ -p 6380:6379 \ --network mynet \ -v /data/redis-slave:/data \ -d redis:6.0 \ redis-server --appendonly yes --slaveof redis-master 6379这里注意,从节点要能解析到主节点,必须和主节点在同一个 Docker 网络里。--slaveof redis-master 6379的参数里,redis-master是主节点的容器名,Docker 内置 DNS 会把它解析成主节点容器的 IP。
验证主从是否成功:
docker exec -it redis-master redis-cli info replication docker exec -it redis-slave redis-cli info replication主节点输出里connected_slaves:1,从节点输出里role:slave且master_link_status:up,就说明主从搭建成功。
这个场景里最容易出的问题是:从节点刚起来时日志报MASTER <-> REPLICA sync started,然后看到Master did not respond to PING这种错误。绝大多数情况都是网络不对,要么两个容器不在同一个自定义网络,要么端口映射绑定的地址不对。
5.3 青龙面板的依赖管理
青龙(qinglong)是一个跑定时脚本的常见容器应用,很多人在“依赖管理”上吃过亏。创建青龙容器本身不复杂:
docker run --name qinglong \ -p 5700:5700 \ -v /data/qinglong:/ql/data \ -d whyour/qinglong:latest创建成功后在浏览器打开http://宿主机IP:5700,按提示初始化账号密码就行。但青龙跑脚本时经常缺系统依赖,比如 Python 包、Node 包、Linux 库。很多人创建容器后,直接进容器手动apk add或pip install,然而容器一重建,这些全没了。
我的做法是:把依赖安装命令固化到容器启动后的初始化脚本里,或者用额外的-v挂载把依赖缓存目录也映射出来。比如让 pip 缓存留在宿主机:
-v /data/qinglong/pip-cache:/root/.cache/pip这样即使容器重建,重新安装依赖时还能命中宿主机上的缓存,下载速度提升明显。依赖管理这件事,关键思路是:容器的文件系统是临时的,任何要持久化的东西都要靠挂载。
我见过有人直接把整个/ql/data目录挂出来,也包括了依赖所在路径,这样容器重建后脚本依赖还在。但因为依赖跟运行环境强相关,系统镜像一升级,原来的依赖还是可能失效,所以更推荐在容器初始化脚本里声明依赖,实现可重复安装。
6. 常见问题与排查技巧实录
6.1 Docker Desktop 启动失败:虚拟化未开启
很多 Windows 用户装完 Docker Desktop,双击启动就弹窗“Docker Desktop failed to start because virtualisation support wasn't detected”。这个报错信息我在热搜词里看到也是高频。原因基本只有一个:BIOS 里的 CPU 虚拟化没开。
排查顺序:
- 打开任务管理器,切到“性能”选项卡,看“虚拟化”字段是“已启用”还是“已禁用”
- 如果已禁用,重启电脑进 BIOS,找到 Intel VT-x 或 AMD-V 选项并启用
- 保存退出后重新启动 Docker Desktop
Windows 系统还需要保证“Windows 虚拟机监控程序平台”和“适用于 Linux 的 Windows 子系统”这两个功能是开着的。可以在控制面板的“启用或关闭 Windows 功能”里勾选,然后重启。
我有个朋友就是卡在这一步,最后发现是公司电脑的 BIOS 被统一锁了虚拟化,找 IT 解锁才解决。如果你也遇到这种情况,可以先检查是不是系统层面的权限策略挡着。
6.2 创建 MySQL 容器失败排查思路
MySQL 容器在创建阶段失败的几个常见原因,我整理成一个排查顺序:
docker logs mysql8看日志,如果出现data directory ... is not empty,说明挂载的数据目录里已经有数据,和镜像初始化逻辑冲突- 端口被占用,报错里会出现
port is already allocated,用docker ps看看是不是已有容器占用了 3306 - 磁盘空间不足,拉取镜像或容器写入数据时都会报
no space left on device - 权限问题,宿主机挂载目录的属主不对,MySQL 写不进去,日志里常见
Permission denied
遇到 MySQL 初始化失败,我基本不看网上杂七杂八的文章,直接照着日志一步步排查。容器技术的好处就是问题会直接暴露在日志里,比原生安装好定位得多。
另外有一个非常容易忽略的点:--restart=always加上后,容器每次退出都会被 Docker 拉起来,如果服务一直崩溃,容器会反复重启。此时先用docker inspect看容器的RestartCount,能帮你判断是不是真的“反复崩溃”。
6.3 容器网络不通的排查套路
“docker网络不通”是热搜词里的高频词,我分享一个自己的排查方法清单:
# 先看容器是否在一个网络里 docker inspect 容器名 | grep -i network # 查看容器 IP docker inspect -f '{{.NetworkSettings.IPAddress}}' 容器名 # 进入容器测试网络连通性 docker exec -it 容器名 ping 目标IP # 查看当前所有网络 docker network ls最常见的网络问题原因,是宿主机网络模式(--network host)加了之后,端口映射-p就不生效了。host 模式下容器直接用宿主机网络,不需要也不能用端口映射。如果你用 host 模式发现访问不了容器服务,优先想想是不是端口绑定的问题,而不是继续加参数。
如果容器之间要互相访问,默认的 bridge 网络其实也能通信,但需要靠 IP,容器重建后 IP 会变。更推荐的方式就是前面说过的自定义网络,容器名即主机名,稳定省心。
6.4 时区、编码和文件权限的隐藏坑
容器创建完成后,经常遇到时区不对的问题。很多官方镜像默认时区是 UTC,和北京时间差 8 小时。如果你在容器里跑定时任务或者看日志时间,会非常别扭。
解决办法是在创建容器时加一条时间挂载,或者用环境变量:
# Linux 下推荐直接挂载时区文件 -v /etc/localtime:/etc/localtime:ro # 或者用环境变量,部分镜像支持 TZ -e TZ=Asia/Shanghai有些镜像支持TZ环境变量,有些不支持,所以-v /etc/localtime这种挂载方式更通用。Windows 上因为没有/etc/localtime,通常只能靠TZ环境变量。
文件权限的问题则更多出现在挂载目录的时候。容器里的进程以 root 身份运行没问题,但如果你的宿主机目录权限太严,容器里创建的文件会写到宿主机会变成 root 所有,宿主机普通用户没法改。经常见到宿主机上没法删容器生成的文件的问题,就是这个原因。
6.5 创建容器时的常用排查命令速查
我把日常排查创建容器故障的高频命令整理成一张速查表,方便照着敲:
| 目标 | 命令 |
|---|---|
| 查看容器详细配置和状态 | docker inspect 容器名 |
| 查看容器启动以来重启的次数 | docker inspect -f '{{.RestartCount}}' 容器名 |
| 查看容器 CPU 和内存占用 | docker stats 容器名 |
| 查看容器日志,尾部 100 行 | docker logs --tail 100 容器名 |
| 查看所有容器(含停止) | docker ps -a |
| 查看容器的挂载卷和网络配置 | docker inspect 容器名 -f '{{json .Mounts}}' |
| 查看容器的环境变量 | docker inspect 容器名 -f '{{json .Config.Env}}' |
| 进入容器交互界面 | docker exec -it 容器名 bash |
我排查创建容器故障时,固定套路就是:docker ps -a看状态,docker logs看日志,docker inspect看配置。这三步走完,八成问题都能定位到。
7. 个人经验总结与建议
最后分享几点我自己的体会。创建容器这件事,核心其实不是“记命令”,而是理解容器从镜像到运行时的过程。docker run后面的参数本质上是你在回答三个问题:容器怎么被访问、数据怎么存、和谁怎么通信。把这三个问题想清楚,参数自然就记住了。
另外,我特别建议把“创建容器的命令”用脚本或 Makefile 管理起来。比如在项目里放一个run.sh,把docker run的长命令写进去,这样下次重建容器时不用翻历史记录,直接跑脚本就行。自己写脚本时顺便把版本号写死,不要用 latest,时间久了你就懂这个习惯有多重要了。
最后再分享一个调试小技巧:当你拿不准某条docker run命令会创建出什么容器时,可以先用docker create代替docker run,创建完用docker inspect看看配置对不对,再手动docker start。等确认无误后,再放回正式脚本里用。这样能少踩很多因为参数写错导致的返工坑。
如果你感觉创建容器这台词里最麻烦的还是数据卷挂载,就拿 MySQL 和 Redis 各练一遍,把容器删了再重建,数据都还在,你就真的理解了“数据卷解耦容器生命周期”这句话。