☰
Docker容器创建实战:docker run参数详解及MySQL/Redis/Nginx部署
2026/10/6 13:36:35 网站建设 项目流程

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:7

2.4 环境变量与启动策略:-e 和 --restart

很多镜像允许通过环境变量传递初始配置。最典型的是 MySQL:

docker run -d --name mysql8 \ -e MYSQL_ROOT_PASSWORD=myPass123 \ -e TZ=Asia/Shanghai \ mysql:8.0

MYSQL_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 -p
ALTER 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 reload

4. 容器创建后的日常管理命令清单

创建完容器不等于结束,日常维护才是大头。以下命令是我在排障和日常运维里最高频使用的,基本能覆盖 80% 的场景。

4.1 查看容器状态:ps、stats、port

docker ps docker ps -a docker stats docker port mysql8

docker 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 ping

4.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 prune

docker rm -f是强制删除运行中的容器,docker container prune清理所有已停止的容器。配--rm的临时容器不用手动删。

还有两个容易被忽略的命令:

docker inspect mysql8 docker logs --since 30m mysql8

docker 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.0

mysql.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才不会像随手玩玩具,而是一份真正可靠的基础设施配置。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询