Docker 容器实战手册:镜像、部署与权限排错全攻略

发布时间:2026/10/9 8:36:01
Docker 容器实战手册:镜像、部署与权限排错全攻略 第一次在服务器上敲下docker ps得到的不是容器列表而是permission denied while trying to connect to the docker api。那台服务器是我自己的root 权限都在却连看个列表都不行当时觉得特别莫名其妙。后来才明白问题出在 docker 的 socket 权限上——Docker 默认通过/var/run/docker.sock跟守护进程通信而这个 socket 归root组管普通用户根本碰不得。这种“装好了却用不了”的挫败感几乎每个接触 Docker 的人都经历过。这篇内容就是围绕“docker 使用”这个宽泛话题展开从镜像、容器、仓库的核心概念到 Windows、Ubuntu、CentOS 三端的安装与报错再落到 MySQL、Redis 这类中间件的实战部署最后把权限、网络、磁盘三个高频故障的完整排查链路捋一遍。适合两类人一类是刚听说 Docker、想知道它到底解决什么问题的入门者另一类是已经装过 Docker但总被启动失败、网络不通、权限报错卡住的实操型选手。1. Docker到底是什么先搞清楚镜像、容器、仓库这三件套很多教程上来就让你执行docker run hello-world跑通了也不知道刚才发生了什么。Docker 的整个体系说穿了就是围绕三个东西转镜像、容器、仓库。把这三个搞明白后面所有命令都是在跟它们打交道。1.1 镜像和容器模板与实例的区别镜像Image是一个只读的、分层存储的文件系统模板。里面装好了操作系统的基础文件、运行时、依赖库和应用代码。容器Container则是镜像运行起来之后产生的实例它在镜像的只读层之上叠加了一个可写层你往容器里写的任何文件都落在这个可写层里不会改动镜像本身。这个关系和“光盘镜像文件”与“刻录后的光盘”很像但更准确的类比是“安装包”和“装好软件正在运行的那台机器”。同一个镜像可以同时启动出多个容器彼此互不影响。我经常在一台机器上用同一个 MySQL 镜像起好几个容器分别给不同项目用端口不一样而已各跑各的完全独立。镜像还有一个关键特性分层。每个 Dockerfile 指令都会生成一个只读层构建时如果有层没有变化会被直接复用。这也是为什么你改了一行代码重新构建速度往往非常快——只有变化的那一层需要重新生成。这也是镜像不像虚拟机那样动辄几十 GB 的原因基础镜像可能只有几十 MB应用层只在需要时追加。1.2 镜像仓库、标签与版本锁定仓库Registry就是存放镜像的地方最常用的是 Docker Hub 这种公共仓库。你执行docker pull mysql实际上是往默认仓库拉取mysql这个镜像名对应的最新内容docker pull mysql:8.0则是拉取8.0这个具体标签tag。标签就是镜像的版本号用冒号跟在镜像名后面。这里有个新手容易踩的坑很多人拉镜像时图省事不写标签直接docker pull mysql拿到的其实是latest标签。latest是一个会移动的指针哪天维护者更新了你再拉就是新版本。如果你的生产环境依赖某个数据库版本建议始终锁定具体标签比如mysql:8.0.36同时把latest标签的行为当成“永远在变”来理解。这个细节在写完 Dockerfile 之后尤其重要——镜像名和标签写得不够明确等你想回滚版本时根本找不到当时的那个镜像。1.3 容器和虚拟机的边界与局限容器和虚拟机最本质的区别在于虚拟机虚拟化的是硬件每个虚拟机里都有一个完整的操作系统内核容器共享宿主机的内核只做进程级别的隔离。启动一个容器相当于启动一个被隔离的进程所以秒级启动、占用内存极小。而虚拟机要从 BIOS 到内核完整地引导分钟级起步是常态。但这带来一个限制Linux 容器必须在 Linux 内核上运行。在 Windows 上直接跑 Linux 容器是不行的Docker Desktop 的做法是用 WSL2 或 Hyper-V 构建一个轻量 Linux 虚拟机容器真正运行在那个虚拟机里。所以你在 Windows 上装 Docker Desktop 时系统会要求开启虚拟化支持原因就在这里。理解了这三件套下面进入安装环节。这部分也是大多数人卡住、搜索关键词最多的地方。2. 环境准备三个主流系统的安装姿势与两个高频报错Docker 的安装本身不难但不同系统的环境和版本坑很多。尤其是 Windows 上的 Docker Desktop以及老版本 Ubuntu、CentOS 的兼容性问题几乎每隔几天就能看到有人在群里问。2.1 Windows 上 Docker Desktop 的虚拟化检测失败Windows 上最常见的报错是类似Docker Desktop failed to start because virtualisation support wasnt detected这样一段话大意是检测不到虚拟化支持。第一次看到这个提示很多人第一反应是“我电脑是不是坏了”其实大概率是三个地方没到位。第一步进 BIOS/UEFI 确认 CPU 虚拟化开关。Intel 的 VT-x 和 AMD 的 SVM 都被关掉的情况在一些品牌机上是出厂默认需要进了 BIOS 找到类似Intel Virtualization Technology的选项把它打开。第二步确认 Windows 功能里“虚拟机平台”和“适用于 Linux 的 Windows 子系统”都已开启这一步可以通过控制面板里的“启用或关闭 Windows 功能”来勾选。第三步安装或更新 WSL2 内核只启用 WSL 但内核太旧Docker Desktop 一样起不来执行wsl --update更新到最新即可。还有一个不太容易想到的干扰源如果机器上装了其他虚拟化软件比如某些模拟器、沙箱工具可能抢占虚拟化能力导致 Docker Desktop 检测失败。不同版本对 WSL2 和 Hyper-V 二选一的处理方式也有差异新版 Docker Desktop 的 4.x 系列大多数情况走 WSL2 这条路径。如果你的机器确实硬件太老不支持虚拟化我的建议是别折腾模拟器方案了换一台支持虚拟化的机器体验差距很大。2.2 Ubuntu 和 CentOS 安装 Docker 的差异Linux 上装 Docker不同发行版的做法不一样。Ubuntu 上最简单的是sudo apt install docker.io这个是发行版仓库自带的版本通常够用。但如果你需要较新的 Docker 引擎特性建议走官方 apt 源安装 docker-ce。官方源的好处是版本新、迭代快缺点是第一次配置 GPG 密钥和源地址的时候步骤稍多但照着做一次就记住了。CentOS 7 的情况稍微复杂一点。系统默认仓库里的 docker 是非常旧的老版本如果你想用新特性需要添加 docker-ce 的 yum 源来安装新版。安装时经常碰到的问题是缺依赖比如container-selinux版本不满足解决办法是先启用 CentOS 的 extras 仓库再安装。更早的系统比如 Ubuntu 14、内核版本停留在 3.x 的机器装新版 Docker 大概率会起不来因为新版 Docker 对内核有最低要求。这种老系统上我试过最稳妥的办法是用旧版 Docker 对应的版本在确定内核能跑起来之前不要把生产环境随便升上去。2.3 装完后第一件事镜像加速与加速失效排查镜像下载慢是所有人都会遇到的问题。公共仓库在高峰时段拉镜像经常卡到怀疑人生一个几百 MB 的镜像能拉十分钟。解决办法是配置镜像加速器编辑/etc/docker/daemon.json把registry-mirrors指向可用的公共镜像源{ registry-mirrors: [ https://your-mirror-address ] }改完之后执行sudo systemctl daemon-reload sudo systemctl restart docker再运行docker info查看Registry Mirrors一栏确认加速器已经生效。这里提醒一句加速器列表里有一大堆镜像源有些能稳定用很久有些过段时间就失效了失效后最直接的表现是拉镜像速度又回到龟速。排查时先确认daemon.json里写的地址还能不能正常访问。2.4 Docker 服务启动失败怎么办安装完成后Docker 服务起不来的情况也不少见。排查思路其实很固定先看服务状态systemctl status docker看有没有红色的错误输出如果输出信息不够再journalctl -u docker -n 100拉最近 100 行日志。还有一个技巧是直接前台运行dockerd这样所有日志会直接打到终端上比翻 journal 更直观。常见原因无非这么几类daemon.json写坏了导致 dockerd 解析失败端口或 iptables 规则冲突磁盘空间满了。我曾经遇到过一台机器上同时装了 Docker 和另一个占用 443 端口的服务dockerd 启动时因为端口冲突直接退出。这类问题把冲突源排掉重新systemctl start docker就能解决。环境准备好了接下来进入真正的实战环节。用一个很多人都会遇到的场景来练手数据库中间件的容器化部署。3. 第一次实战把MySQL 8.0和Redis主从装进容器我最初用 Docker 就是从数据库开始的。在一台服务器上同时给两三个项目各自提供数据库实例又不想每个项目都在宿主机上装一套 MySQL——那会把环境搞得很乱。容器化之后每个项目一个 MySQL 容器互不干扰删了重建也很快。3.1 起一个带持久化的 MySQL 8.0 容器先看一个标准的 MySQL 8.0 容器启动命令docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDYourPassword123 \ -e MYSQL_DATABASEtestdb \ -v mysql_data:/var/lib/mysql \ --restart unless-stopped \ mysql:8.0拆开看每个参数什么意思。-d是后台运行--name给容器起名-p 3306:3306做端口映射把宿主机 3306 端口转发到容器内部的 3306。-e是环境变量MySQL 镜像用MYSQL_ROOT_PASSWORD来设置 root 密码MYSQL_DATABASE会在首次初始化时自动创建一个库。-v mysql_data:/var/lib/mysql是把一个命名卷挂载到容器的数据目录这是重中之重没有这一行容器一旦被删数据全部消失。--restart unless-stopped表示容器异常退出时除了手动 stop 的情况都会自动拉起这样机器重启后数据库也自动起来了。访问方式也是新手容易混乱的地方从宿主机访问直接用127.0.0.1:3306或者宿主机 IP 的 3306 端口从另一个容器访问则要用容器所在的自定义网络里的服务名或容器 IP后面 3.2 节会细说。进入容器内部也有办法docker exec -it mysql8 mysql -uroot -p3.2 Redis 主从两个容器之间怎么建立关系Redis 主从是很多人第一次接触“多个容器协作”的场景。思路是起两个 Redis 容器一个主、一个从从节点通过slaveof指定主节点的地址。但这里藏着一个坑如果用默认的 bridge 网络两个容器之间不能直接用容器名互相访问因为默认网络不提供容器名 DNS 解析。解决办法是先创建一个自定义网络让两个容器加入同一个网络docker network create redis-net docker run -d --name redis-master \ --network redis-net \ -p 6379:6379 \ redis:7.0 redis-server --appendonly yes docker run -d --name redis-slave \ --network redis-net \ -p 6380:6379 \ redis:7.0 redis-server --slaveof redis-master 6379重点在--network redis-net有了这行之后两个容器在同一条自定义网络里从节点里写的redis-master会被解析成主节点的容器 IP。这比在配置里写死 IP 要稳妥得多因为容器重建后 IP 是会变的但容器名是我们可以固定控制的。启动完验证一下redis-cli -p 6380 info replication看输出里的role:slave和master_link_status:up主从就算建立成功了。3.3 数据卷的三种姿势与备份思路Docker 的数据持久化有三种常见方式命名卷、绑定挂载、tmpfs。命名卷由 Docker 管理位置在/var/lib/docker/volumes/下适合数据库这类不需要直接关心数据文件位置的场景。绑定挂载是把宿主机某个目录直接映射进容器比如-v /opt/myapp/logs:/app/logs适合日志、配置这类需要从宿主机直接查看和修改的内容。tmpfs 只存在内存里容器一停数据就没了很少在业务场景使用。三种方式对比如下方式数据位置易用性典型场景命名卷Docker 管理高无需关心路径数据库数据绑定挂载宿主机指定目录中需要自己管理目录日志、配置、代码tmpfs内存高临时缓存数据备份我一般这么操作先停容器然后把数据卷打包成 tar 文件。比如备份 MySQL 的命名卷docker run --rm -v mysql_data:/data -v /backup:/backup alpine tar czf /backup/mysql_data.tar.gz -C /data .这个命令启了一个临时容器把mysql_data卷挂载到/data宿主机/backup挂到/backup然后打包。恢复的时候反向解包就行。这个思路适合大多数基于数据卷的应用不用管数据卷底层到底存在哪里。3.4 容器日常操作速查容器的日常操作命令整理下来就这些docker ps查看运行中的容器加-a看所有容器包括已停止的docker logs -f 容器名跟踪容器日志docker exec -it 容器名 bash进入容器内部交互式执行命令docker cp 容器名:/路径 ./从容器里复制文件出来反方向也能复制进去docker inspect 容器名查看容器的详细配置包括 IP、挂载、环境变量docker rm 容器名删除容器删之前最好确认数据卷是否要保留这些命令我用得最多。特别是docker logs几乎成了排错的第一入口不管是应用报错还是启动失败先看日志再动手比瞎猜靠谱得多。跑通了单容器之后下一层的问题是项目不止一个 MySQL还有 Redis、应用服务、队列任务需要一次性把整个环境拉起来。这就轮到 Dockerfile 和 docker-compose 上场了。4. Dockerfile与docker-compose把单容器升级成项目编排单容器适合跑通功能但真实项目往往涉及多个服务。这时候如果还一个一个docker run命令会越来越长且很难管理。Dockerfile 解决“怎么把项目变成镜像”的问题docker-compose 解决“一堆镜像怎么配合启动”的问题。4.1 用 Dockerfile 把项目打包成镜像Dockerfile 就是一份构建镜像的说明书。以一个 PHP 项目为例FROM php:8.2-fpm COPY . /var/www/html WORKDIR /var/www/html RUN docker-php-ext-install pdo_mysql EXPOSE 9000 CMD [php-fpm]FROM指定基础镜像COPY把当前目录的文件复制进镜像RUN在构建时执行的命令CMD是容器启动后执行的命令。构建命令是在项目目录下执行docker build -t myphp-app:1.0 .这里的.表示把当前目录作为构建上下文发送给 Docker 守护进程。有一个细节很关键项目里的node_modules、vendor、target、.git这些大目录也会被发过去拖慢构建速度。解决办法是在项目根目录放一个.dockerignore文件内容类似node_modules vendor target .git *.log还有一种更进阶的构建技巧叫多阶段构建对 Java 项目尤其有用。以下面这个 Dockerfile 为例FROM maven:3.8-openjdk-17 AS build COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn package -DskipTests FROM openjdk:17-jdk-slim COPY --frombuild /app/target/*.jar /app/app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, /app/app.jar]第一阶段用 Maven 镜像编译出 jar 包第二阶段只拷贝编译产物进瘦身版 JDK 镜像。好处是最终镜像里没有任何 Maven 和源码文件体积能小一半以上。这也是把 Java 项目做成镜像的主流姿势。4.2 用 docker-compose 一次性拉起一套环境docker-compose 最大的价值是把多个容器的启动逻辑写在一个 YAML 文件里一条命令全部拉起来。下面是一个包含 MySQL、Redis 和应用服务的编排文件services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: rootpass MYSQL_DATABASE: app_db volumes: - mysql_data:/var/lib/mysql ports: - 3306:3306 healthcheck: test: [CMD, mysqladmin, ping, -h, localhost] interval: 5s timeout: 3s retries: 20 redis: image: redis:7.0 ports: - 6379:6379 app: build: . depends_on: mysql: condition: service_healthy ports: - 8080:8080 environment: DB_HOST: mysql REDIS_HOST: redis volumes: mysql_data:老版本里命令是docker-compose新版 Docker Compose 插件已经集成进 Docker CLI命令变成docker compose中间没有横杠。用docker compose up -d启动全部服务docker compose down停止并删除容器docker compose logs -f跟踪所有服务的日志。写这个文件我还有一个小小的建议给数据库加上healthcheck这样应用服务可以等数据库真正就绪后再启动否则经常出现应用先启动、连不上数据库直接崩溃的情况。4.3 部署 GitLab 这类重量级镜像的特殊考虑GitLab 是很典型的重量级容器化应用镜像本身就有几个 GB运行起来内存占用轻松超过 4G。有同学在 2G 内存的云主机上跑 GitLab结果不光 GitLab 起不来整个机器的内存都被吃满了。第一次部署 GitLab建议至少给到 4G 内存有条件的话 8G 更从容。启动命令参考这个docker run -d \ --name gitlab \ --restart unless-stopped \ -p 8080:80 \ -p 8443:443 \ -p 8022:22 \ -e GITLAB_OMNIBUS_CONFIGexternal_url http://your-domain:8080; gitlab_rails[gitlab_shell_ssh_port] 8022; \ -v gitlab_config:/etc/gitlab \ -v gitlab_logs:/var/log/gitlab \ -v gitlab_data:/var/opt/gitlab \ gitlab/gitlab-ce:latest几个容易忽略的点宿主机 80/443/22 端口通常已经被占用或需要权限所以我映射到 8080/8443/8022。GITLAB_OMNIBUS_CONFIG里的external_url和gitlab_shell_ssh_port必须跟端口映射对应上否则页面里显示的克隆地址是错乱的。另一个坑是 GitLab 第一次启动要做大量初始化工作需要几分钟甚至更久才能响应别因为docker logs刷了几百行就以为卡死了多等等。4.4 IDEA打包镜像与Docker插件开发阶段最常用的是 IDE 的 Docker 支持。以 IDEA 为例装了 Docker 插件后可以对远程或本地的 Docker 守护进程进行操作。右键项目里的Dockerfile选择Build Image on Docker就能直接构建镜像构建好的镜像还能右键Run起容器。如果代码在本地、Docker 在远程服务器上IDEA 的 Docker 配置里填上远程地址就能用省去了先把文件传到服务器再构建的麻烦。这条链路对于日常开发效率提升非常明显。我经常在 IDEA 里改完代码一键构建镜像再一键运行本地调试和测试环境部署的差距就没那么大了。不过要提醒一句远程构建时构建上下文是本地的如果项目目录很大上传上下文的时间也会很长.dockerignore在这里同样很重要。5. 排错链路权限、网络、磁盘三类问题的完整排查思路实战过程中总会遇到各种报错。这里整理三条最典型的排查链路每条我都按自己实际排查时的顺序来写按这个顺序走能少走很多弯路。5.1 permission denied 的根因与修复链路报错原文通常是permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock这句话的直译是连接 Docker 守护进程的 socket 时没有权限。Docker CLI 本身只是一个客户端真正干活的是 dockerd 守护进程CLI 通过/var/run/docker.sock这个 unix socket 跟守护进程通信。这个 socket 文件默认属于 root 组普通用户没有访问权限。排查链路就三步。第一步确认问题源头ls -l /var/run/docker.sock正常会看到类似srw-rw---- 1 root docker ...的结果说明文件归属于 root 用户和 docker 组。第二步看你当前用户属于哪些组id如果你的用户名后面的组列表里没有 docker那问题就找到了。第三步把当前用户加入 docker 组sudo usermod -aG docker $USER执行完记得重新登录或者执行newgrp docker让组权限生效再执行docker ps就正常了。这个做法简单有效但要说明白背后的安全考量加入 docker 组的用户等于拿到了 root 级别的能力因为 Docker 的 socket 访问权本质上可以让你操作宿主机上的任意文件。所以生产服务器上不要随便把普通用户加进 docker 组能用 sudo 就用 sudo。5.2 容器网络不通的排查顺序“访问不到容器里的 MySQL”“两个容器之间 ping 不通”“容器里上不了外网”这些都属于网络问题。很多人在这一步就开始乱试防火墙和 iptables其实网络排查有固定的顺序。第一层看端口映射有没有生效。宿主机上执行ss -lntp | grep 3306如果看到监听地址里有127.0.0.1:3306或0.0.0.0:3306说明映射是建立起来了。如果什么都没有回头看docker ps里的PORTS列确认启动时有没有-p参数。第二层看从宿主机能不能直接访问容器 IP。用docker inspect 容器名查到容器 IP然后curl或telnet测试。这一步能区分到底是映射坏了还是容器内部服务没起来。如果容器 IP 能访问、宿主机映射端口不行问题大概率出在防火墙或 iptables 规则上。第三层看容器之间的连通性。默认 bridge 网络下两个容器只能通过 IP 通信容器重建后 IP 会变所以强烈建议用自定义网络加服务名。一个非常经典的场景是两个容器在同一台机器上其中一个访问另一个的容器 IP 可以通但重启后 IP 变了应用就崩了。解决办法就是把两个容器放到同一个docker network create my-net创建的网络里用容器名代替 IP。第四层容器里 DNS 解析不了外网域名时检查/etc/resolv.conf必要时在启动时加--dns 8.8.8.8这类参数覆盖默认 DNS。这个情况多发生在自己搭了内网 DNS 的机房里。5.3 磁盘被镜像填满清理命令与日常预防Docker 用久了最头疼的问题之一就是磁盘被吃满。常见现象是docker system df显示的镜像和构建缓存占了几个 GB而你自己根本不知道它们是什么。Docker 的缓存机制是构建时为了提速保留的中间层每次构建都会产生如果项目经常迭代这些缓存堆积得特别快。磁盘清理遵循从轻到重的顺序# 只清理停止的容器和悬空镜像dangling image docker system prune # 清理更彻底包括未使用的镜像 docker system prune -a # 单独清理构建缓存 docker builder prune # 查看各资源占用情况 docker system df-a参数会把所有没有被容器引用的镜像都删掉这意味着下次要用得重新拉取或构建。我在生产环境机器上只跑docker system prune不带-a避免误删需要但暂时没在运行的镜像。开发机上则比较随意反正镜像拉回来很快。日常预防比事后清理更重要。一个习惯是给镜像和容器设置命名规范部署什么服务就用什么名字过期的服务及时删容器和镜像不要觉得“可能以后还用得上”而留着。另一个习惯是给/var/lib/docker所在的磁盘做监控磁盘使用率超过 80% 就提醒。5.4 开机自启、重启策略与安全收尾Docker 服务本身要开机自启systemctl enable docker这个操作让 docker 守护进程在系统启动时自动运行。但服务起来了不等于容器起来了容器是否随开机自动启动取决于--restart参数。我统一建议生产环境的容器都加上--restart unless-stopped这样除了手动 stop 的情况外容器都会在 Docker 服务启动后自动拉起。已经创建的容器可以用docker update --restart unless-stopped 容器名补上这个策略不用重新创建。还有一点安全提醒端口不要随意绑到公网地址。-p 3306:3306默认绑定所有网卡的 3306 端口如果你的服务器有公网 IP数据库就等于暴露在公网上。只需要内网访问时绑到内网 IP 或127.0.0.1更稳妥比如-p 127.0.0.1:3306:3306这个改动成本极低但能省掉很多被扫描和攻击的烦恼。最后再分享一个我自己的习惯每次跑docker run之前先想清楚这个容器需不需要持久化数据、需不需要开机自启、需不需要暴露端口。这三个问题想清楚了命令基本就不会有大的疏漏。Docker 用久了会发现大部分所谓“疑难杂症”追根溯源都是启动参数没想明白造成的。手机上装个 Docker 客户端管理远程容器或者给常用命令配个别名也能提升不少效率。核心还是把镜像、容器、数据卷、网络这几件事的边界和关系搞透遇到任何报错都有据可依不会手忙脚乱。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询