
我第一次真正意识到 Docker 的价值是在公司新发的电脑上复现旧项目的时候。装 Python、装数据库、配环境变量折腾了一下午最后服务还是起不来后来发现是依赖版本和源码不匹配。后来我把整个项目封装进 Docker 镜像换到哪台机器都是“一条命令启动”这种体验确实让人上瘾。这篇笔记不是理论科普而是把我从安装 Docker 到实际部署 MySQL、Redis、GitLab再到排查容器网络、权限、启动失败等问题的完整过程整理出来重点写那些文档里不细讲、但实际动手时一定会踩的细节。如果你正准备学 Docker或者已经在用但经常被各种报错卡住接下来的内容基本都是可以直接照着操作的。1. 先弄清 Docker 解决了什么再动手安装1.1 容器其实是“打包好的运行环境”Docker 的核心思路可以理解成“把运行环境一起打包”。以前部署一个项目除了代码本身还要保证操作系统、运行库、依赖版本、配置文件全部正确换成容器之后这些内容全部被固化在镜像里任何安装了 Docker 的机器都能用同一套环境启动。用一个类比来说代码是菜谱环境是厨房。以前每次换地方做菜都要重新准备一模一样的厨房换一个厨具就翻车Docker 相当于把菜谱连同整个厨房一起装进集装箱搬到哪儿都能直接开火。镜像Image是只读的模板容器Container是镜像运行后的实例。同一个镜像可以启动多个容器每个容器之间互相隔离修改容器里的文件不会影响镜像本身。这种设计带来的直接好处就是开发、测试、生产环境可以做到高度一致不再出现“本地能跑服务器上跑不了”的尴尬。1.2 容器和虚拟机最大的区别是共享内核很多人第一次接触容器时都会问这跟虚拟机有什么区别虚拟机会虚拟出一整套硬件在硬件之上运行一个完整的操作系统所以启动需要几分钟占用内存动辄几个 GB。容器则完全不一样它直接使用宿主机内核只做进程级别的隔离启动时间通常是毫秒级镜像占用也小得多。代价是容器的隔离性不如虚拟机彻底。虚拟机之间是不同操作系统容器之间则共享同一个内核所以当宿主机内核出现漏洞时容器也会受影响。不过对于绝大多数应用部署场景这种隔离已经足够可靠。基于这个特性容器特别适合“同一个服务开多个副本”“快速搭建临时环境”“在资源有限的机器上跑尽量多的服务”这些场景。我见过有人在只有 4GB 内存的服务器上用 Docker 同时跑数据库、业务代码和消息队列这在虚拟机环境下基本不可能。1.3 为什么团队协作也离不开它Docker 对团队协作的价值甚至比对单机部署的价值更大。新成员入职不需要再按照文档一步步装环境老成员改完代码也不需要在群里反复说“我这边没问题”直接把镜像推上去大家跑同一个镜像就行。我在实际项目里最常用的一种协作方式是把项目从构建到启动的全过程写进 Dockerfile提交到仓库后任何人都能从源码构建出完全一样的镜像。只要 Dockerfile 写得清晰整个项目的技术栈、依赖版本、启动命令就都有了明确记录比任何环境文档都实时。2. 环境安装实录Windows 与 Linux 的差异2.1 Windows 上安装 Docker Desktop卡住最多的一步在 Windows 上安装 Docker绝大多数人的选择是 Docker Desktop。安装包本身没什么好说的下载、双击、下一步。真正让人卡住的是安装完启动时弹出的报错Docker Desktop failed to start because virtualisation support wasn’t detected。出现这个提示先别急着重装系统按顺序排查几个地方。第一检查 BIOS 里的虚拟化开关。Intel 的 CPU 叫 VT-xAMD 的 CPU 叫 AMD-V不同主板的 BIOS 界面差异很大但一定能找到类似 “Virtualization Technology” 的选项设为 Enabled。这一步最容易被忽略因为很多品牌机默认是关闭的。第二确认 Windows 功能里已经开启“虚拟机平台”和“适用于 Linux 的 Windows 子系统”。可以去“启用或关闭 Windows 功能”里勾选勾完必须重启。第三检查 WSL 的版本。Docker Desktop 依赖 WSL2如果系统里装的是 WSL1需要升级。在 PowerShell 里执行wsl --status查看默认版本如果显示的还是 WSL1执行wsl --update升级。我有一次在公司机器上折腾了一整个下午最后发现就是 BIOS 虚拟化没开好在这种问题只要定位到原因解决起来就是几分钟的事。2.2 Linux 服务器上用命令行安装 DockerLinux 服务器安装 Docker 的路径比 Windows 简单得多但不同发行版也有各自的坑。以最常见的 Ubuntu 为例最省事的方式是直接用系统自带的 docker.io 包sudo apt update sudo apt install docker.io sudo systemctl enable --now docker docker version如果要用官方发布的最新版本可以用官方脚本安装也可以配置官方 apt 仓库后再安装。生产环境建议使用固定版本不要盲目追 latest。CentOS 7 的情况稍微特殊默认源里的 docker 包版本很旧而且被改名过最好先卸载旧的版本再配置 Docker CE 仓库。装完后同样systemctl start docker如果配置了外部镜像源记得改完/etc/docker/daemon.json后systemctl daemon-reload再重启 docker。2.3 权限错误和守护进程启动失败两个最常见的问题Linux 下安装完 Docker第一次执行 docker 命令经常会看到permission denied while trying to connect to the Docker daemon socket这通常不是没装好而是当前用户不在 docker 用户组里。docker 命令需要访问/var/run/docker.sock这个 socket 默认只允许 root 用户和 docker 组访问。解决方法sudo usermod -aG docker $USER newgrp docker重新登录一次终端就能生效。注意把用户加入 docker 组等同于给了这个人 root 权限因为在容器里可以挂载宿主机的任意目录所以生产服务器上添加这个组别要慎重。至于 docker 服务启动失败现象通常是systemctl status docker显示 failed或者执行 docker info 时报Cannot connect to the Docker daemon。优先排查两件事第一看/var/lib/docker所在磁盘是否满了第二看journalctl -u docker的系统日志。以我遇到的案例来说八成以上都是磁盘空间不足或者之前异常断电导致 docker 数据目录需要清理。3. 拉取镜像和容器操作从 hello-world 到第一个服务3.1 镜像仓库、镜像源与下载慢的问题Docker 本身是一个客户端镜像都存放在镜像仓库里。最常用的公共仓库是 Docker Hub装好 Docker 后执行docker run hello-world如果能看到一段说明文字说明整个链路已经通了。但国内网络环境访问 Docker Hub拉取大镜像时经常慢到崩溃。解决思路不是找轮子而是给 Docker 配置镜像源。镜像源是仓库的前置缓存修改/etc/docker/daemon.json加一段 registry-mirrors{ registry-mirrors: [ https://docker.mirrors.ustc.edu.cn ] }改完重启 Dockersudo systemctl daemon-reload sudo systemctl restart docker不同镜像源的可用性和稳定性会随时间变化如果发现某个地址失效换一个就行。有一点要注意镜像源只影响 Docker 拉取镜像的流量不影响容器内的网络访问不要把这两个概念混在一起。3.2 容器生命周期操作速查日常使用频率最高的容器操作其实就那么几条。启动一个 Nginx 容器docker run -d --name mynginx -p 8080:80 nginx这条命令的含义-d是后台运行--name给容器命名-p 8080:80把宿主机的 8080 端口映射到容器内的 80 端口。之后浏览器访问http://宿主机IP:8080就能看到 Nginx 欢迎页。常用命令我整理成了一张表用途命令查看运行中的容器docker ps查看所有容器含已停止docker ps -a查看容器日志docker logs -f 容器名进入容器终端docker exec -it 容器名 bash停止容器docker stop 容器名删除容器docker rm 容器名列出本地镜像docker images删除镜像docker rmi 镜像名还有一个很实用的用法用一次性容器跑临时脚本不需要保留任何文件docker run --rm -v $PWD:/app -w /app python:3.11 python app.py这条命令把当前目录挂载到容器里的/app工作目录也切换过去直接用 Python 3.11 镜像执行脚本执行完容器自动删除。在 Ubuntu 服务器上临时跑一段 Python 脚本这种方式比装完整 Python 环境干净得多。3.3 镜像管理的一些细节镜像不是越大越好也不是 tags 越多越好。固定版本号有助于复现比如mysql:8.0就比mysql:latest更可控。拉取镜像后如果想减少磁盘占用可以用多阶段构建把编译环境和运行环境拆分最终镜像只保留运行所需内容。另外提醒一句不要轻易docker commit。把运行中的容器重新打包成镜像虽然方便但会产生大量不必要的文件也让镜像的构建过程不可追溯。正确的做法是写 Dockerfile每次构建都从源码走一遍保证镜像可复现。4. 经典部署案例MySQL 8.0 与 Redis 主从4.1 安装 MySQL 8.0 并持久化数据很多人第一次用 Docker 跑数据库最大的困惑是容器删了数据不就没了所以要理解“数据卷”这个概念。比如装 MySQL 8.0docker run -d \ --name mysql8 \ -e MYSQL_ROOT_PASSWORD你的数据库密码 \ -e MYSQL_DATABASEtestdb \ -p 3306:3306 \ -v /data/mysql:/var/lib/mysql \ mysql:8.0-v /data/mysql:/var/lib/mysql的意思是把宿主机上的/data/mysql目录映射到容器内的 MySQL 数据目录。无论容器怎么删除重建数据都留在宿主机目录里。这样容器只负责提供运行环境数据永远握在自己手里。启动过程中容易遇到的坑有三个。第一宿主机 3306 端口如果已经被本机 MySQL 占用要把映射端口改成3307:3306。第二如果/data/mysql目录里已经有残留文件MySQL 可能启动失败docker logs mysql8里通常能看到数据目录错误清空这个目录再重启即可。第三MySQL 8.0 默认认证插件是caching_sha2_password老客户端连接不上需要在 SQL 里给对应用户改成mysql_native_password。如果需要连接字符集万无一失启动时可以追加参数docker run -d --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD你的数据库密码 \ -v /data/mysql:/var/lib/mysql \ mysql:8.0 \ --character-set-serverutf8mb4 \ --collation-serverutf8mb4_unicode_ci4.2 Redis 主从复制容器搭建Redis 容器化部署同样简单而且主从结构用 Docker 跑特别顺手。先创建一个自定义网络让容器之间用容器名通信docker network create redis-net启动主节点docker run -d --name redis-master \ --network redis-net \ -p 6379:6379 \ redis:7 \ redis-server --appendonly yes启动从节点docker run -d --name redis-slave \ --network redis-net \ -p 6380:6379 \ redis:7 \ redis-server --replicaof redis-master 6379注意从节点连接的是容器名redis-master不是宿主机 IP。在自定义网络里Docker 自带的 DNS 解析会自动把容器名映射成对应 IP这种方式比直接写死 IP 更健壮——重启容器就算 IP 变了主从关系也不会断。验证是否配置成功docker exec -it redis-slave redis-cli info replication输出里如果看到role:slave和master_link_status:up说明主从已经好了。很多人在这一步失败是因为没有使用自定义网络直接在默认 bridge 网络里靠容器名互访这是行不通的。4.3 不只是数据库各种业务应用的容器化部署MySQL 和 Redis 只是最基础的两个例子。日常开发中能用 Docker 封装的应用非常多我简单列几个有代表性的KodBox 是一个网盘应用Docker 启动就是一条命令docker run -d --name kodbox -p 8080:80 -v /data/kodbox:/var/www/html kodbox。域名、端口、存储目录都通过挂载和端口映射搞定。Metabase 是数据分析可视化平台跑起来就三步拉取镜像、启动容器、浏览器打开 3000 端口。Dify 这类 LLM 应用开发平台依赖数据库、向量库、API 服务、Worker 等多个组件官方直接给了一套 docker compose 配置一条命令拉起一排容器。这些案例背后都有一个共同逻辑应用依赖的所有组件都被固化成镜像用户只需要关心端口、数据卷、网络这三个维度。5. 多容器编排Compose 与微服务打包部署5.1 docker run 命令太长Compose 应运而生当需要管理的容器越来越多一条条 docker run 命令会把所有人都搞疯。Docker Compose 就是来解决这个问题的用 YAML 文件把多个容器的启动参数全部描述出来之后一行命令就能启动整个服务栈。比较早的版本里命令叫docker-compose现在 Docker 官方已经集成了docker compose注意中间有空格。Desktop 版本自带Linux 环境下如果没有需要单独安装 compose 插件。判断办法很简单执行docker compose version有输出就说明支持。最基本的 Compose 文件长这样services: web: image: nginx ports: - 8080:80 redis: image: redis:7然后在文件目录下执行docker compose up -dup -d表示后台启动。之后的docker compose ps、docker compose logs -f、docker compose down分别对应查看、看日志、清退整个服务栈。5.2 用 Compose 部署 GitLab 社区版GitLab 社区版是很多团队自建代码仓库的选择但直接安装奇慢无比且依赖一堆组件。用 Docker 部署则是另一种体验。我先放一份可以直接用的 Compose 配置services: gitlab: image: gitlab/gitlab-ce:latest container_name: gitlab restart: always hostname: gitlab.example.com environment: GITLAB_OMNIBUS_CONFIG: | external_url http://gitlab.example.com ports: - 80:80 - 443:443 - 2222:22 volumes: - ./gitlab/config:/etc/gitlab - ./gitlab/logs:/var/log/gitlab - ./gitlab/data:/var/opt/gitlab提醒几点。第一GitLab 非常吃内存建议宿主机至少给 4GB 以上否则启动过程中会不断报 502。第二首次启动要几分钟甚至更久启动期间网页打不开是正常的不要急着删容器。第三SSH 端口映射用了 2222是为了避免和宿主机原有的 22 端口冲突clone 仓库时要注意地址里的端口号。5.3 微服务项目的常规打包与部署思路微服务项目用 Docker 部署核心不是某一条命令而是“每个服务一个镜像 Compose 串联”的思路。以常见的“前端 后端 数据库 消息队列”架构为例每个服务各自写一份 Dockerfile构建出独立镜像然后在 Compose 文件里把这些服务组织起来统一网络、统一卷、统一配置。给后端服务写 Dockerfile 时多阶段构建是非常实用的技巧。第一段用 Maven/Gradle 镜像编译源码第二段只拷贝编译产物放进精简运行镜像FROM maven:3.8-openjdk-17 AS builder WORKDIR /build COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests FROM openjdk:17-slim WORKDIR /app COPY --frombuilder /build/target/*.jar app.jar ENTRYPOINT [java, -jar, app.jar]最终镜像里没有源码、没有编译器只有一个可运行的 Jar 包体积小安全性也更好。微服务之间通过 Compose 服务名相互访问和 Redis 主从里的容器名通信是同一个原理。6. 网络不通容器网络的排错思路6.1 Docker 默认的网络模式容器网络是新手最容易懵的环节。“容器都启动了为什么访问不了”“两个容器为什么 ping 不通”这类问题每天都有。Docker 的网络模式其实不算复杂归纳起来就几种。默认是 bridge 模式。容器通过 NAT 访问外网对宿主机上的端口映射外部通过“宿主机 IP 映射端口”访问容器。这种模式下容器和宿主机共享一个虚拟网桥容器之间可以通过 IP 互访但用容器名互访不行除非自定义网络。host 模式直接共享宿主机的网络命名空间性能高但不够隔离在需要大量端口或追求低延迟的场景才用。还有一个 none 模式容器不配置网络适合只需要离线计算的场景。真正要重点掌握的是自定义网络。执行docker network create mynet docker run -d --network mynet --name web nginx docker run -d --network mynet --name db mysql:8.0这样web和db之间就能直接用容器名互相访问了天然解决了服务发现的 DNS 问题。6.2 典型的网络问题与排查步骤遇到容器网络不通我的排查顺序一向是固定的。先看容器是不是在同一个网络里docker network inspect 网络名查所有容器看每个容器网络的归属。如果不在同一个网络就不能互相用容器名访问连接方式换成 IP 也一样。再看端口映射有没有生效docker ps看 PORTS 列是否显示0.0.0.0:8080-80/tcp只有这样的映射外部才能用宿主机 IP 访问。容器内服务起来了但宿主机防火墙没放开端口同样访问不通。Ubuntu 上要看 ufwCentOS 上要看 firewalld。很多 Docker 网络问题的最终原因不是 Docker 配置错了而是宿主机的防火墙拦住了。最后进入容器内部测试docker exec -it 容器名 bash ping 目标容器名如果容器内解析不到目标容器名优先检查是否在自定义网络里。如果 ping 不通 IP就要考虑 DNS、路由、宿主机配置等多层原因。这种逐层排查的方式看起来慢但比瞎试命令有效得多。7. 进阶GPU 容器、镜像打包与特殊部署需求7.1 Ubuntu 上让容器使用 GPU随着大模型相关部署越来越多“让容器里跑着 Python 进程直接调用 GPU”成了很常见的需求。Docker 本身不支持直接访问 GPU需要在宿主机上安装 NVIDIA Container Toolkit。装好之后启动容器时加一个参数docker run --gpus all \ -v /data/models:/models \ nvidia/cuda:12.0-base \ nvidia-smi如果容器里能正常输出 GPU 信息说明显卡已经被容器“看到”了。现在很多 AI 推理服务比如用 vLLM 加载 Qwen 系列模型官方推荐的方式就是跑在 GPU 容器里。宿主机只需要准备好驱动和 toolkit剩下的 CUDA 环境、Python 依赖、模型文件都固化在镜像里特别适合多个模型服务在同一台机器上切换。7.2 IDEA 里一键生成 Docker 镜像开发阶段如果每次构建镜像都要手写 docker build遇到带上下文路径、多阶段构建时容易出错。JetBrains 系列 IDE 自带 Docker 插件可以在编辑器里直接连上本机或远程的 Docker 服务右键 Dockerfile 就能构建镜像还能一键启动容器。配置起来也简单打开 Settings 里的 Docker 区域填上 Docker socket 或远程 TCP 地址IDEA 会自动识别本机镜像和容器。这样调试时能直接在 IDE 里看日志、看容器状态对微服务开发非常友好。除了 IDEA也可以用 Maven 插件把打包 Docker 镜像这件事写进构建流程里比如通过 dockerfile-maven-plugin 在mvn package之后自动执行镜像构建。这套方案的好处是镜像版本和代码版本可以联动不用手动维护两套版本号。7.3 Docker 生态里的特殊部署场景Docker 的应用范围远不止常规的 Web 服务。我近期接触到的几个特殊场景也都能用同一套容器逻辑解决。“人大金仓数据库”这类国产数据库官方提供 Docker 镜像内网部署时一条 docker run 就能省去大量初始化配置。日志分析平台 openobserve 也支持 Docker Compose 一键部署把存储、检索、告警全部跑起来适合日志量不大但想快速搭一套观测系统的团队。还有像 ROS2 与 micro-ROS 这类机器人开发环境依赖版本复杂很多开发者直接把 Humble 容器拉下来用宿主机只装一个 Docker剩下的交给容器避免环境污染。这些场景放在一起看其实都在重复同一件事把原本需要手工初始化、特别容易出错的运行环境固化成镜像文件让任何人、任何机器都能以同样方式启动。8. 实操心得先日志再网络最后配置8.1 三个超实用的调试习惯使用 Docker 一段时间后我慢慢形成了一套自己的排错顺序。第一遇到任何问题先看日志命令是docker logs -f --tail100 容器名。很多启动失败的原因比如数据库密码错误、端口冲突、目录权限不正确日志里写得清清楚楚。先看日志能省掉大量猜测时间。第二用docker inspect 容器名查看容器真实配置确认挂载路径、环境变量、网络归属是不是自己预期的。有时候命令看起来没问题但实际映射的端口和内存限制会在 inspect 里露出马脚。第三不要一失败就删容器。先用docker ps -a看容器退出码和状态很多容器的退出码本身就指向明确的错误原因删了反而丢失线索。8.2 根据个人经验避免翻车的几个原则折腾 Docker 这几年有几条原则我几乎每次都会提到。不要在生产环境直接使用latest标签。镜像更新不可控可能今天能用明天同样的命令就拉到一个不兼容的版本。固定版本号回滚时才心里有底。数据库类容器一定要把数据卷挂出来。容器本身是“活不过下次 surprise 的”只有宿主机上的数据卷才属于自己。另外对/var/lib/docker占用空间保持敏感如果磁盘满了清理时不要随手执行docker system prune -a --volumes因为--volumes会把没有被容器引用的数据卷一起删掉那些数据卷里可能存着重要数据。还有一点配置好镜像源之后并不是所有镜像都能秒下。Docker Hub 上一些超大镜像比如完整版的 GitLab、包含 CUDA 的 AI 镜像即便是源地址比较通畅也可能需要几分钟。这时候耐心等一等不要因为卡住就反复 CtrlC 再重试反而容易损坏本地未完成的下载事务。我自己最深的体会是Docker 不是魔法它只是把环境管理这件事变得可重复、可验证、可分发。学会看日志、学会在出错时按顺序排查比背下更多命令重要得多。