身边越来越多的项目采用容器化部署,Docker几乎成了现代运维和开发绕不开的基础工具。但很多朋友在真正动手时,却常常卡在第一步:Windows下Docker Desktop死活启动不了,Linux上装完又遇到权限问题,好不容易跑起来一个容器,网络又不通了。这篇文章就是一套从零基础到生产环境的完整实战指南,我会把安装、核心命令、Compose编排、网络持久化,以及生产环境上线时那些文档里不会明说的关键点串起来讲,帮你把Docker真正用起来,而不只是停留在“会跑个hello-world”的阶段。
无论你是刚接触容器的新手,还是已经趟过一些坑、想系统梳理一遍的开发者或运维,这篇文章都会有用。我会把整个链路上最容易翻车的环节拆开揉碎,结合我在实际项目中踩过的坑和验证过的稳定方案,尽量让每个操作都有依据、可复现。
1. 从零搭建Docker环境:安装踩坑全记录
安装Docker这件事,看起来简单,实际上是整个容器化之路上的第一个大坑,尤其是Windows用户。这里把最常见的几种环境和问题一次性说清楚。
1.1 Windows下安装Docker Desktop:虚拟化检测与WSL2的坑
很多人在Windows上安装Docker Desktop,双击安装包后满怀期待地点击Start,结果迎面就碰到一个报错:Docker Desktop failed to start because virtualisation support wasn't detected。这个报错的意思是系统没有检测到虚拟化支持。很多人第一反应是去BIOS里开启虚拟化,但这只是其中一个原因,而且不是最常见的。
这个问题的排查思路应该是这样的。
先去任务管理器,“性能”标签页,看右下角的“虚拟化”是否显示“已启用”。如果是“已禁用”,那就需要重启电脑,进BIOS/UEFI里找到Intel Virtualization Technology(Intel VT-x)或AMD SVM Mode,开启后保存退出。这一步做完,大部分人就能解决。
如果你的电脑CPU较老,或者主板固件比较特殊,BIOS里根本没有虚拟化选项,那Docker Desktop基本就告别了。但Docker本身还需要一个东西叫WSL2(Windows Subsystem for Linux,适用于Linux的Windows子系统),Docker Desktop现在默认依赖WSL2后端。所以即便虚拟化已启用,如果WSL2没装好或版本不对,启动时也可能报别的错误,比如failed to connect to the docker api at npipe:////./pipe/dockerdesktoplinuxen。
这个报错我见过太多次了,它通常不是Docker本身的问题。比较标准的处理流程是:先确认Windows功能里“适用于Linux的Windows子系统”和“虚拟机平台”两个选项都已勾选,然后以管理员身份运行PowerShell,执行wsl --update更新WSL内核,再执行wsl --set-default-version 2,把默认版本切到WSL2。之后重启Docker Desktop,基本上就能解决。
注意:Windows 11对WSL2的支持比Windows 10好很多。如果是Windows 10,请确保系统版本在21H2以上,否则依赖旧版WSL1,Docker Desktop跑起来性能很拉胯,甚至起不来。
还有一个容易被忽略的点:Docker Desktop在Windows下是通过WSL2来实现Linux内核的,所以WSL2的发行版不能是空的。我第一次装的时候就遇到这个问题,wsl -l -v显示没有任何发行版,Docker Desktop启动也一直转圈。解决办法是在PowerShell里执行wsl --install -d Ubuntu,装一个默认发行版,然后再启动Docker Desktop。
1.2 Linux服务器安装:Ubuntu、CentOS 7与欧拉系统的差异
Linux下的安装相对Windows要简单很多,但不同发行版的坑也不一样。这里把最主流的几类系统分开说。
Ubuntu以及Debian系,推荐用官方推荐的apt仓库方式。不建议直接sudo apt install docker.io,因为这个包通常版本较老,很多新特性没有。正确做法是:
sudo apt update sudo apt install -y ca-certificates curl gnupg sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.ioCentOS 7是另外一个故事了。这套系统默认的docker包其实也叫docker,但它是老古董,版本停留在1.13。如果你搜索引擎直接搜“centos7安装docker”,大概率会看到一大堆让你先sudo yum install -y yum-utils,然后配置yum源的教程。关键在于,CentOS 7的官方yum源已经停更了,如果你直接执行yum install docker-ce,可能会遇到依赖解析失败或网络404。更稳妥的方式是换用阿里的yum源,或者直接下载rpm包离线安装。如果条件允许,我建议CentOS 7直接升级到CentOS 7.9,或者干脆换成Ubuntu 22.04 LTS这类长期支持版本。
华为欧拉系(openEuler 23.09等)的安装也略有不同。欧拉本身基于RHEL,但它的软件源里不一定有Docker的官方包,更常见的是用containerd和podman作为底层运行时。如果你在欧拉系统上硬要装Docker,可以直接用docker的静态二进制包,解压后放到/usr/bin,然后自己写systemd服务。这种方式其实挺干净的,不受发行版包管理器的限制。
1.3 镜像源配置:解决镜像下载慢的根治方案
镜像下载慢,这个痛几乎所有国内用户都经历过。docker pull一个镜像,进度条卡在几KB/s,等到你怀疑人生。解决办法就是配置镜像加速器。
国内主流云厂商都提供过Docker镜像加速服务,但近年来很多已经关闭或转为内网服务了。目前还比较稳定可用的是几个大的公共源。配置方法很简单,在/etc/docker/daemon.json里写上:
{ "registry-mirrors": [ "https://docker.m.daocloud.io", "https://dockerproxy.com", "https://hub-mirror.c.163.com" ] }写好之后执行sudo systemctl daemon-reload,再sudo systemctl restart docker。注意这里有个顺序问题,必须是先daemon-reload再restart,只restart不reload的话配置不一定生效。
提示:镜像源配置完之后,不需要重新拉镜像才能生效,下次docker pull时就会自动走新的源。但这个源的可用性和速度会随时间和网络环境变动,如果发现某个源变慢了,替换成其他源就行,多个源可以同时配置,Docker会按顺序尝试。
2. Docker核心概念与常用命令:从入门到熟练
很多新手装了Docker之后,第一件事是docker run hello-world,跑通了之后觉得不过如此,然后就不知道干嘛了。其实Docker的核心概念和命令体系,理解透了整个容器化的逻辑就清晰了。
2.1 镜像与容器的关系:理解“类”与“实例”
把镜像(Image)和容器(Container)的关系,用面向对象来类比可能最直观:镜像是类,容器是实例。镜像是静态的、只读的模板,里面包含了你应用的代码、运行时、环境变量、配置文件等所有东西;容器是运行中的进程,是镜像的一个可写副本。
所以你在容器里改的文件、装的东西,只存在于这层可写层。如果你把这个容器删了,一切改动都没了。想重现这个改动,要么把改动提交成一个新镜像(docker commit),要么用Dockerfile把它固化下来。
理解了这个基本关系,很多操作行为就说得通了。比如为什么docker exec -it进入容器装个vim,退出之后容器还在,但重启容器后vim又没了——因为容器重启并不代表镜像重来,但容器删除再重建就会丢失所有非持久化数据。这就是为什么生产环境一定要有数据卷的概念,后面我会专门讲。
2.2 高频命令速查:覆盖90%使用场景的命令清单
Docker命令很多,但真正高频的就是那么十几个。我把平时用得最多的命令分类整理成了一张速查表,方便大家照抄。
| 用途 | 命令 | 说明 |
|---|---|---|
| 拉取镜像 | docker pull nginx:1.25 | 指定版本拉取,生产环境必须用tag,不要用latest |
| 查看镜像 | docker images | 列出本地镜像 |
| 删除镜像 | docker rmi 镜像ID | 删除前需先删除使用该镜像的容器 |
| 运行容器 | docker run -d --name web -p 8080:80 nginx:1.25 | -d后台运行,--name命名,-p端口映射 |
| 查看容器 | docker ps -a | -a包含已停止的容器,生产排查必须加-a |
| 进入容器 | docker exec -it 容器名 /bin/bash | 前提是镜像里有bash,没有可以换成sh |
| 查看日志 | docker logs -f 容器名 | -f持续跟踪输出,排查问题必备 |
| 停止/启动 | docker stop 容器名 / docker start 容器名 | 停止不删除,容器还在 |
| 删除容器 | docker rm -f 容器名 | -f强制删除运行中的容器 |
| 查看资源 | docker stats | 查看所有容器的CPU、内存、网络IO用量 |
这里我想特别强调一个细节:docker run -d并不意味着容器一定会一直运行。如果容器里的主进程退出了,容器就立刻进入Exited状态。这个机制是Docker基于“PID 1进程”的隔离逻辑,主进程退出,容器使命结束。所以如果你跑一个没有前台服务的镜像,docker run -d之后一查,容器已经退了,这不是Docker坏了,是镜像本身没有设计成常驻进程。写Dockerfile的时候一定要注意CMD指令是不是一个前台阻塞式命令。
2.3 镜像构建与Dockerfile优化:从能用变好用
Dockerfile是Docker的精髓,没有它,Docker就只是一个带隔离的进程管理器。写Dockerfile有几个层次,从能构建,到构建快、体积小、安全,中间差距很大。
一个典型的Python项目Dockerfile长这样:
FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple COPY . . EXPOSE 8000 CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]这里面有几个值得注意的点。第一,基础镜像用了python:3.11-slim而不是python:3.11,slim版本比标准版小很多,少了大量用不到的工具链,体积可能差出一倍以上。第二,COPY requirements.txt . 和COPY . .分开写,正确利用了Docker的层缓存机制——只要requirements.txt没变,RUN pip install这一层就会命中缓存,构建速度快非常多。第三,用清华pip源加速依赖安装,这是被无数人忽略的细节,没有它,你在国内服务器上构建镜像时,pip拉依赖会慢到崩溃。
构建镜像的方式有两种,一种是docker build -t myapp:1.0 .,直接在本地构建。另一种是BuildKit的高级用法,比如多阶段构建。多阶段构建的典型场景是需要编译的语言,比如Java用maven编译、前端用node打包,最终镜像只需要运行产物不需要编译工具。只需要第一阶段的编译,再把产物COPY到第二阶段的基础镜像里,最终镜像体积能缩小一个数量级。
3. 实战:用docker-compose部署Redis主从与MySQL 8.0
单容器只是入门,真正的实战在于多容器编排。docker-compose是官方推出的单机编排工具,语法简单,能力足够覆盖大部分中小项目的需求。
3.1 为什么选择docker-compose:从复杂到规整
如果没有compose,部署一个多服务应用,你需要先docker network create建一个自定义网络,然后依次docker run各个容器,每个容器都要手动指定网络参数、端口映射、存储卷,命令长到一眼看不完。而且这些配置没法放进代码仓库,换台机器就得重新敲一遍。
有了docker-compose,一切变成了一个docker-compose.yml文件。这个文件和代码一起提交、一起版本化,注释写清楚,任何人clone下来都能一键复现整个环境。这才是容器化部署的真正价值——环境一致性。
3.2 Redis主从部署:一份可以照抄的compose配置
Redis做主从,目的无非是读写分离、数据冗余。下面这份配置我实际部署过很多次,稳定可靠。
version: "3.8" services: redis-master: image: redis:7.2-alpine container_name: redis-master ports: - "6379:6379" volumes: - redis-master-data:/data - ./redis/master.conf:/etc/redis/redis.conf command: redis-server /etc/redis/redis.conf restart: always networks: - redis-net redis-slave-1: image: redis:7.2-alpine container_name: redis-slave-1 ports: - "6380:6379" volumes: - redis-slave-1-data:/data - ./redis/slave.conf:/etc/redis/redis.conf command: redis-server /etc/redis/redis.conf depends_on: - redis-master restart: always networks: - redis-net volumes: redis-master-data: redis-slave-1-data: networks: redis-net: driver: bridge主节点的redis.conf里,只需要简单配置:
bind 0.0.0.0 protected-mode no appendonly yes从节点的slave.conf里,加上主从关系:
bind 0.0.0.0 protected-mode no replicaof redis-master 6379 appendonly yes这里的关键点在于,从节点的replicaof后面写的是redis-master而不是127.0.0.1。这正是因为compose会自动为服务创建DNS解析,容器之间通过服务名互相访问。在compose网络里,服务名就是主机名。
启动命令两个:docker-compose up -d启动,docker-compose ps查看状态。验证主从是否正常,可以执行docker exec -it redis-slave-1 redis-cli info replication,看到role:slave和master_link_status:up就说明成功了。
注意:配置里如果用redis主从,端口映射只在宿主机占用一个6379,从节点映射的是6380。如果你的服务器上已经装了系统自带的redis,那6379端口一定被占,compose里就要改端口映射host部分,比如"16379:6379"。这个细节,线上栽过无数次。
3.3 MySQL 8.0部署与数据持久化:生产环境的完整姿势
MySQL的容器化部署比Redis要复杂一些,主要复杂在数据持久化、字符集和权限初始化。下面这份配置是我在生产环境验证过的。
version: "3.8" services: mysql: image: mysql:8.0 container_name: mysql8 restart: always environment: MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD}" MYSQL_DATABASE: app_db MYSQL_USER: app_user MYSQL_PASSWORD: "${MYSQL_PASSWORD}" TZ: Asia/Shanghai command: - --character-set-server=utf8mb4 - --collation-server=utf8mb4_general_ci - --default-authentication-plugin=mysql_native_password ports: - "3306:3306" volumes: - mysql-data:/var/lib/mysql - ./mysql/init/:/docker-entrypoint-initdb.d/ - ./mysql/conf/:/etc/mysql/conf.d/ healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-p${MYSQL_ROOT_PASSWORD}"] interval: 10s timeout: 5s retries: 5 volumes: mysql-data:几个值得注意的细节:
MYSQL_ROOT_PASSWORD建议通过环境变量文件传入,不要直接写在compose文件里提交到代码库。具体做法是在同一目录下创建.env文件,里面的键值对会被compose自动读取,比如MYSQL_PASSWORD=YourStrongPassword2024。然后.gitignore忽略掉.env,只在服务器上维护。
command里的三行参数,第一行了utf8mb4字符集和排序规则,保证中文存储不乱码;第三行指定了认证插件为mysql_native_password,这个在老项目里面尤为重要。MySQL 8.0默认用的caching_sha2_password,在部分老旧客户端(比如PHP 5.x、某些老版本Navicat)下面会连不上,指定成mysql_native_password就能兼容。
volumes里面的./mysql/init/目录很关键。容器首次启动时,会自动执行该目录下的.sql或.sh脚本进行初始化。这个特性对交付场景来说非常友好——把建库建表语句放到init目录,首次启动就自动完成,不用人工登进容器执行SQL。
数据持久化方面,mysql-data这个命名卷就足够可靠。只有在极个别需要直接操作物理文件的场景下,才用bind mount把宿主机目录挂载进来,比如./data/mysql:/var/lib/mysql。但bind mount在macOS和Windows下的文件权限和性能都有偏差,生产环境我更推荐命名卷。
4. 容器网络与数据管理:让服务稳定支撑业务
很多人在本地部署容器没问题,一到生产环境就各种出岔子,网络不通、数据丢失、日志爆炸,其实都是网络和数据管理这两个模块没吃透。
4.1 容器间通信:为什么要创建自定义网络
Docker容器之间要通信,最忌讳的是直接使用端口+宿主机IP来互相访问。这样耦合了部署细节,一旦IP变了,所有相关配置都要改。正确的做法是:让容器在同一个自定义网络里,通过服务名或容器名直接通信。
Docker默认的网络模式是bridge(桥接),在这个模式下,容器之间可以通过IP互通,但IP是动态分配的。如果想要用容器名来互通,需要在创建容器时通过--link参数指定,但Docker官方早已把--link标记为legacy,推荐方案就是创建自定义网络。
docker network create my-network docker run -d --name app1 --network my-network nginx:1.25 docker run -d --name app2 --network my-network nginx:1.25在同一个自定义网络里,app1可以直接ping app2,也可以通过http://app2:8080这样的地址访问app2的服务。这种基于服务名的服务发现机制,是Docker内置DNS实现的,不需要额外部署任何组件。
对于docker-compose部署的项目,compose文件里的每个服务默认都会加入一个由项目名+网络名组成的默认网络,服务之间的服务名DNS解析是自动生效的,这也是前面Redis主从配置里replicaof redis-master 6379能生效的原因。
4.2 数据卷与持久化:容器删除后数据不丢失的保证
容器是“一次性”的,但数据必须是“永久性”的。这个矛盾必须通过数据卷来解决。
Docker的数据持久化方式有三种,应用场景不同。
命名卷(Named Volume)是官方最推荐的方式,比如上面Redis配置里声明的redis-master-data。创建一个独立的卷,容器写入这个卷的数据由Docker管理,即使容器删了卷还在。适合放生产数据,比如MySQL的/var/lib/mysql。
绑定挂载(Bind Mount)是把宿主机的一个目录直接映射进容器,比如-v /data/app:/app。好处是数据改动能直接看到,方便调试,适合放配置文件、日志文件这类需要从宿主机能直接访问的数据。缺点是权限不好控制,容易碰到semanage或SELinux之类的问题。
内存文件系统(tmpfs)是把数据放在内存中,容器重启数据就没了。只适合放临时文件、缓存文件,不能放业务数据。
最需要记住的一个规则:不要把数据写在容器内部的可写层。容器更新、迁移、重建之后,写在可写层的数据会全部丢失。
4.3 资源限制与日志管理:生产环境不能忽略的细节
生产环境里,一个不设限的容器会杀死整台服务器。这是我在一次线上事故里用血泪换来的教训。
有次某个服务内存泄漏,Docker容器疯狂吃内存,直接把宿主机搞到OOM,其他容器全部被内核杀掉。从那之后,我所有的容器一律加内存限制和一个合适的swap限制。
services: web: image: nginx:1.25 deploy: resources: limits: memory: 512M cpus: "0.5"注意deploy.resources这个配置在docker-compose v1里是直接支持的,但如果你是还在用老版本的compose语法(docker-compose不带版本号),版本号要写的3.x以上才有这个字段。否则需要改用docker run --memory 512m启动。
日志方面,Docker容器默认的日志驱动是json-file,默认情况下这些日志文件会一直增长,不做切割就能把磁盘写满。官方也在1.13版本之后为json-file驱动增加了max-size和max-file参数,在daemon.json里全局配置:
{ "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } }这样每个容器最多保存3个10MB的日志文件,超过就滚动清理,有效防止日志占满磁盘。
5. 从开发到生产:容器化上线的完整链路
会跑容器和能把项目容器化上生产,中间的差距包括镜像分发、CI/CD、健康检查、环境变量管理等多个环节。这里把核心链路拆开讲一遍。
5.1 镜像仓库与版本管理:千万别用latest打生产
构建出的镜像要分发到生产服务器,要么走Docker Hub,要么走自建的私有仓库。生产环境最推荐的做法是使用一个私有镜像仓库,比如Harbor,或者各大云厂商的容器镜像服务,比如阿里云ACR或腾讯云TCR。
不管用哪个仓库,有一个铁律:生产环境禁止用latest标签。因为latest是可变标签,这次拉的和下次拉的可能版本不同,这种“不确定性”在生产和回滚场景下是致命的。正确的做法是给每个镜像打上语义化版本号或者Git提交号,比如registry.example.com/myapp:1.4.2,或者registry.example.com/myapp:20240115-ab3f9c2。
5.2 生产环境的健康检查与自动重启
容器崩溃后,Docker引擎本身可以自动重启,但要做到更精细的健康判断,光靠重启策略是不够的。Dockerfile或者compose配置里加一个healthcheck,就能让Docker定期检查容器是否“真正健康”。
对于Web服务,最简单的健康检查是curl或者wget探测一个固定路径:
healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/health"] interval: 30s timeout: 5s retries: 3 start_period: 40sstart_period这个参数很重要,它表示容器启动后等待多少秒再开始健康检查,避免应用还在初始化的时候就因为健康检查失败而被标记为unhealthy。有了健康检查之后,配合restart: always策略,容器异常时会自动重启,并且只有通过健康检查后,才会被负载均衡接收流量。
5.3 与Kubernetes的关系:生产环境的下一个层级
很多人在了解Docker一段时间后,会接触到K8s或k8s相关的热词。这里需要澄清一个概念:Kubernetes是一个容器编排平台,它可以管理多个Docker容器(当然K8s和Docker在运行时层面的集成近几年有一些变化,但本质上依然是把容器作为最小调度单元)。
单个Docker容器适合单机场景,但如果你的服务需要多副本、弹性伸缩、滚动更新、跨节点调度,那就需要考虑K8s。K8s的复杂度比Docker高一个量级,所以从Docker迁移到K8s,通常建议先把docker-compose的精髓吃透,因为很多理念是相通的,比如服务发现、配置挂载、健康检查——在compose里用healthcheck,在K8s里用livenessProbe和readinessProbe。
6. 常见问题排查与避坑实录
6.1 容器网络不通:从宿主机到容器逐层排查
我在各个社区看到的容器网络问题,一半以上都是“两个容器之间网络不通”或者“宿主机访问容器端口不通”。排查思路应该从底向上,逐层筛选。
首先,看容器有没有正常启动。docker ps -a里如果容器状态是Exited,那还轮不到谈网络,先把容器拉起来再说。
其次,看端口映射是否生效。docker port 容器名,如果输出是空的,说明没有做端口映射。宿主机上访问不到容器内部服务,第一排查方向就是端口映射,比如-p 8080:80,宿主机的8080才能访问。
再次,看防火墙和网络安全组。Linux服务器上用iptables -L -n检查是否有禁ping或禁端口规则。云服务器的话,还要检查云控制台的安全组入站规则是否放行了8080端口。这个坑真的是经典中的经典,安全组没放行端口,容器配置得再完美也白搭。
最后,是容器DNS解析问题。如果之前自定义的网络里服务名解析失败,可以docker exec -it 容器名 cat /etc/resolv.conf查看DNS配置。Docker默认通过127.0.0.11:53内嵌DNS服务来解析,如果被覆盖掉了,服务名解析就会失败。
6.2 权限错误:解决Docker socket权限问题
Linux下刚装完Docker,执行docker ps大概率会报错:permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock。
这个问题的本质是当前用户不在docker用户组里。Docker守护进程通过/var/run/docker.sock这个Unix socket与客户端通信,默认只有root和docker组的成员能访问。最标准的解决方式:
sudo usermod -aG docker $USER newgrp docker注意newgrp命令让当前会话立即生效,不用重新登录。但如果你是在SSH会话里执行,最好重开一个SSH会话,确保环境变量都带上。
注意:把用户加入docker组等同于授予该用户root权限,因为用户可以挂载宿主机任意目录进容器,以root身份操作文件。所以在生产环境服务器上,给谁分配docker组权限要谨慎再谨慎。
6.3 Docker Desktop启动失败:Windows环境下的排查清单
前面提到了virtualization support wasn't detected和npipe连接失败,这里把Windows下Docker Desktop各种启动失败的排查清单整理成表,方便直接对号入座。
| 报错/现象 | 可能原因 | 解决办法 |
|---|---|---|
| virtualisation support wasn't detected | BIOS未开启虚拟化 | 进BIOS开启VT-x/AMD SVM |
| failed to connect to the docker api at npipe... | WSL2未正确安装或配置 | 执行wsl --update,wsl --set-default-version 2 |
| Docker Desktop一直转圈 | WSL2没有默认发行版 | 执行wsl --install -d Ubuntu |
| WSL2启动WSL报错0xffffffff | 内核更新导致旧版问题 | 执行wsl --shutdown,或重启电脑 |
| Hyper-V被其他虚拟化软件占用 | 比如VMware、VirtualBox冲突 | 关闭其他虚拟化软件的Hyper-V兼容模式 |
6.4 镜像下载慢的变通方案:离线部署与私有仓
镜像下载慢的问题在配置镜像源之后,大多数情况能解决。但如果你的生产服务器在完全隔离的内网环境,连公网都没法访问,那就只能走离线部署路线。
离线部署的思路很简单:在一台能联网的机器上把镜像拉下来,docker save保存成tar文件,拷贝到内网服务器,再docker load加载进去。
# 在联网机器上 docker pull nginx:1.25 docker save nginx:1.25 | gzip > nginx-1.25.tar.gz # 拷贝到内网机器后 docker load < nginx-1.25.tar.gz这个方案对于一次性部署很实用,但如果经常升级版本,每次都要手动打tar包和拷贝就太笨重了。更优的方案是部署一个Harbor私有仓库,开发网络推镜像,生产网络拉镜像,省去了打包拷贝的环节。
6.5 青龙面板依赖管理:容器里的依赖问题
很多用青龙面板跑任务的朋友会遇到依赖管理的问题——容器里没有某些Python或Node依赖,脚本跑不起来。这个问题的根本原因在于容器环境的隔离性:宿主机装了什么依赖,容器里是感受不到的。
解决方案是在部署时就把依赖打进去。要么用Dockerfile构建自定义镜像,在构建阶段pip install或npm install;要么在容器启动后docker exec进入容器安装,但需要注意容器重建后依赖会丢失。生产环境更推荐前者,维护成本低,镜像可复用。
7. 项目实战:一个完整的容器化部署流水线
把前面的知识点串起来,这里整理一个完整的小项目部署流程,从仓库到服务器,全链路贯穿,方便对照整体操作。
7.1 初始化项目结构
假设我们有一个简单的Node.js Web应用,项目结构如下:
myapp/ ├── src/ │ └── index.js ├── package.json ├── Dockerfile ├── docker-compose.yml └── .env.exampleDockerfile内容:
FROM node:20-alpine WORKDIR /app COPY package*.json ./ RUN npm install --registry=https://registry.npmmirror.com COPY . . EXPOSE 3000 CMD ["node", "src/index.js"]docker-compose.yml:
version: "3.8" services: app: build: . image: myapp:1.0.0 restart: always ports: - "3000:3000" env_file: - .env healthcheck: test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"] interval: 30s timeout: 5s retries: 37.2 构建、推送、拉取、部署
开发机上构建并推送镜像到私有仓库:
docker build -t registry.example.com/myapp:1.0.0 . docker push registry.example.com/myapp:1.0.0生产服务器上,只需要一份docker-compose.prod.yml,用仓库里的镜像而非本地构建:
version: "3.8" services: app: image: registry.example.com/myapp:1.0.0 restart: always ports: - "3000:3000" env_file: - .env然后执行:
docker compose -f docker-compose.prod.yml pull docker compose -f docker-compose.prod.yml up -d升级版本时,修改compose文件里的版本号,然后重新pull + up -d即可。如果要回滚,把tag改回旧版本再执行一次,整个过程干净利落。
7.3 CI/CD集成:Git提交自动触发构建
上面还是手动流程,真正生产级做法是接入CI/CD流水线。当Git代码提交时,自动触发:拉代码、构建镜像、打版本号、推送镜像仓库,然后在服务器上执行拉取和更新。工具选型上,GitLab CI低频、稳定,GitHub Actions也不错,如果整套都在一台机器上,轻量方案用Drone就很舒服。
CI流程文件(以GitLab CI风格为例)核心阶段就是构建与部署:
stages: - build - deploy build-image: stage: build script: - docker build -t registry.example.com/myapp:$CI_COMMIT_SHORT_SHA . - docker push registry.example.com/myapp:$CI_COMMIT_SHORT_SHA deploy: stage: deploy script: - ssh deploy@server "cd /opt/myapp && sed -i 's/myapp:.*/myapp:$CI_COMMIT_SHORT_SHA/' docker-compose.prod.yml && docker compose -f docker-compose.prod.yml pull && docker compose -f docker-compose.prod.yml up -d"这套流程跑起来之后,日常发布就变成一行git push的事,而且每个版本都有对应的镜像tag,天然支持回滚。
8. 实用经验:那些会帮到你、但书里没有的技巧
最后分享一些我在实际项目中沉淀下来的个人经验,这些细节很琐碎,但能帮你在关键时刻少折腾好几个小时。
第一个是关于容器时间的。默认情况下,Docker容器的时间是UTC时区,和国内的时间差8小时。日志时间全部错位,排查线上问题会有种错乱感。解决办法很简单,在compose文件的service下加两个环境变量:
environment: - TZ=Asia/Shanghai固定加这两个值,不会额外消耗任何资源,却能在日志和调度上减少大量的无谓困惑。
第二个是关于镜像清理的。运行一段时间的服务器,/var/lib/docker目录很容易膨胀,因为每次构建都会产生中间镜像、缓存层。定期跑一下docker system prune -a,能回收不少磁盘。注意这个命令会删除所有未被任何容器使用的镜像和构建缓存,如果你有“只是想留着备用”的镜像,会被一并清理,生产环境执行前务必三思。
第三个是关于容器内数据库备份的。用mysqldump命令在容器里备份数据库时,最好不要直接进容器交互式执行,一条命令直接在宿主机完成:
docker exec mysql8 sh -c 'exec mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" --databases app_db' > backup.sql这里利用了docker exec的非交互模式,把容器的stdout重定向到宿主机的文件。既不用进入容器,又拿到了备份文件,配合定时任务就可以形成自动备份方案。
最后一个是关于docker-compose版本兼容的。新版Docker已内置了docker compose(没有横杠,作为docker的子命令)以及老式的docker-compose独立命令。两者语法基本一致,但新版自带的那个是老式独立命令的超集,所以新项目直接使用新命令即可,老项目在迁移时,最好先在测试环境把compose文件跑通验证,避免直接在生产切换命令而出现问题。