恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Docker容器化部署实战指南:从安装避坑到生产环境落地
首页
资讯中心
/
Docker容器化部署实战指南:从安装避坑到生产环境落地
Docker容器化部署实战指南:从安装避坑到生产环境落地
发布时间:2026/9/24 19:18:59
身边越来越多的项目采用容器化部署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 wasnt detected。这个报错的意思是系统没有检测到虚拟化支持。很多人第一反应是去BIOS里开启虚拟化但这只是其中一个原因而且不是最常见的。这个问题的排查思路应该是这样的。先去任务管理器“性能”标签页看右下角的“虚拟化”是否显示“已启用”。如果是“已禁用”那就需要重启电脑进BIOS/UEFI里找到Intel Virtualization TechnologyIntel VT-x或AMD SVM Mode开启后保存退出。这一步做完大部分人就能解决。如果你的电脑CPU较老或者主板固件比较特殊BIOS里根本没有虚拟化选项那Docker Desktop基本就告别了。但Docker本身还需要一个东西叫WSL2Windows 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以上否则依赖旧版WSL1Docker 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 ar /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.11slim版本比标准版小很多少了大量用不到的工具链体积可能差出一倍以上。第二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-serverutf8mb4 - --collation-serverutf8mb4_general_ci - --default-authentication-pluginmysql_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_PASSWORDYourStrongPassword2024。然后.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 wasnt detected和npipe连接失败这里把Windows下Docker Desktop各种启动失败的排查清单整理成表方便直接对号入座。报错/现象可能原因解决办法virtualisation support wasnt detectedBIOS未开启虚拟化进BIOS开启VT-x/AMD SVMfailed to connect to the docker api at npipe...WSL2未正确安装或配置执行wsl --updatewsl --set-default-version 2Docker Desktop一直转圈WSL2没有默认发行版执行wsl --install -d UbuntuWSL2启动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 --registryhttps://registry.npmmirror.com COPY . . EXPOSE 3000 CMD [node, src/index.js]docker-compose.ymlversion: 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 deployserver 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: - TZAsia/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文件跑通验证避免直接在生产切换命令而出现问题。