
Docker 容器管理部署这个话题最近在开发者社区里讨论得很多。实际使用中大部分人不是卡在概念上而是卡在安装、启动、持久化、日志排查和批量编排这一连串具体操作上。“双栈工坊”这个场景很形象一套应用服务栈一套数据服务栈都用 Docker 来安装、启动、升级和排障。这篇文章我就按真实落地顺序拆一遍从安装 Docker 开始讲到部署 Nginx、MySQL、Redis、本地大模型容器再讨论日志、资源占用和常见故障。适合正在搭建本地开发环境的前后端开发、运维工程师和独立开发者最值得关注的不是 Docker 命令有多少而是怎么用最少的时间把一套服务稳定跑起来并在机器重启后仍然保证数据和配置不丢。1. 双栈工坊中的 Docker 到底承担什么角色1.1 先拆清楚“双栈”是哪两套栈双栈工坊里的“双栈”最常见的理解是“应用服务栈”和“数据服务栈”。应用服务栈负责业务逻辑、接口、前端页面和模型推理数据服务栈负责存储、缓存和中间件。比如一个典型的 Web 项目前端可以跑在 Nginx 容器里后端跑在 Java 或 Python 容器里MySQL 和 Redis 分别提供持久化存储和热点缓存。没有 Docker 的时候这套环境要么直接装在宿主机上要么用虚拟机。直接装在宿主机上最大的问题是依赖冲突。今天装 MySQL 8.0明天项目要 MySQL 5.7版本一换就很容易污染系统环境。虚拟机隔离性够但资源开销大启动慢而且分发给别人时也不好复现。Docker 的价值就是把“应用代码依赖的运行时环境”和“宿主机系统”隔离开来让同一台机器可以同时存在多套互不干扰的服务。另一种理解是把“双栈”看作“开发栈”和“运行栈”。开发环境用 Docker 起一套服务提交到服务器后生产环境也尽量用相同的镜像和编排文件启动。这样本地能跑服务器上大概率也能跑差距被尽量压缩。这也是容器化部署最值得关注的一点可复现性。1.2 没有 Docker 时部署问题出在哪很多人第一次接触“部署”时会遇到这样的场景开发同学说本地运行正常结果运维同学在服务器上一敲命令报错信息完全不一样。原因多半出在系统版本、依赖包、动态库、环境变量和配置文件差异上。部署问题的本质不是“代码写错了”而是“运行环境不能被稳定复现”。Docker 把运行环境打包进镜像里代码、依赖、配置、SHELL、系统库都被固化下来。镜像一旦构建完成放到不同机器上内部环境基本一致。还有一个容易被忽略的点团队协作。项目成员增加后新人入职第一件事往往是“把环境跑起来”。如果靠手动安装环境搭建文档可能过时依赖版本也可能对不上。用 Docker Compose 或 Dockerfile 描述完整环境后新人只需要一条命令就能启动整套服务省掉大量调试时间。1.3 什么人适合用这套方式如果你是前端开发者主要想用容器起一个 Nginx 或 Node.js 服务Docker 的命令不需要学太多掌握镜像、容器、端口映射这三个概念就能上手。如果你是后端开发需要管理 MySQL、Redis、MQ 等中间件我希望你能重点关注数据卷和网络配置防止容器删除后数据丢失。如果你是运维或全栈工程师docker-compose 与容器日志、资源限制会是你最常用的部分。日常学习也比平时轻松。多数开源项目都会提供 Dockerfile 或 docker-compose.yml你不必担心本地缺了什么依赖。比如想部署一个本地模型或知识库服务官方通常也会给出容器启动示例只需要改一下端口、目录和模型名称。2. 安装 Docker 前先确认宿主机条件2.1 Windows / macOS / Linux 的最低前置条件在 Windows 上安装 Docker Desktop核心依赖是 WSL2 或 Hyper-V。新版本 Docker Desktop 更推荐 WSL2因为启动速度更快内存占用也更可控。你需要在“启用或关闭 Windows 功能”里打开“虚拟机平台”和“适用于 Linux 的 Windows 子系统”然后执行wsl --set-default-version 2。注意如果电脑 BIOS 里没有开启硬件虚拟化安装过程可能正常但 Docker Desktop 启动会失败。macOS 上也有 Docker DesktopApple 芯片和 Intel 芯片都能用。Apple 芯片的机器默认拉取 arm64 镜像很多中间件都有对应的 arm 版本真实使用中如果遇到“镜像架构不匹配”的报错可以去换一个平台相关的 tag或者用 Docker 的--platform参数指定。Linux 服务器通常不安装 Docker Desktop直接安装 Docker Engine。以 Ubuntu 为例官方推荐使用 apt 仓库安装docker-ce、docker-ce-cli和containerd.io。服务器上如果装了旧版 Docker建议先卸载冲突包避免版本混乱。这里有一个很重要的判断标准Docker 不是装完就能使用所有功能的。你还需要保证当前用户有权限访问 Docker 套接字或者执行命令时带 sudo。平时测试可以临时用 sudo长期使用建议把用户加入 docker 组但要注意这等于给了该用户较高的系统权限多人共用服务器时要谨慎。2.2 安装 Docker Desktop 的常见启动失败热搜词里有一个很典型的问题Docker Desktop failed to start because virtualisation support wasnt detected。这里的“virtualisation support”指的是 CPU 硬件虚拟化支持或 Windows 虚拟化功能没有正常开启。排查顺序建议这样来看 BIOS 设置重启进入 BIOS确认 Intel VT-x 或 AMD-V 已开启。看 Windows 功能确认“虚拟机平台”和“适用于 Linux 的 Windows 子系统”都处于启用状态。看 Hyper-V 冲突如果系统同时开启了 Hyper-V但又没完全启用虚拟化平台Docker Desktop 也会启动失败。看 WSL 版本打开 PowerShell 执行wsl --status确认默认版本为 2。最后看日志Docker Desktop 的日志路径在%LOCALAPPDATA%\Docker\log如果前面几步都正常可以打开日志看具体报错。在 Linux 服务器上也有类似问题表现通常不是“桌面软件启动失败”而是Cannot connect to the Docker daemon。遇到这个提示时先执行systemctl status docker看守护进程是否运行再检查当前用户是否在 docker 组里。2.3 安装完成后的 3 条验证命令装完 Docker 后很多人会直接开始拉镜像结果镜像拉不下来或拉得慢第一反应是更换网络。我更建议先跑 3 条验证命令确认 Docker 本身没问题。第一条docker version这能看到 Client 和 Server 两部分的版本信息。如果只出现 Client 而 Server 报错说明守护进程没起来或者当前用户没有权限连接。第二条docker info可以看到容器数量、镜像数量、存储驱动、系统时间等基础信息。如果 Server 正常这里会输出大量配置重点是能确认“Server Version”存在。第三条docker run --rm hello-world这条命令会从 Docker Hub 拉取一个很小的 hello-world 镜像并执行。执行成功说明拉取、创建、启动容器的链路是通的。如果镜像下载慢可以检查是否配置了可信的镜像加速地址或者把网络切换后再试。下载慢不一定代表 Docker 坏了需要把这个判断区分清楚。3. 跑通第一个 Nginx 容器最小可用案例3.1 用一条命令启动 NginxDocker 入门最合适的案例不是 hello-world而是 Nginx。因为 Nginx 能通过浏览器直接访问结果直观而且涉及端口映射和目录挂载这两个概念是后面所有容器化部署的基础。执行下面这条命令docker run -d \ --name web-test \ -p 8080:80 \ -v ./html:/usr/share/nginx/html:ro \ nginx:alpine解释一下参数-d表示后台运行不占用当前终端。--name web-test给容器命名后续查看日志、停止、删除时都用这个名字。-p 8080:80把容器的 80 端口映射到宿主机的 8080 端口。不要直接映射到 80因为宿主机 80 端口可能已被占用而且通常需要 root 权限。-v ./html:/usr/share/nginx/html:ro把当前目录下的 html 目录挂载到容器内的 Nginx 网页目录。:ro表示只读宿主机改文件容器内部能立即看到。nginx:alpine是镜像名加 tagalpine 版本体积小适合学习和轻量部署。启动后在浏览器访问http://localhost:8080。如果出现 Nginx 默认页面说明容器启动成功。然后修改./html下的 index.html刷新页面就能看到自己的内容不需要重新构建镜像。这里最值得记住的原理是镜像负责“运行环境”数据卷负责“数据交换”。把网页目录挂载出来你就不需要每次改内容都进入容器内部操作。3.2 查看容器、日志和内部运行状态容器启动后最常用的三条命令docker ps docker logs web-test docker exec -it web-test shdocker ps只显示运行中的容器。如果想看所有容器包括已经退出的加-a。当端口映射失败或容器启动后立刻退出时docker ps -a能看到容器的状态字段是Exited。docker logs web-test查看 Nginx 的访问日志和错误日志。如果容器启动失败日志里通常会有明确提示。查看实时日志可以加-f刷新页面时能看到新的请求记录。docker exec -it web-test sh进入容器内部适合查看网络、文件路径和进程。进入后执行ls /usr/share/nginx/html能看到挂载进去的文件。这个操作不是为了改配置更多是排查容器内的实际状态。3.3 容器命名、停止、清理和端口冲突容器管理里最容易出问题的不是命令记不住而是命名和清理不统一。比如docker run --name web-test后换一个名字旧容器没有删除再启动时会提示“name is already in use”。解决办法不是随机改名字而是先执行docker rm -f web-test清理旧容器。停止容器可以用docker stop web-test docker rm web-teststop会发送停止信号然后等待容器退出。如果容器迟迟不退可以加-t指定等待时间。rm删除容器如果容器还在运行需要先 stop 或者用rm -f强制删除。端口冲突也是一个常见问题。执行docker run -p 8080:80时如果报port is already allocated说明宿主机 8080 端口已经被别的程序占用了。排查顺序是先确认宿主机上是谁占用了端口再决定换端口还是停掉旧服务。网上很多教程直接让“杀掉占用进程”但我不建议在批量任务或生产环境里这样做。端口冲突时优先选择换一个高位端口比如 8081、18080影响范围更小。4. 数据服务栈MySQL 8.0 与 Redis 主从容器化4.1 MySQL 8.0 容器环境变量、端口映射和持久化MySQL 是双栈工坊里数据栈的核心组件。容器化部署 MySQL 最关键的不是“能启动”而是“重启不丢数据”。一条比较完整的启动命令是这样的docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDYourPass123 \ -e TZAsia/Shanghai \ -v mysql-data:/var/lib/mysql \ mysql:8.0-e MYSQL_ROOT_PASSWORD是 MySQL 镜像要求的环境变量用于初始化 root 用户密码。生产环境不要用这种明文方式放在终端和 history 里更推荐用 docker-compose 里的 environment 文件或 Docker Secret后面第 8 章会提到。-v mysql-data:/var/lib/mysql创建了一个命名卷。命名卷的特点是容器删了卷还在。下次用同一个卷名启动新容器数据自动恢复。如果你用-v ./mysql-data:/var/lib/mysql就是把宿主机当前目录下的文件夹作为数据目录。两者都可以但命名卷更简单因为不需要考虑目录权限和路径混淆。启动后可以用docker exec -it mysql8 mysql -uroot -p进入 MySQL 客户端验证。这里有一个很典型的坑MySQL 8.0 默认 root 用户只允许 localhost 登录很多开发者从宿主机用 Navicat 或 DBeaver 连接时报Host xxx is not allowed to connect。解决办法不是改跳过授权表这种高风险操作而是创建远程用户并授予权限。CREATE USER app% IDENTIFIED BY AppPass123; GRANT ALL PRIVILEGES ON *.* TO app%; FLUSH PRIVILEGES;对于本地开发环境这是一个合理方案如果是生产环境%表示允许任意主机登录需要配合防火墙或安全组限制来源 IP不能盲目照抄。4.2 容器内 MySQL 的权限、时区和数据丢失问题容器化 MySQL 还有一个经常被忽略的权限问题数据目录的所有者和访问权限。如果直接把宿主机目录挂载进去宿主机目录的 UID 和容器内 MySQL 用户的 UID 不一致MySQL 可能无法写入数据文件启动日志里会连续出现权限不足的错误。处理权限问题最稳妥的方式是用命名卷而不是普通本机目录。Docker 会自动处理命名卷的权限初始化。如果确实需要挂载宿主机目录那就先确认宿主机目录的属主和属组然后调整成容器内 MySQL 用户对应的 UID。这个操作没有统一的固定答案要看你的系统用户和镜像基础层决定。时区问题同样影响日常使用。容器默认时区通常是 UTC数据库里记录的时间比北京时间早 8 小时。解决方式是在启动命令或 compose 文件里设置环境变量TZAsia/Shanghai。但要注意设置 TZ 能影响新写入的时间字段已经写入的数据不会自动转换。数据丢失问题是最需要提前防范的。有人会认为“容器还在数据就应该还在”但实际上容器的生命周期和数据的生命周期是两回事。容器被docker rm删除容器内所有文件都会丢失除非你把数据目录挂载到宿主机或命名卷里。判断标准很简单启动命令里没有-v的 MySQL一旦容器重建数据就没了。4.3 Redis 主从复制容器怎么搭Redis 容器化比 MySQL 轻量但主从复制涉及到容器网络和启动顺序。先创建一个自定义网络让容器之间可以直接通过容器名通信docker network create redis-net启动主节点docker run -d \ --name redis-main \ --network redis-net \ -p 6379:6379 \ redis:7启动从节点docker run -d \ --name redis-slave \ --network redis-net \ -p 6380:6379 \ redis:7 \ redis-server --replicaof redis-main 6379--network redis-net让容器共享同一个桥接网络。这样从节点里的redis-main会被解析为主容器的内网 IP而不是写死某个地址。redis-server --replicaof redis-main 6379是在容器启动时传入 Redis 启动参数告诉这个实例复制主节点数据。验证主从是否生效执行docker exec -it redis-main redis-cli info replication看输出里的connected_slaves:1。如果从节点连不上先看网络是不是同一个再看两个容器是否在运行最后再看 Redis 日志。这里有一个常见的误区想用 Redis 做高可用主从复制只解决数据冗余和读扩展不能自动做主从切换。如果主节点挂了需要手动把从节点提升为主或者使用 Redis Sentinel 等组件。Docker 里跑 Redis 主从适合学习和读写分离测试生产环境需要额外设计哨兵或集群方案。4.4 容器访问外部地址的几种方式让容器访问宿主机上的服务或外部网络是双栈工坊里经常遇到的网络需求。容器默认可以访问外部网络但“外部地址”如果是宿主机本身事情会变得复杂。在 Docker Desktop 的 macOS 和 Windows 环境中容器内访问宿主机服务可以用host.docker.internal这个地址。比如宿主机有一个服务监听 8080容器内可以通过http://host.docker.internal:8080访问。在 Linux 服务器上host.docker.internal不一定默认存在。更通用的办法是使用--network host启动容器容器直接共享宿主机网络栈这时用127.0.0.1就能访问宿主机服务。缺点是端口不能做独立映射容易冲突。使用默认 bridge 网络然后在容器内用宿主机的内网 IP 访问。这个 IP 可以通过ip addr查看不一定是 127.0.0.1。在 docker-compose 里通过extra_hosts添加一条映射extra_hosts: - host.docker.internal:host-gateway这个方式在 Linux 环境中比较实用。注意这几种方式和访问外网没有关系只解决容器与宿主机之间的服务互通问题。5. 应用服务栈用 docker-compose 编排 Web 和依赖5.1 为什么不能一直手动 docker run单容器用docker run完全够用但双栈工坊里通常要同时管理 Nginx、MySQL、Redis、后端服务多个容器。如果每个容器都手动输入docker run会出现三个问题第一命令太长。每次启动都要输入端口映射、数据卷、环境变量和重启策略不仅累而且容易敲错。第二依赖关系容易乱。后端服务要在 MySQL 和 Redis 启动之后才能连接成功。如果你先启动后端再启动中间件后端可能连接失败后重试几次也可能直接退出。手动控制容器启动顺序可以做到但不直观。第三配置不统一。A 开发者的启动命令和 B 开发者的启动命令可能不一样比如端口不同、密码不同最后导致“本地跑通但协作时对不上”。docker-compose 的作用就是把一组容器的配置写成一个 YAML 文件统一启动、停止和管理。它不算 Kubernetes 那样的大规模编排但已经能覆盖本地开发和小型生产部署的大部分需求。5.2 一个包含 Nginx、MySQL、Redis 的 compose 示例下面是一个可以直接运行的 docker-compose.yml 示例。这里的版本号、镜像名、密码都是示例实际使用时要先确认镜像仓库里确实存在对应 tag。version: 3.9 services: web: image: nginx:alpine container_name: web-app ports: - 8080:80 volumes: - ./html:/usr/share/nginx/html:ro restart: unless-stopped depends_on: - mysql - redis mysql: image: mysql:8.0 container_name: mysql8 environment: MYSQL_ROOT_PASSWORD: YourPass123 TZ: Asia/Shanghai ports: - 3306:3306 volumes: - mysql-data:/var/lib/mysql healthcheck: test: [CMD, mysqladmin, ping, -h, localhost, -uroot, -pYourPass123] interval: 10s timeout: 5s retries: 3 redis: image: redis:7 container_name: redis7 ports: - 6379:6379 restart: unless-stopped volumes: mysql-data:这个文件包含了几个关键点。version字段现在可能不再必需但写上可以明确文件格式。container_name是自定义容器名如果不写compose 会自动生成“项目名-服务名-序号”的容器名也不难记。restart: unless-stopped表示容器异常退出后自动重启手动 stop 后不自动启动适合开发环境。生产环境通常用always或unless-stopped之一看维护习惯。depends_on控制启动顺序。web 服务会在 mysql 和 redis 之前启动注意这里只是“先启动”的保证不是“就绪”的保证。也就是说 mysql 容器可能正在初始化还没监听端口web 服务就已经开始尝试连接了。所以我在 mysql 服务里加了healthcheck进一步判断 MySQL 是否真的可连接。compose 支持depends_on后面配合condition: service_healthy但这里保持简单写出来之后可以自行扩展。5.3 启动、重启、查看日志和删除的常用操作在 docker-compose.yml 所在目录下执行docker compose up -d-d表示后台运行。第一次执行会先拉取镜像然后按依赖顺序启动服务。查看服务状态docker compose ps查看某个服务的实时日志docker compose logs -f web重建某个服务。当 nginx 镜像 tag 变化或者配置改了之后可以在容器存在的情况下重新创建docker compose up -d --force-recreate web停止并删除所有服务docker compose down注意down默认不会删除命名卷。如果加了-v命名卷也会被删除MySQL 数据会一起清掉。这个参数要非常谨慎批量任务或生产环境里确认备份没问题再执行。5.4 失败重试、健康检查和重启策略compose 在真实使用中的核心不是“能把容器拉起来”而是“容器挂了之后能不能按预期恢复”。重启策略建议这样理解no容器退出后不自动重启适合调试。always容器退出后总是自动重启包括 Docker 守护进程启动时。unless-stopped容器退出后自动重启但如果你手动 stop 过Docker 不会在下次启动 Docker 时把它带起来。这个选项更适合开发机。on-failure只有异常退出才重启适合临时任务。健康检查是另一个层级的保障。一个容器进程可能还活着但内部服务已经无法响应了。比如 MySQL 进程没退出但崩溃循环重试中docker ps看起来是 Up实际连接却失败。healthcheck 可以定期执行检查命令如果连续多次失败容器状态会变成unhealthy。在 compose 里通过depends_on加condition: service_healthy可以让依赖方等待真正就绪。绝大多数情况下单机部署使用restart: unless-stopped加healthcheck已经足够。如果服务有更多实例、需要自动扩缩容那就不是 docker-compose 的主场了可以再考虑 Kubernetes 和 HPA 这类容器编排架构。6. 本地大模型容器化Ollama 与 DeepSeek 部署6.1 为什么本地大模型也适合容器化本地大模型部署是最近热度很高的场景。很多人想跑 DeepSeek、Ollama 这类本地模型最难的不是模型本身而是运行环境Python 版本、CUDA 驱动、依赖库、模型文件路径。手动安装时一个 PyTorch 版本不匹配就可能导致模型跑不起来。用容器把模型运行时包装起来可以解决依赖隔离问题也让模型文件通过数据卷持久化。这样即使容器的镜像更新或换了一台机器只要模型目录被正确挂载就不需要重新下载几 GB 的模型权重。需要注意容器化不等于“没有资源要求”。模型本身占用磁盘推理时占用内存和显存。用容器部署只是把环境问题简化了硬件资源下限并不会因此消失。6.2 Ollama 容器部署步骤Ollama 是一个比较方便的本地模型运行工具官方也提供 Docker 镜像。这里给出一套通用示例具体镜像名称和模型名称以你实际拉取时看到的官方仓库为准。启动 Ollama 容器docker run -d \ -v ollama:/root/.ollama \ -p 11434:11434 \ --name ollama \ ollama/ollama如果你的机器有 NVIDIA GPU并且已经安装了对应的容器运行时可以加上docker run -d \ --gpus all \ -v ollama:/root/.ollama \ -p 11434:11434 \ --name ollama \ ollama/ollama-v ollama:/root/.ollama是保存模型文件和数据的关键。以后容器被删除模型不会丢。-p 11434:11434是 Ollama 默认端口后面应用通过这个端口调用模型。进入容器拉取模型比如 DeepSeek 系列模型docker exec -it ollama ollama pull deepseek-r1:7b模型名字里的 tag 表示不同大小和量化版本。常见的有 1.5b、7b、14b 等。第一次拉取会下载比较大的文件耗时和磁盘占用都要提前估算。如果你磁盘空间有限不要一次拉取多个模型先拉一个小尺寸模型验证流程再决定是否切换大模型。6.3 用 API 方式把模型接入应用Ollama 启动后暴露了一个 HTTP API应用服务不需要和容器内部文件直接交互只需要请求这个 API 即可。一个简单的调用示例curl http://localhost:11434/api/generate \ -H Content-Type: application/json \ -d { model: deepseek-r1:7b, prompt: 用三句话介绍 Docker 容器, stream: false }返回内容里会包含生成的文本、耗时统计和 token 数。stream设置为 false 表示等待完整结果返回如果设置为 true则服务端会按流式方式返回内容适合做打字机效果。在这个结构里模型容器和业务容器是解耦的。后端 Java、Python 或 Node 服务只要通过端口访问 Ollama API就能把大模型能力接入自己的应用。后续想升级模型版本只需要拉取新模型不需要改业务代码。6.4 低配置机器的降级思路如果你的机器显存不足或者没有独立显卡可以先不追求大模型。几个可行的降级思路使用更小尺寸的模型比如 1.5b 或 3b 的量化版本。在纯 CPU 环境先跑小模型速度慢但能验证流程。关闭 Ollama 的并发请求处理一次只推理一个任务减少资源抢占。磁盘空间不足时用docker exec -it ollama ollama rm 模型名删除不用的模型释放磁盘。本地大模型容器的资源判断依据不是“容器启动了就算成功”而是看推理是否正常返回、平均耗时是否符合预期、连续几轮是否稳定。如果输入相同问题两次输出内容差异很大或者中途报 OOM说明模型大小或量化级别可能超过了当前机器承受范围。7. 容器日志、资源占用和 Java 应用崩溃排查7.1 日志文件去哪看不能只看 docker ps容器只看“Up”或“Exited”远远不够。一个容器状态是 Up但业务接口持续 500底层实际上是日志刷屏、死循环或依赖没有连上。只看进程状态很容易误判。查看容器标准输出日志docker logs web-test只查看最近 200 行docker logs --tail 200 web-test跟随日志输出docker logs -f web-test注意docker logs只能看到容器内主进程写到 stdout 和 stderr 的内容。Java 应用的 System.out、Spring Boot 的默认日志、Nginx 的访问日志一般都会输出到标准输出所以能看到。如果你在应用里配置了日志写到文件但文件没有重定向到 stdoutDocker 看不到这些内容需要把日志文件挂载到宿主机目录。生产环境还应该限制日志文件的大小。Docker 默认的 json-file 日志驱动会一直把日志写到宿主机长时间运行后可能占用几个 GB。可以在 docker-compose 里配置logging: driver: json-file options: max-size: 50m max-file: 5这样每个容器日志超过 50MB 后会轮转最多保留 5 个文件。7.2 Java 容器异常重启JVM 日志在哪里热搜词里有一个很真实的问题Docker 里部署的 Java 程序异常重启JVM 日志在哪儿。这个问题很容易踩坑因为很多人找不到日志的第一反应是“容器日志被清掉了”实际可能是根本没配置 JVM 日志输出。Java 程序的日志分为两层。第一层是应用日志也就是 Spring Boot 或业务代码里打印的运行日志第二层是 JVM 自身日志包括 GC 日志、OOM 堆转储和类加载问题。应用日志可能通过 logback、log4j 写到文件也可能写到标准输出。JVM 日志默认也可能不输出或者只写到标准错误。排查思路建议这样先查看容器是否被 OOM 杀死docker inspect --format{{.State.OOMKilled}} 容器名如果输出 true说明容器内存超限被内核杀掉。查看容器退出码docker inspect --format{{.State.ExitCode}} 容器名JVM 正常退出通常返回 0异常退出可能是 1、137、143 等。137 常见于被 OOM 杀掉。查看容器日志docker logs --tail 200 容器名如果有 OutOfMemoryError 或 GC overhead limit exceeded那就是 JVM 堆内存不够。如果还没有明确信息需要主动配置 JVM 日志落盘。启动 Java 容器时通过命令参数把 GC 日志和堆转储写到挂载目录。示例docker run -d \ -v ./jvm-logs:/logs \ my-java-app \ -Xlog:gc:/logs/gc.log \ -XX:HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath/logs/heapdump.hprof注意启动参数的位置要正确。很多镜像里 Java 应用的启动入口可能是java -jar app.jar这时 Java 参数要放在-jar之前或根据具体启动脚本调整。这个问题的根本原因是容器把文件系统隔离了但你却没有把 JVM 日志目录挂到宿主机。容器一旦重启容器内文件系统就被丢弃或重置日志自然就没了。解决方式不只是“找日志”而是提前把日志目录、输出路径和数据卷规划好。7.3 用 docker stats 判断资源瓶颈排查容器问题时不能只凭主观感受判断快或慢。docker stats可以看到每个运行中容器的 CPU、内存、网络 I/O 和磁盘 I/O 实时数据。docker stats如果要限制容器资源可以在启动命令里加docker run --memory2g --cpus2 my-app在 compose 文件里对应services: app: image: my-app deploy: resources: limits: memory: 2g cpus: 2.0资源限制的意义在于防止单个容器把整台机器的 CPU 或内存吃满。本地开发可能感觉不到这有多重要但当你在一台服务器上同时跑多个容器时一个没有资源限制的 Java 应用可能拖垮其他容器。7.4 启动失败的标准排查链路遇到容器启动失败不要急着改参数。按下面顺序排查会更高效看现象。是启动立即退出还是启动后运行一段时间退出还是状态一直 restarting。看日志。先执行docker logs --tail 100 容器名。很多问题在日志里已经有明确答案。看输入格式。挂载的配置文件路径、环境变量名称、端口号对不对。比如 MySQL 的配置文件写错一个缩进容器可能启动失败。看资源占用。执行docker stats或free -h确认内存、磁盘、inode 是否耗尽。看权限。数据卷目录、挂载文件、日志目录是否可写。看依赖版本。镜像 tag 是否存在容器内程序版本和宿主机驱动是否匹配。这个顺序不是万能的但能减少很多无效尝试。我见过大量案例其中不少是路径打错或配置缩进有问题这些问题改参数改不出来必须回到输入和日志。8. 把 Docker 从“能跑”推进到“生产可用”8.1 哪些场景不适合只用 DockerDocker 解决了环境隔离和部署效率问题但不是所有场景都适合用它。这一点要先有边界感。本地开发、测试环境、中小型服务的单机部署Docker 非常合适。但如果你需要一个 MySQL 高可用集群涉及主主同步、自动故障切换、读写分离和备份恢复单纯用 docker-compose 起几个容器是远远不够的。容器本身解决的是“运行环境一致”不是“分布式系统自动运维”。当业务需要多个服务实例按流量自动扩缩容靠restart: always也不够。这时一般会引入 Kubernetes 或容器编排平台通过 HPA 等机制实现水平扩缩容。容器镜像依然是基础但管理工具需要升级。不要因为学会 Docker 就认为所有部署问题都解决了后面还有监控、日志采集、配置中心、服务发现等环节。8.2 镜像和容器的安全检查清单容器安全不是一个漂亮的词而是真实存在的风险点。这里给出一份通用的检查清单尽量使用官方镜像或可信来源镜像。不要在不知名仓库随便拉镜像尤其是敏感环境。固定镜像 tag。不要一直用mysql:latest而是指定mysql:8.0或更精确的版本。latest 会随仓库变化导致不同时间启动的容器环境不一致。不要把密码写在命令行里。命令行记录、bash history 都可能在后续被查看。在 compose 中使用.env文件并确保该文件被.gitignore忽略。不要把宿主机的重要目录随意挂载进容器特别是只读保护需要明确。生产容器考虑以非 root 用户运行应用。很多镜像提供USER指令或运行时参数。定期更新镜像和容器及时处理漏洞。容器不是安全隔离伞它也会包含存在漏洞的系统组件。8.3 部署前要确认的 6 个参数在正式部署一套容器化服务前我会先逐项确认下面这些参数。它们直接影响服务是否稳定端口宿主机端口是否已被占用容器端口与外部端口映射是否准确。持久化数据目录、日志目录、模型目录、配置目录分别挂载到哪个卷或目录容器删除后数据是否还在。重启策略容器异常退出后是否自动重启使用always还是unless-stopped。时区和编码容器内时区是否设置为本地时区数据库字符集是否满足业务需求。日志策略日志驱动是否限制大小日志文件是否挂载到宿主机构便于收集。资源限制CPU、内存是否限制是否存在某个容器独占资源的风险。这些参数在 compose 文件里都有明确位置。把它们提前确认清楚比等出问题后临场排查快得多。8.4 我会遵守的三个落地习惯写到最后分享三个我自己一直遵守的落地习惯。第一先跑单条任务再开批量。不管是部署 Nginx、MySQL 还是本地模型先跑一个最小容器验证输入、输出和日志确认没问题后再把参数扩展到业务集群或批量任务。不要一上来就拉起 10 个容器然后发现公共配置写错了。第二先看日志再改参数。遇到启动失败先docker logs再检查输入文件、环境变量和权限。不要反复调整并发数或资源限制那样容易掩盖真实问题。第三批量任务要考虑失败重试和输出命名。如果业务场景是批处理文件容器每次处理一个文件那就要设计清晰的输入目录、输出目录和重试机制。Docker 做批量任务时“能跑通”和“能长期稳定跑完几百个任务”是两件完全不同的事。双栈工坊里的 Docker 管理部署核心就是让复杂的运行环境变得可复制、可维护。如果你刚开始接触先不要追求一次性掌握所有命令。把 Nginx、MySQL、Redis 这三个容器跑通理解端口、数据卷和日志就已经覆盖了日常开发中绝大多数场景。等真正遇到生产环境的多实例、高可用需求再去学习编排、监控和容器安全也不晚。