1. 动手前先搞清楚:镜像、容器与 docker run 的关系
很多人第一次接触 Docker 都是因为一句话:环境不一致、部署太麻烦。但真到自己敲命令的时候,还是会被docker run那一长串参数劝退。这篇文章不打算讲理论,就是一份我自己平时创建容器用的命令笔记,从最基础的docker run到 MySQL、Redis、Nginx 的实战创建,再到翻车之后的排查思路,全部按实际操作记录整理。
先统一认识一件事:docker run不是“创建一个容器”这么简单。它其实包含三步动作——找到镜像、创建可写容器层、启动容器进程。你写的每个参数,比如-p、-v、--network,本质上都是在给这三步加配置。所以你会看到同一个镜像,有人用一条命令就跑起来了,有人加了一堆参数也能跑起来,区别不在命令长短,而在于运行环境、数据持久化、网络模式这些需求不同。
1.1 把 “镜像” 和 “容器” 的关系想明白,命令才不会乱
我常用一个比喻:镜像是“安装包”,容器是“已安装并正在运行的程序”。镜像本身是只读的,你每次docker run都会在镜像之上叠加一个可写层。对这个容器做的任何修改,比如装软件、改配置、删文件,都只发生在可写层里。一旦容器被删除,这个可写层也跟着没了。
这就引出一个最常见的认知误区:**不要在容器内部做数据持久化,除非你挂载了卷。**默认情况下 MySQL 的数据写在容器里,看似正常,但删掉容器之后数据库全没了。这不是命令的问题,而是你对镜像和容器的关系理解不到位。后面讲-v参数的时候,我会专门强调这一点。
还有一个点常被忽略:docker run如果本地没有镜像,Docker 会尝试从仓库拉取。但我在生产环境一般会先单独执行docker pull,把镜像 tag 确定好再docker run。好处很明显:我拉什么版本、什么 digest 是明确的,不会出现“昨天本地跑通了,今天服务器上拉了个 latest 导致行为不一致”的尴尬。
1.2 创建容器的前置条件:Docker 环境本身要正常
创建容器之前,你得先确认 Docker 服务是在跑的。Linux 上一般执行:
systemctl status docker如果没启动,先systemctl start docker,再设置开机自启systemctl enable docker。Windows/macOS 上一般就是用 Docker Desktop,任务栏图标变绿一般没问题。
这里插一个我见过很多次的坑:Docker Desktop 启动时弹窗提示“virtualization support wasn't detected”或者“virtualization support not detected”,表面上是软件问题,实际是宿主机没有开启 CPU 虚拟化。去 BIOS/UEFI 里找 Intel VT-x(或 AMD SVM)打开,保存重启,Windows 的“启用或关闭 Windows 功能”里把“虚拟机平台”和“适用于 Linux 的 Windows 子系统”勾上,再启动 Docker Desktop 就好了。这不是命令问题,但排查起来容易让人一头雾水。
2. 创建容器的核心命令 docker run 全参数拆解
docker run是我用得最多的命令,没有之一。我把它拆成几个维度来讲:容器运行方式、网络端口、数据卷、环境变量、启动策略和资源限制。每个维度都有对应参数,工程上大多数容器配置都由这些组合而成。
2.1 前台运行还是后台运行:-d 和 -it
创建容器时先问自己:这个容器需要一直留在后台给人访问,还是只需要临时执行一条命令?
后台运行用-d(detach 的简写):
docker run -d --name myapp nginx:alpine加了-d之后,命令行会直接输出一个容器 ID,容器在后台运行,日志不占用当前终端。适合 Web 服务、数据库这类长期存活的服务。
临时执行命令用-it(-iinteractive 保持标准输入,-ttty 分配虚拟终端):
docker run -it --rm ubuntu:22.04 bash这条命令用完即焚,--rm保证退出后自动删除容器,不会在你机器上留下一堆僵尸容器。这类组合适合调试镜像、验证命令、临时体验某个软件环境。
我几乎不会用docker attach去重新连接一个后台容器的标准输入,因为 attach 会把终端的输入输出直接绑定到容器主进程,操作不好会把容器搞退出。要进容器里执行命令,后面讲docker exec,那才是主流方案。
2.2 端口映射:-p 和 -P 的区别
容器默认在独立的网络命名空间里,宿主机访问不到容器的 IP 和端口。要让外部访问,就得做端口映射。
常用的是-p指定端口映射关系:
docker run -d --name nginx-web -p 8080:80 nginx:alpine这里8080是宿主机端口,80是容器内端口。外部请求访问宿主机的 8080 端口时,Docker 会把流量转发给容器的 80 端口。习惯上我会把宿主机端口写在前面,容器端口写在后面,格式固定为宿主机IP:宿主机端口:容器端口,比如:
docker run -d --name mysql8 -p 127.0.0.1:3306:3306 mysql:8.0这个写法就会把 MySQL 的 3306 端口只绑定在宿主机的回环地址上,局域网其他机器访问不到。这在安全性敏感的场景很有用。
-P是大写 P,表示把所有镜像里声明的 EXPOSE 端口随机映射到宿主机高位端口。说实话,生产环境我基本不用,因为它不可预测,排查问题不方便。测试环境偶尔用,配合docker port 容器名查看实际映射关系。
2.3 数据持久化:-v 与卷的使用
创建容器时最重要、最容易写错的就是-v。它有两种写法:命名卷和绑定挂载。
命名卷:
docker run -d --name mysql8 \ -v mysql-data:/var/lib/mysql \ mysql:8.0这里mysql-data是 Docker 自己管理的一个卷,数据会放在 Docker 的数据目录下,即使容器删除,数据还在。它的好处是耦合低、不需要关心宿主机具体路径,且多个容器可以共享同一个命名卷。
绑定挂载:
docker run -d --name nginx-web \ -v /home/user/www:/usr/share/nginx/html:ro \ nginx:alpine这里把宿主机的/home/user/www目录直接映射到容器内 Nginx 目录,后面加的:ro表示容器内只读,宿主机可以随意修改,适合静态文件、代码目录这类需要频繁同步的场景。
为什么我强调 MySQL 必须用卷?因为 MySQL 的数据目录是/var/lib/mysql,如果不用卷,容器删除等于删除数据库。这个坑几乎每个新手都踩过。Redis 同理,appendonly yes后 AOF 文件默认写在/data,所以也要挂载:
docker run -d --name redis7 \ -v redis-data:/data \ redis:72.4 环境变量与启动策略:-e 和 --restart
很多镜像允许通过环境变量传递初始配置。最典型的是 MySQL:
docker run -d --name mysql8 \ -e MYSQL_ROOT_PASSWORD=myPass123 \ -e TZ=Asia/Shanghai \ mysql:8.0MYSQL_ROOT_PASSWORD是 MySQL 官方镜像初始化 root 密码用的,TZ设置时区。还有MYSQL_DATABASE、MYSQL_USER这些,第一次启动时有效,容器初始化完成后你再改环境变量不会生效,必须进容器里改 MySQL 配置或直接用 SQL 处理。
--restart决定容器退出后怎么处理。我最常用的值:
--restart always:不管容器是正常退出还是异常退出,都自动重启。服务类容器我基本都用它。--restart unless-stopped:除非手动 stop,否则自动重启。手动停掉后 Docker 重启也不会再拉起来,这个比 always 更符合人的直觉。--restart on-failure:5:非正常退出时最多重启 5 次。
要注意:如果镜像本身入口命令写错了,或者启动参数不符合镜像要求,容器会无限重启,日志全是报错。遇到容器一直 Restarting 状态,先看日志,不要盲目改 restart 策略。
2.5 资源限制参数:--memory 和 --cpus
不加资源限制的后果我见过太多次:一个偏内存的服务吃光宿主机内存,整台机器卡死,不得不硬重启。Docker 提供了配置项:
docker run -d --name myapp \ --memory=512m \ --memory-swap=512m \ --cpus=1.5 \ myapp:latest--memory限制容器最大内存,--memory-swap限制内存加交换分区,--cpus限制 CPU 核心数。
这里有个容易踩的坑:只设置--memory不设置--memory-swap,Docker 默认会允许容器使用相当于--memory两倍的内存(其中一部分算作 swap)。想严格限制,就把--memory-swap设成和--memory一样。
给生产环境的容器写资源限制时,我建议留出余量。比如物理机有 16G 内存,规划给两个 JVM 容器各 4G,剩下的留给系统和其他辅助进程。宁可少分一点,也不要让 OOM 把整个宿主机搞死。
3. 高频实战:创建 MySQL、Redis、Nginx 容器
讲了这么多参数,还是要落到实际场景里看怎么组合。我挑三个最常见的服务,每个都给出可以直接复制的命令,以及我实际执行时遇到的问题和调整思路。
3.1 实例一:MySQL 8.0 容器,从创建到连接
我的标准命令:
docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=Root@123456 \ -e TZ=Asia/Shanghai \ -v mysql-data:/var/lib/mysql \ --restart unless-stopped \ mysql:8.0等待十几秒后,容器状态从starting变成healthy或Up,然后用客户端连接。
“docker 安装 mysql 失败”这个问题在网上问得最多,总结下来就是三个原因:
第一,端口被占用。宿主机上本来就有 3306 服务,端口映射会启动失败。先确认:
docker logs mysql8如果提示bind: address already in use,说明端口冲突,把-p 3306:3306改成-p 3307:3306,或者停掉占用端口的旧服务。
第二,MySQL 8.0 默认的认证插件变了。8.0 默认使用caching_sha2_password,而老的 Navicat、部分 JDBC 驱动可能不支持,连接时报Authentication plugin 'caching_sha2_password' cannot be loaded。这和 Docker 没关系,解决办法进容器改用户认证方式:
docker exec -it mysql8 mysql -uroot -pALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'Root@123456'; FLUSH PRIVILEGES;或者创建应用账号时直接用mysql_native_password。
第三,密码复杂度被客户端拦截。MYSQL_ROOT_PASSWORD设置太简单,MySQL 初始化可能会拒绝。如果想测试,加一个环境变量降低校验:
docker run -d \ --name mysql8 \ -e MYSQL_ROOT_PASSWORD=123456 \ -e MYSQL_ROOT_HOST=% \ mysql:8.0 --default-authentication-plugin=mysql_native_password注意最后面的--default-authentication-plugin是传给容器主进程的额外参数。镜像的入口点是mysqld,你写的额外参数都会附加到mysqld后面。这也是“容器崩溃/秒退”最常出现的原因——你往命令里塞了镜像不认识的参数。
3.2 实例二:Redis 主从复制容器
单机 Redis 容器很简单:
docker run -d \ --name redis7 \ -p 6379:6379 \ -v redis-data:/data \ redis:7 \ redis-server --appendonly yes最后的redis-server --appendonly yes是为了告诉容器以appendonly模式启动,而不是用镜像默认的配置。
但主从结构就要考虑“容器之间怎么互相访问”。我一般先建一个自定义网络:
docker network create redis-net然后启动一个 master 和两个 slave:
docker run -d \ --name redis-master \ --network redis-net \ -p 6379:6379 \ redis:7 \ redis-server --appendonly yes --requirepass masterPass123 docker run -d \ --name redis-slave1 \ --network redis-net \ -p 6380:6379 \ redis:7 \ redis-server --replicaof redis-master 6379 --masterauth masterPass123注意两点:
从节点要设置--replicaof redis-master 6379,这里用的是容器名redis-master而不是 IP,因为 Docker 内置 DNS 会在同一个自定义网络里解析容器名。如果不加--network redis-net,两个容器都在默认 bridge 网络里,也能通过 IP 互通,但容器重建后 IP 会变,用容器名更可靠。
--masterauth必须和 master 的--requirepass保持一致,否则从节点同步时会报NOAUTH Authentication required。
验证方式:
docker exec -it redis-slave1 redis-cli -p 6379 -a masterPass123 INFO replication看到role:slave和master_link_status:up,就说明主从跑起来了。
3.3 实例三:Nginx 静态站点容器
静态站点适合用绑定挂载:
docker run -d \ --name web \ -p 8080:80 \ -v /opt/www:/usr/share/nginx/html:ro \ --restart always \ nginx:alpine把静态文件丢到宿主机的/opt/www,访问http://宿主机IP:8080就能看到页面。用:ro是防止容器内进程误改宿主机文件,同时宿主机改完直接生效,不需要重启容器。
如果你还需要改 Nginx 配置,可以再加一个配置目录挂载:
docker run -d \ --name web \ -p 8080:80 \ -v /opt/www:/usr/share/nginx/html:ro \ -v /opt/nginx/conf.d:/etc/nginx/conf.d:ro \ nginx:alpine改完配置重载:
docker exec web nginx -s reload4. 容器创建后的日常管理命令清单
创建完容器不等于结束,日常维护才是大头。以下命令是我在排障和日常运维里最高频使用的,基本能覆盖 80% 的场景。
4.1 查看容器状态:ps、stats、port
docker ps docker ps -a docker stats docker port mysql8docker ps只看“正在运行”的容器,docker ps -a连已经退出和创建失败的容器也显示出来。排障时一定要用-a。
docker stats会以实时刷新的方式显示 CPU、内存、网络 I/O,我在确认容器内存限制是否合理时用。
用--format可以自定义输出,适合脚本处理:
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}"把这条命令写进 shell 别名,排查环境会顺眼很多。
4.2 进入容器执行命令:exec 优先于 attach
进入正在运行的容器:
docker exec -it web bash比如查 Nginx 配置、看日志、装调试工具,都在这里面操作。如果容器里没有 bash,只有 sh(很多 Alpine 镜像默认没有 bash),就改成:
docker exec -it container-name sh直接把命令写在 exec 后面也行:
docker exec web nginx -t docker exec mysql8 mysqladmin -uroot -p ping4.3 日志、文件拷贝与容器删除
查看容器日志:
docker logs mysql8 docker logs --tail 50 -f mysql8--tail 50只看最近 50 行,-f跟随输出,类似tail -f。
宿主机和容器之间复制文件:
docker cp /opt/app.jar web:/app.jar docker cp web:/var/log/nginx/access.log ./access.log复制出来的文件归属会是 root,这在宿主机上编辑时需要注意权限。
删除容器和清理残留:
docker rm -f mysql8 docker container prunedocker rm -f是强制删除运行中的容器,docker container prune清理所有已停止的容器。配--rm的临时容器不用手动删。
还有两个容易被忽略的命令:
docker inspect mysql8 docker logs --since 30m mysql8docker inspect输出容器的完整 JSON 配置,IP 地址、挂载信息、环境变量都在里面。--since 30m只看最近 30 分钟的日志,排查“刚才发生了什么”时比全量日志高效很多。
5. 常见问题与排查技巧实录
创建容器的命令本身不难,难的是出错后的定位。下面这些是我和团队在实际项目中反复遇到过的场景,整理成速查表形式,遇到同款问题可以直接对照。
5.1 端口占用与映射失败
报错表现:
docker: Error response from daemon: driver failed programming external connectivity on endpoint Bind for 0.0.0.0:3306 failed: port is already allocated排查:
# Linux ss -tlnp | grep 3306 # Windows netstat -ano | findstr 3306 # macOS lsof -i :3306找到占用进程后,要么停掉它,要么改映射端口。我的建议是:不到万不得已别改容器内端口,直接换宿主机端口,因为镜像内部服务端口往往是写死的。
5.2 容器创建后秒退(Exited 0 / Exited 1)
先看日志:
docker logs 容器名常见的退出原因:
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| Exited (0) | 主进程跑完直接退出,比如临时执行命令 | 加-it保持前台,或者改用-d |
| Exited (1) | 入口进程报错,MySQL 初始化失败、Redis 参数错误 | 看日志具体报错,重点检查命令末尾传入的参数 |
| Exited (127) | 找不到命令 | 确认镜像内实际有没有这个二进制文件,常见于 Alpine 镜像没有 bash |
| Restarting | 进程崩了但--restart一直拉 | 先docker logs,不要急着改 restart 策略 |
Redis 容器最容易出这个问题。很多人执行:
docker run -d redis:7 --appendonly yes因为它把--appendonly yes当成了docker run的参数传给 Docker 进程,而不是传给容器内的redis-server。正确的做法是在镜像名之后写参数:
docker run -d redis:7 redis-server --appendonly yes镜像名后面的内容会作为入口点参数传进容器。这个规则适用于几乎所有镜像。
5.3 容器网络不通
现象:容器能创建成功,但无法访问外部服务,或者外部访问不到容器。
排查命令:
docker exec -it 容器名 ping 目标地址 docker network inspect bridge docker network ls docker inspect 容器名 | grep IPAddress最常用的解决方案是显式指定网络:
docker run --network host ... docker run --network 自定义网络 ...端口映射-p只对默认 bridge 网络有效,如果用host网络模式,容器直接用宿主机的网络栈,-p就会失效,访问端口直接用容器服务自己的端口。
自定义网络还有一个好处:同一个网络里的容器可以用容器名互相访问,不需要查 IP。多容器联调时强烈建议自建网络,比默认 bridge 稳定可控得多。
5.4 Docker Desktop 启动失败:虚拟化与权限
Windows 上常见提示:
docker desktop failed to start because virtualization support wasn't detected解决思路我已经在前面说过,进 BIOS 开 VT-x/SVM,Windows 功能里开“虚拟机平台”。如果开启后仍然不行,用命令确认:
systeminfo | findstr "Hyper-V" # 或者 powershell -Command "Get-ComputerInfo | Select-Object HyperVisorPresent, VirtualizationFirmwareEnabled"VirtualizationFirmwareEnabled为 False,说明 BIOS 层面还没打开。改完后重启,再启动 Docker Desktop。
Linux 上另一类常见问题是权限:
Got permission denied while trying to connect to the Docker daemon socket把当前用户加入 docker 组:
sudo usermod -aG docker $USER newgrp docker然后重新登录终端。这是新手高频错误,很多人以为是命令写错了,其实只是没权限访问 Docker 套接字。
5.5 挂载目录权限导致的诡异问题
用绑定挂载容器时,宿主机目录权限和容器内用户不匹配,会出现“能创建容器但服务起不来”“写入失败”这类奇怪现象。
比如 MySQL 挂载到宿主机目录:
docker run -d --name mysql8 \ -v /opt/mysql-data:/var/lib/mysql \ mysql:8.0宿主机/opt/mysql-data如果权限不是 777,MySQL 容器内的 mysql 用户无法写数据,初始化就会失败。日志里报Permission denied。
解决思路不是粗暴改 777,而是确认容器内用的 UID,再看宿主机目录属主。更稳妥的方式是用命名卷,Docker 会管理权限,避免这个坑。
6. 再分享几个我自己的使用习惯
列几个我长期坚持的命令习惯。很多细节不是文档里写的,但真到事故现场,能少踩不少坑。
第一个习惯:创建容器永远加--name。不加名字 Docker 会随机生成一个名字,比如docker run后你得到的是一个admiring_hopper。日志、重启、删除全靠 ID,太费神。加上名字之后,所有操作都能用语义化标识符,尤其写脚本时友好太多。
第二个习惯:环境变量和敏感信息不要直接裸写在命令行里。命令行会被 shell history 记录,多个人共用服务器时很容易泄露密码。更安全的做法是用环境文件:
docker run -d --name mysql8 --env-file ./mysql.env mysql:8.0mysql.env里写:
MYSQL_ROOT_PASSWORD=Root@123456 TZ=Asia/Shanghai文件本身也要设置权限,比如chmod 600 mysql.env。
第三个习惯:改成用 docker compose,但先理解 run 再碰 compose。docker run是理解容器配置的最小单元,compose 只是把杂乱的长命令结构化而已。我见过不少直接抄 compose 文件但不知道每个字段对应什么参数的人,出事以后完全不会排查。先把docker run玩透,再看 compose,会发现就是换个姿势写配置。
第三个习惯之后,我要补充一点关于“删不掉”“stop 不掉”的体会。如果容器卡在停止过程中,等十几秒还不退,直接:
docker rm -f 容器名强制删掉重来。不要恋战,不要反复尝试优雅停止。“删了重建”在容器世界是最廉价的修复手段,前提是数据卷挂载正确,重来不丢数据。
最后分享一下我的一个判断:容器创建命令写得是否规范,直接影响故障恢复速度。一个没有打--restart的 Web 服务,进程崩溃后没人发现,等到用户反馈才去手动拉起来;而一个挂了--restart unless-stopped的服务,崩溃后会自动恢复,最多丢失几秒请求。同样是创建容器的命令,差别只在多了一个不起眼的参数。这种参数不是讨论“能不能跑”,而是讨论“出了事怎么办”。我们写命令的时候,心里要永远默认“它一定会出故障”,然后去设计它的边界:数据丢不丢、自动拉起与否、资源会不会互相挤兑。想清楚这几个问题,你写出的docker run才不会像随手玩玩具,而是一份真正可靠的基础设施配置。