一条在网上抄来的 docker run 命令,容器起来了,应用却连不上,或者干脆卡在启动阶段反复报错,这种场景我见过太多次。Docker 常用镜像启动参数的坑,说大不大,说小不小,但每次都能把人卡到怀疑人生。官方文档写得很全,可日常使用中大多数人根本不翻文档,而是打开搜索引擎复制一条看着差不多的命令,改个名字就往上跑。结果就是:MySQL 拉下来已经是 8.x,初始化参数却还是 5.7 时代的写法;Redis 主从复制怎么配都不认;GitLab 起来之后内存直接拉满,整台机器鼠标都挪不动。
这不是 Docker 的锅,也不是镜像的锅,问题出在我们对启动参数的理解是碎片化的。所以这篇东西不打算讲高深原理,就做一件事:把常用镜像的启动参数整理成一张能直接照着抄的对照表,并且把每个参数到底在干什么、为什么这么写、哪些地方容易踩坑,全部讲透。这篇内容更适合个人开发者、小团队内部环境搭建,以及刚开始把服务迁到容器里的同学。生产环境里你还要考虑 K8s、资源限额、编排平台,但理解了 docker run 这套参数设计,换到哪一层都不会懵。
1. 为什么需要一张"能真正跑起来"的启动参数对照表
先说明一下,网上不是没有命令大全,但很多命令存在三种硬伤。一是版本不对,作者写博客的时候 MySQL 还是 5.7,现在拉镜像默认可能是 8.0 甚至 8.4,参数和初始化逻辑早就变了。二是缺少关键参数,比如数据卷没挂、时区没指定、初始化环境变量没给,容器能启动但根本没法正常用。三是把短期调试参数当长期运行参数来用,比如临时测试用的--rm被用在服务上,重启一次数据就没了,哭都来不及。
我见过最典型的案例,是有人照着旧教程启动 MySQL,容器状态一直显示 healthy,但他用客户端从宿主机连 3306 永远超时。一查发现教程里根本没有-p 宿主机端口:容器端口,容器网络默认桥接,3306 只暴露在 Docker 内部网络里,宿主机外面根本摸不到。这就是参数缺位的典型后果,也是网上那些命令"看着对、跑起来错"的核心原因。
所以这张对照表的第一个作用,是帮你把"能跑起来"和"跑得对"之间的差距填上。第二个作用是给你一个参照系,让你修改某个镜像的参数时,至少知道该动哪个位置、动了会有什么影响。我不会把重点放在罗列所有镜像的所有参数上,那没有意义;真正的核心是选常用、高频、典型的那几个,把参数逻辑拆开讲透。下面每个镜像的启动命令,我都按当前主流的版本习惯来写,你直接复制,改改密码和端口就能用。
2. 先拆开一条 docker run:参数到底在配置什么
在给每个镜像列参数之前,得先弄清 docker run 后面那一长串东西分别是什么。很多人记参数完全是死记硬背,一旦镜像换了就不知道怎么改。其实一套容器启动参数背后就四件事:镜像是谁、端口怎么暴露、数据放在哪、初始状态和环境怎么交代。把这四件事想清楚,任何镜像的启动命令你都能自己拼出来。
2.1 镜像名和标签:latest 的隐藏风险
docker run的第一个参数是镜像名,也就是仓库名/镜像名:标签。很多新手容易忽略标签,直接写mysql,这时 Docker 默认拉取latest标签。latest 本身不是一个特殊版本,只是维护者打上去的一个标签,时间一长它指向的版本谁也不知道。我建议所有镜像都显式写版本标签,比如mysql:8.0、redis:7、nginx:stable。
确定版本号还有个额外好处,是排障时可以复现。同一个镜像不同版本的启动参数兼容性差异很大,比如 MySQL 从 5.7 到 8.0,默认认证插件从 mysql_native_password 变成了 caching_sha2_password,你如果拿着 5.7 的连接配置去连 8.0,客户端版本旧一点就直接报认证失败。写上明确的标签,至少保证你是用同一个版本在排查问题,而不是在一个"薛定谔的版本"上浪费时间。
2.2 端口映射:-p 的写法决定了谁能访问
-p是最容易出错的参数。它的完整格式是-p 宿主机地址:宿主机端口:容器端口,比如-p 127.0.0.1:3306:3306。如果只写-p 3306:3306,就等价于0.0.0.0:3306:3306,宿主机所有网卡上的 3306 都会被映射进去。本地开发数据库如果不想暴露到局域网,我会习惯写成127.0.0.1:3306:3306,这样只有本机能连。
另外要注意宿主机端口冲突。本地 3306 已经被原生 MySQL 占了,容器再映射 3306 就会启动失败,这时候要么改宿主侧端口-p 3307:3306,要么停掉本机的原生服务。容器内部的应用端口保持不变,改的永远是冒号左边,这个规则对任何镜像都适用。
2.3 数据卷:容器可以删,数据必须留
容器本身是"一次性的",镜像删了重建都很正常,但数据不能跟着没。数据卷有三种常见用法,对应不同场景:
- 命名卷:
-v mysql-data:/var/lib/mysql,由 Docker 管理目录,位置不固定但迁移方便。 - 路径挂载:
-v /data/mysql:/var/lib/mysql,数据直接落在宿主机指定目录,方便查看和备份。 - 匿名卷:
-v /var/lib/mysql,只有容器内路径,用完不好找位置,日常使用不太推荐。
挂到哪个路径是镜像决定的,不能凭感觉猜。比如 MySQL 数据目录是/var/lib/mysql,PostgreSQL 是/var/lib/postgresql/data。路径挂错的结果是:容器起来了,写的数据根本不在你挂的目录里,或者目录权限不对,数据库起不来。这个坑我在下面具体镜像部分会再展开。
2.4 环境变量与重启策略:容器启动时的"初始状态"
-e环境变量是镜像对外暴露的配置入口。同一个镜像在不同场景下行为不同,靠的就是环境变量。比如 MySQL 初始化 root 密码用的MYSQL_ROOT_PASSWORD,PostgreSQL 创建用户用的POSTGRES_USER。注意环境变量只在首次初始化时生效,如果数据卷里已经有初始化过的数据,再改环境变量不会重设密码,这是很多人改密码不生效的原因所在。
重启策略--restart是容器服务化的关键。unless-stopped表示除非手动 stop,否则开机和异常退出都会自动拉起,适合长期服务;always更激进,Docker 重启后一定会拉起;no是默认值,适合调试用的一次性任务。调试临时容器时还可以加--rm,容器停止后自动删除,避免机器上堆满死容器。长期服务一定记得选unless-stopped,不然宿主机一重启,服务全没了。
3. 数据库类镜像启动参数:MySQL、Redis、PostgreSQL
数据库是容器化里的重头戏,也是启动参数最容易出事的类别。下面这三款我分别给出完整命令和逐项参数解释,如果你只打算看一部分,建议优先看自己正在用的那款,再把其他两款扫一眼,因为数据库镜像的参数设计思路是相通的。
3.1 MySQL 8.0:初始化参数、字符集与远程访问
以 MySQL 8.0 为例,一条可用的启动命令大概是:
docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=YourPass123 \ -e TZ=Asia/Shanghai \ -v mysql-data:/var/lib/mysql \ mysql:8.0 \ --character-set-server=utf8mb4 \ --collation-server=utf8mb4_unicode_ci这条命令里有几个点值得展开讲。
第一,镜像名后面的--character-set-server和--collation-server不是 docker run 的参数,而是传给容器内 mysqld 的额外启动参数,docker run 会把镜像名之后的所有内容原样传给镜像的默认启动脚本。MySQL 官方镜像支持这种写法,所有 mysqld 选项都可以这样透传。
第二,TZ=Asia/Shanghai这个环境变量很多人不写,结果数据库里的NOW()和宿主机的当前时间差 8 小时,排查日志特别折磨人。建议数据库、中间件、应用镜像统一把时区写进去,这是花的力气最少、收益最高的参数。
第三,MYSQL_ROOT_PASSWORD只在数据卷为空、第一次初始化时生效。如果之前已经在同一个命名卷上启动过,你再改密码环境变量是没用的。要么删掉卷重建,要么进容器用ALTER USER修改。很多人以为改了-e里的密码就能重置,结果连不上就开始怀疑一切,其实问题就出在这个"只生效一次"的机制上。
CREATE USER 'app'@'%' IDENTIFIED BY 'AppPass123'; GRANT ALL PRIVILEGES ON *.* TO 'app'@'%'; FLUSH PRIVILEGES;第四,远程访问要注意两件事。一是确认端口映射写的是0.0.0.0或具体宿主机 IP,而不是只映射到 Docker 内部网络;二是 MySQL 8.0 默认 root 用户只允许 localhost 登录,你从宿主机或局域网连的时候会直接报Access denied。这时需要手动创建一个远程访问用户,或者把 root 的 host 改成%,上面的 SQL 就是干这个用的。我遇到过不少"容器正常、端口也映射了、就是连不上"的案例,一大半是卡在 MySQL 用户授权这里。
3.2 Redis:从单机到主从的启动参数变化
Redis 的启动命令比 MySQL 简单,但主从复制的参数细节很值得单独说。先看单机版:
docker run -d \ --name redis-server \ -p 6379:6379 \ -v redis-data:/data \ redis:7 \ redis-server --appendonly yes --requirepass YourPassredis-server后面跟的是 Redis 自身的启动参数,--appendonly yes开启 AOF 持久化,数据默认写入容器的/data目录,这和挂载的redis-data正好对应。--requirepass设置访问密码,不写的话 Redis 默认无密码,公网映射出去很容易被扫描器盯上,几分钟内就会被恶意写入,这个真不是危言耸听。
再看主从复制。同主机上搭建一主一从,我建议先建一个自定义网络,让容器之间可以通过名称互相解析,而不是去查容器 IP 再手动写死:
docker network create redis-net docker run -d \ --name redis-master \ --network redis-net \ -p 6379:6379 \ -v redis-master-data:/data \ redis:7 \ redis-server --appendonly yes --requirepass masterpass docker run -d \ --name redis-slave \ --network redis-net \ -p 6380:6379 \ -v redis-slave-data:/data \ redis:7 \ redis-server --appendonly yes --requirepass slavepass \ --replicaof redis-master 6379 \ --masterauth masterpass注意几个关键点。从库的--replicaof后面写的是容器名:容器端口,不是宿主机端口。因为两个容器在同一个自定义网络中,Docker 内置 DNS 会把redis-master解析成它的容器 IP。从库自己设了密码时,连接主库要配--masterauth,否则复制连接会被主库的密码挡在外面。
主从起来后,用这条命令确认复制链路状态:
docker exec -it redis-slave redis-cli -a slavepass info replication输出里找到master_link_status:up就代表主从已经连通。如果显示down,优先检查--replicaof的地址对不对、--masterauth的密码是不是主库的--requirepass,这两处是我踩过最多的点。
3.3 PostgreSQL:一条命令完成用户、密码、库的初始化
PostgreSQL 官方镜像把初始化需要的信息都做成了环境变量,一条命令能同时把业务用户、密码、数据库全建好:
docker run -d \ --name pg16 \ -p 5432:5432 \ -e POSTGRES_USER=appuser \ -e POSTGRES_PASSWORD=apppass \ -e POSTGRES_DB=appdb \ -v pg-data:/var/lib/postgresql/data \ postgres:16这里POSTGRES_USER会创建一个超级用户,POSTGRES_PASSWORD设置该用户密码,POSTGRES_DB会自动创建对应的数据库。如果只写POSTGRES_PASSWORD不写用户名,默认创建postgres超级用户。初次连接时直接用appuser,因为它已经是超级用户,开发环境完全够用。
PostgreSQL 的一个常见坑是数据卷权限。官方镜像的 postgres 进程默认以 uid 999 运行,如果你用路径挂载到宿主机目录,目录所有者不是 999,容器会直接报could not create directory之类的权限错误。解决方法是先把目录权限改给 999,或者用命名卷让 Docker 自己管。这也是为什么我在命令里默认用命名卷,它能省掉一大半权限问题的排查时间。
4. 应用服务类镜像启动参数:Nginx、GitLab、青龙面板、系统镜像
数据库讲完,再来看看应用服务。这一类镜像的共性是"配置从哪来"和"资源怎么控",启动参数的设计逻辑也因此各有侧重。
4.1 Nginx:静态站点与反向代理的挂载方式
Nginx 镜像的启动参数核心是两处挂载:站点文件挂到/usr/share/nginx/html,配置文件挂到/etc/nginx/conf.d:
docker run -d \ --name nginx-web \ -p 8080:80 \ -v /data/www:/usr/share/nginx/html:ro \ -v /data/nginx/conf.d:/etc/nginx/conf.d:ro \ nginx:stable挂载时加:ro(只读)是个好习惯。宿主机上的文件更新后,Nginx 会自动读取文件系统里的新内容,静态文件不需要重启容器就能生效;改配置文件之后,执行docker exec nginx-web nginx -t先做语法检查,再docker exec nginx-web nginx -s reload平滑重载,不用把整个容器重启一遍。
有人会把 Nginx 的官方默认配置删掉,然后自己写一份nginx.conf挂到/etc/nginx/nginx.conf,这样也不是不行,但挂载conf.d目录更简洁,不会影响官方镜像内置的主配置结构。反向代理时,proxy_pass指向同网络里的其他容器名称,比如http://app-server:8080,前提是 Nginx 容器和应用容器在同一个自定义网络里。跨网络访问时,Nginx 默认解析的是容器 IP,网络一变 IP 就变,这是"Nginx 突然 502"的常见原因之一。
4.2 GitLab:内存瓶颈与必须调整的启动参数
GitLab 是出了名的"贪吃"镜像,官方自己都在文档里强调要调整/dev/shm大小。我见过太多人直接docker run -d gitlab/gitlab-ce然后发现机器卡死,容器虽然起来了,但 Web 页面打开极慢,跑一段时间直接被系统 OOM 杀掉。
可用的基础命令大概是:
docker run -d \ --name gitlab \ --restart unless-stopped \ -p 8082:80 -p 8443:443 \ --shm-size 256m \ -v gitlab-etc:/etc/gitlab \ -v gitlab-log:/var/log/gitlab \ -v gitlab-data:/var/opt/gitlab \ -e GITLAB_ROOT_PASSWORD=YourGitLabPass \ gitlab/gitlab-ce:latest--shm-size 256m是必然会写进任何一份 GitLab 启动命令里的参数。GitLab 用到大量共享内存,默认的 64MB 很容易不够用,导致重启、卡顿、后台任务失败。GITLAB_ROOT_PASSWORD用来预设管理员root的密码。这里有个坑:如果忘了设置,而容器首次初始化已经完成,环境变量就不会生效了,需要进容器执行gitlab-rails runner来重置密码。
GitLab 第一次启动非常慢,初始化可能需要几分钟到十几分钟,看到日志没有新增内容也别急着断言失败,用docker logs -f gitlab观察是否打印出gitlab Reconfigured之类的完成标记。想省内存的话,可以通过挂载自定义/etc/gitlab/gitlab.rb关闭几个不用的组件,比如prometheus['enable'] = false,但这属于另一个话题了,先用上面的命令跑起来再说。
4.3 青龙面板:数据目录、端口与依赖管理
青龙面板在自动化任务圈子里很常见,它的启动参数不算复杂,但数据目录一定要单独挂出来:
docker run -d \ --name ql \ --restart unless-stopped \ -p 5700:5700 \ -v ql-data:/ql/data \ whyour/qinglong:latest面板跑起来之后访问http://宿主机IP:5700就能看到初始化页面。首次登录后第一件事是检查容器内的脚本环境依赖。很多人面板起来了,任务却一直在报缺少依赖,就是因为镜像内置的依赖不全,需要进入容器手动补装。常见操作是用docker exec -it ql bash进入容器,然后在容器内执行面板自身的依赖管理命令来安装 Node.js 或 Python 相关组件。依赖管理也是这类容器最容易出问题的地方,因为任务脚本的生态变化很快,镜像版本和脚本要求的运行时版本经常对不上。
/ql/data这个目录里全是配置和数据库文件,不挂载的话,容器一删全部配置清零,脚本要重新配一遍,这个教训很痛。另外 5700 是面板的默认端口,如果本机被占,改-p左侧的宿主端口就行,容器内部端口不要动。
4.4 Ubuntu / CentOS 系统镜像:临时环境与构建基座
系统镜像的启动参数思路和上面完全不一样,它们往往不是跑一个常驻服务,而是作为临时排障环境或构建阶段的基础。最典型的两条命令:
docker run -it --rm --name ubuntu-test ubuntu:22.04 bashdocker run -it --rm --hostname centos-dev centos:7 bash-it分配交互式终端,--rm退出即删除容器,最适合临时验证命令、测试脚本、检查某个包的行为。--hostname可以指定容器内主机名,某些环境对主机名敏感,比如 CentOS 里和 systemd 相关的操作。系统镜像是容器世界里最容易被忽略的一类,但它恰恰是最适合"玩坏就丢"的试验田,我建议所有新手都拿它练手。
不过要提醒一句:想在容器里跑 systemd 来模拟完整操作系统环境,会遇到很多问题,比如 init 进程不工作、权限不够,通常需要加--privileged之类的特殊参数,但这会削弱容器隔离性。我的建议是,系统镜像容器更适合做"干净的试验台",不适合硬当作虚拟机用。真要模拟完整操作系统,老老实实用虚拟机。
5. 参数照抄了还是起不来:排障链路与关键操作
参数写对了,容器依然可能起不来。排障不能靠猜,要有一条固定的排查链路,下面这套顺序是我日常用的,省时间、覆盖面广。
5.1 排查顺序:docker logs 到 docker inspect
容器起不来或起来后行为异常,我建议按这个顺序查:
docker ps -a看容器状态,区分Exited、Restarting、Up。docker logs <容器名>看启动日志,大多数初始化错误都会在这里留下明确信息。docker inspect <容器名>看实际配置,确认端口映射、数据卷、环境变量是否真的挂上了。docker exec -it <容器名> <命令>进容器实测应用本身是否正常。
很多问题其实在第二步就已经暴露了,比如 MySQL 报权限错误、PostgreSQL 报目录不可写。但有时候容器状态是Up,日志也正常,就是连不上,那就得用docker inspect验证网络配置。比如我见过有人以为-p 3306:3306写进去了,实际用的却是--network host,端口映射参数在这种网络模式下根本没有意义,这一步不查,后面猜半天都是白费。
5.2 端口映射"正常"却连不上:从防火墙和绑定地址入手
端口映射检查了三遍都对,宿主机上还是连不上,这时候要往 Docker 外找。最常见的拦截来自宿主机本身的防火墙,Linux 上可以是firewalld、ufw,也可能是云服务器安全组规则。端口映射只负责把流量引到 Docker 的网络栈,宿主机层面的拦截它管不着。
另外注意-p左侧不要写错绑定地址。-p 127.0.0.1:3306:3306只允许本机访问,你在另一台机器上访问自然会失败。想对外提供服务,要么显式写宿主机 IP,要么写0.0.0.0,并配合防火墙规则来控制访问范围。我在测试环境图省事会直接写0.0.0.0,但生产环境宁可多写几步安全策略,也不会把这个口子开成全网可连。
5.3 数据卷权限引发的启动失败
数据卷权限出错时,报错信息往往很直白,但也容易被忽略。比如宿主目录是 root 所有,容器进程以普通用户运行,写不进去就退出。MySQL、PostgreSQL 这类官方镜像对数据目录的所有者要求很严格。
处理方式有几种。一是改宿主目录所有者,让它匹配容器内运行用户的 uid,比如 PostgreSQL 的 999;二是直接用命名卷,让 Docker 管理权限,避免错位;三是在支持 SELinux 的环境里,挂载时加上:Z或:z标签。需要说明的是,-v挂载的目录不具备正确 SELinux 上下文时,SELinux 会拒绝容器写入,这不是权限数字的问题,而是安全上下文的问题。国内用 CentOS 当宿主机的人常遇到这个,改之前先确认宿主机有没有开启 SELinux。
5.4 Docker Desktop 在 Windows 上启动失败的常见原因
"virtualization support not detected"这个报错出现的频率非常高。Docker Desktop 在 Windows 上依赖虚拟化支持,报这个错通常意味着两件事:BIOS/固件里没开启虚拟化,或者 Windows 的 Hyper-V/WHPX 功能没启用。
排查顺序也很固定:先进 BIOS 确认 CPU 虚拟化开关处于开启状态;然后在 Windows 可选功能里启用"虚拟机平台"和"适用于 Linux 的 Windows 子系统";最后重启电脑再试。如果公司电脑有策略锁定了 Hyper-V,那就要找管理员解决,命令行里怎么折腾都没用。这个报错本身和 Docker 的启动参数无关,但很多人在这一步就卡住了,所以排障时不要只盯着容器配置。
5.5 镜像拉取超时:配置镜像加速与重试策略
拉取镜像慢或者超时,在 Docker 使用里几乎必然遇到。稳妥的办法是给 Docker 配置镜像加速,原理是让 Docker 从指定的 Registry 镜像站拉取内容。配置文件在 Linux 下是/etc/docker/daemon.json,Windows 上可以在 Docker Desktop 的 Docker Engine 设置里直接改:
{ "registry-mirrors": [ "https://docker.m.daocloud.io" ] }改完重启 Docker 再重新拉取。如果拉取中断,重试时尽量保持docker pull命令不变,不要反复换 tag,因为 Docker 会对已下载的镜像层做缓存,换 tag 反而会丢掉进度。临时需要某个容器但镜像拉不下来时,可以换一个架构相同的其他可用镜像顶替,但那只适合调试,不适合作为长期方案。
6. 从"照着抄"到"自己改":四个值得养成的参数设计习惯
对照表能给一时,给不了一世。镜像更新、场景变化、迁移到编排平台,都会让你不得不自己改参数。下面这四个习惯,是我踩过不少坑之后总结出来的,值得慢慢养成。
6.1 动手前先写一行"意图注释"
启动一条 MySQL 容器前,先在命令上方或旁边写清楚"这条容器是干什么用的、数据放在哪、数据是否可以丢"。这个习惯帮你把命令分成两类:一次性调试和长期服务。调试用--rm,服务用--restart unless-stopped,两种命令的形态完全不同。我在本地写测试脚本时经常一条docker run -it --rm回车就跑完一个实验,而不会创建一个永远留着不删的服务容器,不然/var/lib/docker里会堆一堆不知道干嘛的容器。
6.2 用 docker inspect 验证实际配置
写完命令不代表配置生效。最直接的办法是docker inspect后查看Mounts、NetworkSettings.Ports、Config.Env三个字段,确认数据卷路径、端口映射、环境变量是不是自己设想的样子。有时候命令里写了-e MYSQL_PASSWORD=...,但镜像根本不认识这个变量,外表看起来没问题,实际初始化时没生效,这类"命令看起来对"的问题只有 inspect 才能发现。我把这个当成启动容器后的默认动作,验证一次花不了十秒钟,省下的却是几小时的排障时间。
6.3 记录"上次能跑"的完整命令
我自己的习惯是,每个项目的启动命令都会存成 shell 脚本或者 README 片段,标注日期、镜像版本、宿主端口,甚至是当初为什么这样配置。镜像版本更新后,我会先对比旧命令再下手,不能一把梭直接拉最新版跑。这也是为什么我前面反复强调写明确版本标签,没有标签就无法还原"上次能跑"的环境。生产环境尤其如此,"能跑"的状态必须可以被精确记录和回放,参数设计也是一样的思路。
6.4 从 docker run 平滑过渡到 docker compose
当你发现某个环境里有三四个关联容器要一起启动时,docker run 的劣势就出来了,每条命令都要重复写网络、端口、数据卷,而且容易漏配置。这时候把命令转成 docker compose 是自然的下一步。逻辑上其实就是把 docker run 的参数搬进docker-compose.yml:
-p对应ports-e对应environment-v对应volumes--restart unless-stopped对应restart- 自定义网络对应顶层
networks
之前 Redis 主从的例子,用 docker compose 写会比两条 docker run 直观很多,服务间依赖关系也能一眼看清。但我自己依然保留着一张"docker run 参数怎么映射到 compose 字段"的对照表,因为迁移到 K8s 时,Pod 里的 initContainer、volume、env 也还是这套底层概念的变体。参数永远在变,容器化的思维模型是不变的。
以上是我这次想分享的全部内容。一张对照表真正发挥作用的前提,是你能解释清楚每个参数为什么出现。建议你把上面这些镜像挨个跑一遍,改一个参数观察一次现象,MySQL 改密码、Redis 断主从、Nginx 改代理目标,都试过了之后,再回到这张表就会发现它已经属于你了。