ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Docker容器创建全攻略:docker create与docker run参数详解

Docker容器创建全攻略:docker create与docker run参数详解 刚上手那会儿我创建容器基本只有一个姿势docker run。网上一搜 Nginx 就复制一条docker run -d -p 8080:80 nginx端口起了、页面能开了收工。直到后来要一次性准备一堆容器、想在真正启动前先核对一遍网络配置、或者在 CI 里预创建容器而不急着跑进程才发现 Docker 里负责“创建容器”的命令并不是只有docker run这一条还有个容易忽略的docker create而参数之间的搭配和运行模式选择才是能不能稳定把容器跑起来的关键。这篇笔记是我实际用下来整理出来的创建容器命令完整梳理涵盖了docker create和docker run的关系、常用参数拆解、不同场景的命令组织方式、创建后的状态检查与排查思路最后补了一点 compose 声明式创建容器的经验。适合已经把镜像拉取玩明白、但被各种参数和网络模式搞得眼花缭乱的读者也适合每天要部署多个容器但不想反复敲长命令的运维。1. docker create和docker run创建容器命令的两个入口很多人只用了半个1.1 先弄清“创建容器”在 Docker 里到底完成了什么Docker 里所谓的“创建容器”本质上是基于一个镜像实例化出一个独立的运行环境。这个过程中Docker daemon 会为容器分配一个唯一的容器 ID、创建一层可写的容器文件系统、初始化网络命名空间和网络接口、设置 hostname然后把这些配置固化到容器的元数据里。整套动作做完容器对象就存在了但主进程还没有被拉起。形象一点理解镜像是模板容器是拿模板做出来的实例。创建容器相当于把模板裁剪成一台有自己配置的“虚拟小机器”但还没通电开机。这个“没通电”的状态在docker ps -a里能看到状态栏显示Created。而docker ps看不到它因为默认只显示正在运行的容器。这个细节很关键。很多人排查问题时只看docker ps发现容器不在了就以为镜像有问题其实容器可能一直好端端地躺在Created状态只是没启动而已。1.2 docker run 只是 create 加 start 的复合命令docker run这条命令实际等于docker create加docker start如果再带上交互参数还会自动 attach 到容器主进程上。所以很多时候你想把“创建容器”和“启动容器”两个动作拆开就得用docker create先做出容器再手动docker start。我比较常用的一种调试方式是docker create --name debug-nginx -p 8081:80 nginx:1.27 docker inspect debug-nginx | grep -A5 NetworkSettings docker start debug-nginx这样做的好处是在还没有真正运行之前你可以用docker inspect完整检查容器创建时的配置比如端口映射有没有写错、环境变量有没有传全、网络模式是不是自己想要的。发现问题直接docker rm删掉重来比docker run起来之后发现不对劲再停掉、删掉要干净。如果哪天你创建了很多容器但没有启动也可以统一批量处理docker start $(docker ps -a --filter statuscreated -q)这条命令会把所有处于created状态的容器一次性启动适合批量准备开发环境的场景。1.3 创建命令的语法结构以及 IMAGE 后面那个 COMMAND 是干什么的无论docker create还是docker run语法结构都一致docker create [OPTIONS] IMAGE [COMMAND] [ARG...] docker run [OPTIONS] IMAGE [COMMAND] [ARG...]中括号里的 COMMAND 和 ARG 很多人会忽略其实它们是用来覆盖镜像默认 CMD 的。比如镜像默认启动的是 Nginx但你偏要让这个容器启动后去执行一条 shell 命令做测试就可以这样docker create --name pingtest alpine ping 8.8.8.8这种情况下容器的主进程是ping而不是 Alpine 的默认 shell。以后你想复用某个镜像但当工具用又不想改镜像本身这个位置就是入口。需要注意这个 COMMAND 是容器启动后内部执行的命令不是在宿主机上执行的命令。它会被直接当成容器的主进程PID 1来运行主进程退出了容器也就退出了。这是容器和虚拟机最大的运行差异之一。2. 创建容器时真正要记牢的参数清单2.1 端口映射-p 的几种写法以及一个容易忽略的绑定地址默认情况下Docker 创建容器使用的是 bridge 网络容器有自己独立的 IP通常在 172.17.0.0/16 网段容器内的服务只能在容器网络里被访问。宿主机想访问容器里的服务就得做端口映射也就是把宿主机某个端口转发到容器的某个端口。最常用的写法是docker run -d --name web -p 8080:80 nginx这里的8080是宿主机端口80是容器端口。如果想限制只允许本机访问可以加绑定地址docker run -d --name web -p 127.0.0.1:8080:80 nginx这样外部机器无法通过宿主机 IP 的 8080 端口访问只有宿主机自己能访问到。很多安全要求比较严格的内网服务我会建议用这种方式避免服务直接暴露在整张网络上。多个端口就写多个-pdocker run -d --name app -p 8080:8080 -p 9090:9090 myapp:latest如果你懒得指定端口只想让系统随机分配宿主机端口可以用大写-P它会把镜像里EXPOSE的端口全部映射到宿主机随机端口。配合docker port命令查看实际映射结果。这里有个实操经验创建后的容器端口映射不能直接修改。想换端口唯一的办法是删掉容器重新创建。所以创建前一定规划好端口尤其多个项目共用一台宿主机时端口规划混乱是运维事故的高发源头。2.2 数据卷挂载-v 与 --mount数据不能只留在容器里容器本身是可写的但容器层是临时的容器一删所有没挂载持久化的数据全部没了。所以数据库、日志、上传文件这类数据必须挂载到宿主机目录或 Docker 命名卷里。常用写法docker run -d --name web \ -v /srv/www:/usr/share/nginx/html:ro \ nginx-v /srv/www:/usr/share/nginx/html:ro中的前半段是宿主机目录后半段是容器目录后置的:ro表示只读挂载。对于 Web 静态文件这种只需要读的场景只读挂载可以避免容器内进程意外修改宿主机文件。另一种挂载类型是 Docker 管理的卷docker run -d --name mysql8 \ -v mysql-data:/var/lib/mysql \ mysql:8.0这里mysql-data是 Docker 卷名由 Docker 自动管理数据存放在 Docker 的数据目录下。好处是备份、迁移、权限管理都更规范不用关心卷到底落在宿主机哪个目录。如果你用较新的语法更推荐--mountdocker run -d --name web \ --mount typebind,source/srv/www,target/usr/share/nginx/html,readonly \ nginx--mount的语义更清晰键值对形式不容易写错位置。不过要注意一个坑--mount typebind要求宿主机 source 目录必须存在否则直接创建失败而-v在某些情况下会自动帮你创建目录。我实际踩过几次所以脚本里如果用的是--mount会在前面先mkdir -p。2.3 环境变量与启动命令-e、--env-file 和覆盖命令很多镜像在创建容器时需要通过环境变量来初始化配置。典型的就是 MySQLdocker run -d --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDroot123 \ -e MYSQL_DATABASEblog \ -v mysql-data:/var/lib/mysql \ mysql:8.0MYSQL_ROOT_PASSWORD是 root 密码MYSQL_DATABASE是初始化时自动创建的数据库名。漏传任何一个必需变量MySQL 容器启动后可能反复重启日志里全是初始化失败信息。变量多了之后命令行会变得很长很乱。这时可以用--env-filedocker run -d --name app --env-file .env myapp:latest.env文件的格式很简单一行一个KEYVALUE不需要加引号。这样做还有个好处避免密码直接出现在 shell 历史记录里。覆盖启动命令的场景也很常见。比如你想让一个容器临时去执行某个脚本但镜像默认的 CMD 不是这个脚本就可以在镜像名后面直接跟命令docker run --rm -v $PWD:/app -w /app alpine sh -c echo hello cat config.txt-w /app是设置工作目录相当于在容器内先执行了一次cd /app。这个组合在做一次性数据处理任务时特别方便。2.4 资源限制别让一个容器吃垮整台机器容器共享宿主机内核默认情况下一个容器能使用宿主机全部 CPU 和内存。这在单机部署多个容器时是灾难级的风险某个容器内存泄漏可能把整机拖垮其他容器全被 OOM。所以创建容器时资源限制我几乎必加docker run -d --name app \ --memory1g \ --cpus0.5 \ --pids-limit128 \ myapp:latest--memory1g限制容器最多使用 1GB 内存--cpus0.5限制容器最多使用 0.5 个 CPU 核心--pids-limit128限制容器内进程数能有效防止 fork 炸弹一类的问题。注意一个细节--memory-swap默认情况下会比--memory大一倍如果你不想要 swap建议显式设置跟--memory相同或者直接关闭 swap。否则容器实际可用的内存可能比你预期的大很多。我见过不止一次内存明明限了 1GB容器却跑出了远超 1GB 的用量一查发现 swap 没限制。如果内存超限内核会直接掐死容器的主进程表现就是容器状态变成Exited(137)或 OOMKilled。这种情况用docker inspect能看到OOMKilled: true。2.5 命名、网络和重启策略创建时一次定好--name给容器起名字不指定的话 Docker 会随机生成一个名字。生产环境强烈建议显式命名否则日志和监控里看docker ps的输出全是随机单词定位问题很痛苦。--restart是容器退出后的重启策略可选值有策略行为no容器退出后不自动重启默认on-failure[:次数]非正常退出时自动重启可限制次数always总是自动重启包括崩溃后、Docker daemon 启动后unless-stopped自动重启但如果手动 stop 过就不在 daemon 启动时拉起生产环境的守护型服务我一般用--restartunless-stopped。它和always的区别在于always在 Docker 服务重启后即使是手动 stop 过的容器也会被拉起来这有时会让你莫名其妙地发现某个容器又活了unless-stopped则尊重手动 stop 的状态。还需要清楚一点重启策略只针对容器进程异常退出和 Docker daemon 重启这两种情况不涵盖docker stop手动停止的容器。手动停了就是停了除非你手动docker start。--network决定容器加入哪种网络这个后面专门讲但创建时一定要想清楚。3. 按场景挑命令后台服务、一次性任务、交互调试与网络模式选择3.1 后台服务容器-d 与端口、卷、环境变量、重启策略的组合最典型的场景是跑一个常驻后台服务Nginx、MySQL、Redis 这些都属于这种。组织命令时我一般遵循一个顺序名字、资源限制、重启策略、端口映射、环境变量、数据卷、镜像和 tag。docker run -d \ --name nginx-web \ --restartunless-stopped \ --memory256m --cpus0.5 \ -p 8080:80 \ -v /srv/www:/usr/share/nginx/html:ro \ nginx:1.27-alpine-d是后台运行模式。没有-d容器主进程会占据当前终端输出全部打到屏幕上关掉终端容器也跟着停。加了-dDocker 把标准输出接到日志系统终端释放出来干别的。MySQL 容器除了端口和卷还必须注意环境变量docker run -d \ --name mysql8 \ --restartunless-stopped \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDroot123 \ -e MYSQL_DATABASEblog \ -e TZAsia/Shanghai \ -v mysql-data:/var/lib/mysql \ mysql:8.0注意时区环境变量TZAsia/Shanghai很多镜像默认时区是 UTC日志时间比本地时间差 8 小时排查问题时容易造成错觉。3.2 一次性任务容器--rm 和命令覆盖有些容器是用来跑一次性任务的比如 CI 里的构建、批量数据处理、临时测试。这种容器跑完就该消失留着只会占用磁盘空间。用--rm就行docker run --rm \ -v $PWD:/work \ -w /work \ alpine sh -c echo done result.txt--rm的效果是容器主进程退出时自动把容器删除。注意--rm和-d不能同时用而且它会在容器停止后先执行删除所以如果你还想docker logs查看输出--rm会让你没有机会看日志。需要看日志时就不要加--rm。3.3 交互式调试容器-it 与 exec、attach 的区别调试环境是另一个高频场景。拉一个 Ubuntu 或 Alpine 容器进去装工具、看文件、跑测试docker run -it --name devbox ubuntu:22.04 bash这里的-i是保持标准输入打开-t是分配一个伪终端。很多人记不清为什么要两个一起写——没有-i你敲键盘的输入容器收不到没有-t命令没有终端语义连输出颜色和交互控制都没有。所以“进入容器敲命令”的场景基本就是-it成对出现。容器已经在后台运行了想进去执行命令用docker execdocker exec -it nginx-web bashdocker exec是在运行中的容器里额外启动一个新进程不会影响容器主进程。而docker attach是连接容器主进程的标准输入输出用起来像是直接坐在容器终端前但一个不小心按了 CtrlC会把信号发给主进程容器可能直接退出。生产环境我基本只用exec很少用attach。3.4 四种网络模式怎么选以及 hot 场景下的 host 网络需求创建容器时--network决定了容器接入什么样的网络网络模式说明适用场景bridge默认模式容器有独立 IP通过端口映射对外绝大多数服务容器host容器直接使用宿主机网络栈无独立 IP-p失效性能敏感、需要大量端口none容器没有网络接口离线任务、安全要求极高的场景container:容器名新容器共享另一个容器的网络栈sidecar、服务网格模式host模式经常被误解成“更简单、更高效”。确实它性能高也不需要端口映射宿主机能直接访问容器端口。但它的代价是容器失去网络隔离端口占用直接冲突创建多个用同端口的服务根本跑不起来。除非明确知道自己在做什么否则默认还是用 bridge。有些人在部署 Web 服务时发现 bridge 模式下外部访问不到容器于是改成--networkhost问题立刻消失。这确实是一种绕开端口映射问题的手段但属于拆东墙补西墙更稳妥的做法是检查 bridge 网络的双向访问docker network inspect bridge看端口映射和网关配置是否正常然后再考虑是不是真的需要 host 模式。4. 容器已经创建了启动、检查与状态管理4.1 docker ps 与 docker ps -a创建结果在哪里看容器创建完但没有启动时在docker ps里是看不到的它只显示running状态的容器。必须用docker ps -a才能看到所有容器包括Created、Exited、Up等各种状态。我排查问题时第一步永远是docker ps -a先把“容器到底存不存在、处于什么状态”搞清楚再往下查。4.2 docker inspect 查看创建时的详细配置docker inspect是最重要的排障工具之一它能输出容器的完整 JSON 配置包括网络、挂载、环境变量、Entrypoint、CMD、重启策略等。想看指定字段时配合grep或者jqdocker inspect nginx-web --format {{.HostConfig.PortBindings}} docker inspect nginx-web --format {{json .Config.Env}} docker inspect nginx-web --format {{.HostConfig.RestartPolicy.Name}}使用--format而不是直接看一大坨 JSON效率高很多。创建阶段如果怀疑参数没生效用docker inspect一眼就能确认。4.3 start、restart、stop、rm 对已创建容器的影响docker start 容器启动一个已创建但未运行的容器。docker restart 容器停止再启动适合改了外部配置后让容器重新加载。docker stop 容器先给主进程发 SIGTERM等待宽限期超时后发 SIGKILL。默认宽限期 10 秒可用-t调整。docker rm 容器删除容器。默认不能删除运行中的容器需要-f强制加了--rm创建的容器在停止后会自动删除。这里有个操作习惯修改容器配置端口、环境变量、卷时我从来不会去想“改一下正在运行的容器”因为 Docker 根本不允许。唯一路径是删掉后重新创建。所以重要服务的启动命令我都会整理成脚本或 compose 文件而不是随手打在命令行里。4.4 从日志判断容器创建后的运行状态容器启动失败但状态停在那里时日志是最直接的证据docker logs mysql8 docker logs -f --tail 50 nginx-web-f是跟随输出--tail 50是只看最后 50 行。创建容器后如果发现有异常先别删先用logs看看主进程到底输出了什么。很多时候端口冲突、环境变量缺失、数据目录权限错误都会直接打印在日志里。5. 创建容器失败的常见原因与完整排查链路5.1 容器创建后立即退出先看状态再查日志遇到容器创建后立刻退出不要急着重新docker run。先看状态docker ps -a | grep 容器名如果状态是Exited(1)一般是应用本身报错比如配置文件缺失、初始化失败。如果状态是Exited(137)基本可以断定是被 OOM 杀了。如果状态是Exited(0)说明主进程正常退出而不是崩溃——比如你可能不小心在镜像名后面跟了一条普通命令命令执行完进程退出容器也就结束了。然后看日志docker logs 容器名日志里会直接告诉你失败原因。MySQL 我见到的典型报错是mysqld: Cant create/write to file多半是挂载目录权限问题Nginx 报directory index则多半是 Web 根目录没挂对。5.2 端口冲突Bind for 0.0.0.0:8080 failed这条报错应该是每个用过 Docker 的人都见过的docker: Error response from daemon: driver failed programming external connectivity on endpoint web: Bind for 0.0.0.0:8080 failed: port is already allocated.原因很简单宿主机 8080 端口已经被人占了。排查分两步先看 Docker 自己有没有占用docker ps -a --format table {{.Names}}\t{{.Ports}}如果 Docker 里没有占用再用系统命令看宿主机进程ss -ltnp | grep 8080找到占用进程后要么停掉它要么换端口重新创建容器。注意如果你不想让外部访问该服务完全可以不映射端口让容器只在 Docker 内网里被其他容器访问。5.3 镜像问题Unable to find image 和 tag 漂移创建容器时报Unable to find image nginx:latest locally是正常的Docker 发现本地没有就会去拉取。拉取失败时常见原因是镜像名拼错、tag 不存在或者网络原因导致拉取超时。这里有个值得养成的习惯创建容器时指定具体 tag不用latest。latest是漂移的今天部署的mysql:latest和三个月后部署的mysql:latest可能完全是两个版本升级造成的不兼容问题很难排查。我个人在创建命令里一律写死 tag比如mysql:8.0.36、nginx:1.27-alpine。5.4 挂载目录权限和平台相关的特殊问题挂载目录权限问题非常经典。容器内进程如果以非 root 用户运行而挂载进去的宿主机目录属于 root那容器内进程就没有写权限。典型报错是 MySQL 提示无法写入数据目录。解决思路通常是修改宿主机目录属主让容器内用户 UID 和宿主机目录属主对应或者干脆用 Docker 命名卷它的权限由 Docker 管理通常不会出现这类权限错位。Windows 上的 Docker Desktop 也有自己独特的坑。有时候创建容器时网络配置无法生效报错信息里会出现类似0x8007273f的错误码这是 Windows 网络子系统层面的异常常见于 WSL2 网络状态异常或 Docker Desktop 的虚拟网络适配器出问题。我遇到这种情况的基本处理顺序是先重启 Docker Desktop再不行就在管理员终端里执行netsh winsock reset然后重启系统。这个操作会重置 Windows 的网络编程接口层很多容器网络创建失败的问题能靠它解决。注意这是一步比较重的操作会影响宿主机上所有依赖 Winsock 的程序执行前要确认时机合适。5.5 资源限制与 OOM 导致的启动失败最后一种常见失败是创建容器时给了太小的内存限制。容器能启动但只要业务稍微跑起来就立刻死掉。排查时先用docker inspect 容器名 --format {{.State.OOMKilled}}如果输出true说明容器主进程是被内核 OOM 杀死的。这时调整--memory或者--memory-swap限制把值调大再重建容器。如果你不想在容量上猜来猜去先看一段时间docker stats的实时使用量再确定合理的资源限制。6. 把创建命令交给 compose声明式创建多个容器6.1 为什么容器一多就用 compose 而不是继续敲 docker run当你要同时跑 Web、数据库、缓存三个服务时三条docker run也算勉强能维护。但服务多到十几个或者环境从开发、测试到生产都要重复部署时命令行方式就变得不可维护了。参数散落在 shell 历史里新人接手根本不知道当时为什么是这样创建的。Docker Compose 的解决思路是把容器的创建参数写在一个 YAML 文件里用一条命令创建和启动整个服务组。文件可以提交到 Git变更可评审、可回滚这是命令行方式做不到的。6.2 一个 compose.yml 的创建实例以一个简单的 Web 加数据库为例services: web: image: nginx:1.27-alpine container_name: nginx-web restart: unless-stopped ports: - 8080:80 volumes: - ./html:/usr/share/nginx/html:ro environment: - TZAsia/Shanghai mem_limit: 256m db: image: mysql:8.0 container_name: mysql8 restart: unless-stopped ports: - 3306:3306 environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: blog volumes: - db_data:/var/lib/mysql mem_limit: 1g volumes: db_data:然后在项目目录下执行docker compose up -dCompose 会自动创建并启动web和db两个容器同时创建它们所属的默认网络和声明的db_data卷。docker compose ps可以查看这个服务组的状态docker compose logs -f可以同时跟踪所有服务日志。注意一个细节up -d的语义是“按声明创建并启动”如果已经启动了再执行一次会根据文件变更做增量更新。创建阶段如果只是误操作改了个环境变量重新up -d后容器会按新配置重建。6.3 compose 与 docker run 的字段对照以及各自局限docker run 写法compose 字段说明--namecontainer_name容器名-p 8080:80ports: - 8080:80端口映射-e KEYVALUEenvironment: KEYVALUE环境变量-v /host:/containervolumes: - /host:/container卷挂载--restartunless-stoppedrestart: unless-stopped重启策略--memory1gmem_limit: 1g内存限制--networkxxxnetworks: [xxx]指定网络Compose 很适合标准化部署单元但也有局限。一次性的临时命令、需要交互式进去调试的场景、复杂 shell 拼接的场景直接用docker run更顺手。我的习惯是正式服务的创建参数一律写成 compose 文件进仓库本地临时调试随意用docker run --rm。6.4 一个容易踩的坑compose down 会不会删数据卷docker compose down默认会删除服务组对应的容器和网络但不会删除数据卷这本来是一个很贴心的设计。但如果你手滑加了-vdocker compose down -v它会连带把所有声明在该 compose 文件里的卷一起删掉。数据库数据如果只存在卷里这一步操作就是事故现场。我给自己定的规矩是任何 compose 环境里执行down -v之前必须确认已经做好了数据备份拿不准就不加-v数据卷留着也不占多少空间。最后分享一个我自己用下来的小习惯创建容器前先把镜像 tag 写死拉一遍docker pull nginx:1.27-alpine再创建容器。这样能把“拉取超时”和“容器创建失败”两个问题隔离开排查链路会清晰很多。容器创建这种操作看着简单真正稳定地跑起来靠的还是这些细节上的较真。
返回列表