资讯动态

Docker 上云部署踩坑实录:从本地到云服务器的稳定运行指南

发布时间:2026/10/22 9:31:15 来源:尧图企业网站定制
把一套基于 Docker 的 Web 服务从本地开发环境迁到云服务器我前后折腾了两三天。看日志、查文档、改配置反反复复总算把服务稳定跑起来了。整个过程踩了不少坑有些问题现在回想起来很简单但当时确实卡了很久。这篇就当作一份踩坑实录把我实际遇到的问题、排查思路和最终方案都摊开讲讲给准备做 Docker 上云的同学一个参考。Docker 上云这件事本身并不复杂无非是把本地跑通的容器搬到云服务器上继续跑。但“跑起来”和“稳定跑”之间隔着很多细节系统环境、镜像下载、端口暴露、数据持久化、日志清理、资源限制……任何一个环节没处理好都可能让服务半夜挂掉。这篇内容主要面向已经把 Docker 基本命令搞熟、准备把个人项目或小规模服务部署到云上的开发者。1. 上云前的准备与方案选型1.1 服务器选型与系统版本选择很多人在本地开发时用 Docker Desktop一切都很顺滑于是想当然认为云服务器上也一样。但云服务器和你自己的电脑差距很大尤其是资源规格和网络环境。我第一次踩的坑就是在 1 核 1G 的云服务器上直接跑 Docker结果容器刚启动系统就卡到没反应。原因很简单镜像拉取、容器进程、日志、以及 Docker 守护进程本身都在抢那 1G 内存。所以第一步不是急着装 Docker而是先想清楚你的业务需要多大资源。我个人的建议是个人博客、小型 API 服务2 核 2G 起步勉强够用但建议选 2 核 4G省心很多。带数据库、队列、缓存的中型服务4 核 8G 是基本盘另外要考虑磁盘 IOPS。如果只跑一个静态网站 Nginx1 核 1G 也不是不行但后续扩展空间几乎为零。系统版本方面我强烈建议选 Debian 系的发行版比如 Ubuntu Server 或者 Debian。原因有两个一是 Docker 官方对这两类系统的支持最完善安装文档齐全二是网上大部分踩坑帖、解决方案都是基于 Ubuntu 的遇到问题好搜。尽量不要用 CentOS 8 那种已经停止维护的系统安全更新都没有出了漏洞很被动。系统架构选 x86_64除非你对 ARM 生态非常有经验否则不要为了省那点钱选 ARM 实例很多镜像的兼容性会让你怀疑人生。1.2 安装 Docker 的正确姿势与源配置Docker 的安装不同系统版本号对应的安装命令有些差异所以不要上来就apt install docker.io或者yum install docker那样装的不一定是官方最新版。更稳妥的做法是使用 Docker 官方提供的安装脚本curl -fsSL https://get.docker.com -o get-docker.sh sudo sh get-docker.sh这个脚本会自动检测你的系统配置官方源的 GPG 密钥并安装最新稳定版。装完之后用docker version验证一下确保 client 和 server 都有输出。如果发现只有 client 而没有 server多半是守护进程没启动执行sudo systemctl start docker sudo systemctl enable docker再查。这里有个很多新手容易忽略的点把当前用户加入 docker 组省得每次敲命令都要 sudo。sudo usermod -aG docker $USER newgrp docker然后就可以免 sudo 执行 docker 命令了。注意这个操作相当于把 docker 的权限交给了该用户在多人共用的服务器上要谨慎因为 docker 组里的用户理论上可以挂载宿主机目录拿到宿主机的读写权限。配置源这一步也很关键。国内访问 Docker Hub 的镜像仓库速度不稳定而且经常超时。拉取镜像失败的典型报错是dial tcp: lookup registry-1.docker.io on 10.0.0.2:53: no such host或者一直卡在 pulling。解决办法是配置镜像加速器。在/etc/docker/daemon.json里加入{ registry-mirrors: [https://docker.m.daocloud.io] }然后重启 dockersudo systemctl daemon-reload sudo systemctl restart docker镜像加速本质上只是帮你从更近的节点拉取镜像它解决了拉取超时的问题但并不能保证所有镜像都能加速尤其是那些冷门镜像还是有可能失败。真遇到拉不下来的情况可以考虑直接用docker pull加具体 tag 重试或者换一个镜像源试试。每次改完 daemon.json记得要重启 docker 才能生效。1.3 基础镜像选择与版本固定策略镜像选得对不对直接影响后续的排障难度。我见过很多同事直接用node:latest、python:latest、nginx:latest这种写法在本地开发时没问题但在生产环境就是定时炸弹。因为latest标签会随着时间漂移你本地跑的是今天的版本云服务器上拉到的可能是昨天的或者明天的行为不一致排查起来非常痛苦。我的习惯是固定到具体的小版本号比如node:18.20.2-alpine或者至少固定到大版本node:18-alpine。Alpine 版本体积小镜像通常只有几十兆部署和启动都很快。但要注意Alpine 用的是 musl libc和不少二进制兼容性有问题比如某些原生模块需要编译或者像 Sharp 这种图像处理库装的时候得多花点时间编译。如果不想折腾就用 Debian 或 Ubuntu 为基础镜像虽然体积大一点但兼容性省心。确定好镜像后我建议再单独做一件事在本地先把镜像 pull 下来并用docker inspect看一下默认的 CMD、ENTRYPOINT、暴露的端口和环境变量。生产环境排障时很多问题其实从镜像定义就能看出来。比如镜像默认的工作目录不是你以为的位置或者容器启动时依赖某个环境变量你没设置容器必然退出。提前摸清镜像的“脾性”后面就不用瞎猜了。2. 核心环境搭建与容器编排2.1 告别裸命令用 Compose 管理容器用 docker run 裸命令启动单个容器在本地练手完全没问题。一旦要上云你会面临磁盘目录挂载、网络隔离、环境变量、依赖顺序、日志策略、重启策略等一堆问题。如果还是一条一条 docker run你会陷入灾难。举个例子数据库容器和业务容器启动顺序错了业务容器连接数据库超时就直接退出你根本分不清到底是配置问题还是启动顺序问题。我的建议是直接上 Docker Compose。先把所有容器编排写在一个 compose.yaml 里用配置文件把网络、卷、环境变量、依赖关系都声明清楚。这样可以做到“一条命令拉起整个服务栈”version: 3.8 services: web: image: your-node-app:1.0.0 restart: unless-stopped ports: - 80:3000 depends_on: - redis - db environment: - NODE_ENVproduction - TZAsia/Shanghai volumes: - app-data:/data networks: - app-net deploy: resources: limits: memory: 512M redis: image: redis:7-alpine restart: unless-stopped volumes: - redis-data:/data networks: - app-net db: image: postgres:16-alpine restart: unless-stopped environment: - POSTGRES_USERappuser - POSTGRES_PASSWORDapppass - POSTGRES_DBappdb volumes: - pg-data:/var/lib/postgresql/data networks: - app-net volumes: app-data: redis-data: pg-data: networks: app-net: driver: bridge这套配置里面有几个细节值得说restart: unless-stopped可以让服务在宕机或重启后自动拉起是生产环境的基础配置。但不要用 always因为一旦容器因为代码 bug 反复崩溃always 会让它无限重启占满系统资源。unless-stopped 是“除非你手动 stop 才不强拉”容错性更好。depends_on 只能保证容器的启动顺序不能保证容器内的服务已经就绪。比如数据库容器虽然启动了但 PostgreSQL 还在初始化中业务容器连接照样失败。要么在业务代码里加重试逻辑要么在容器启动脚本里加一个 wait-for-it 的循环探测。deploy.resources.limits 限制内存这是我在 2G 内存服务器上稳定跑多个容器的关键。没有限制的话某个容器内存泄漏瞬间吃光主机内存整个服务器直接卡死。networks 单独建了一个 bridge 网络让容器之间通过服务名互相访问而不是通过宿主机的 IP 去访问这样网络更清晰也避免了端口冲突。2.2 网络模式的选择bridge 与 host 的实际差异网络模式这里很多教程会简单说“用默认的 bridge 模式就好”但实际场景里桥接和 host 的差异很大我踩过坑。第一次部署时我图省事服务器上跑了 80 和 443 端口的 Nginx还挂着几个业务容器全用默认 bridge 模式 端口映射的方式。后来新增一个服务需要在 8080 端口监听我直接写了ports: 8080:8080结果怎么也访问不了宿主机上 curl 一下端口却是通的。问题出在哪呢其实是我把端口映射写反了正确写法是宿主机端口:容器端口。我写成了8080:3000而容器内服务监听的是 3000所以宿主机 8080 没人监听。这种错误听起来很蠢但确实是新手最容易犯的问题尤其是容器端口改过之后配置文件里的映射没同步改。bridge 模式的好处是网络隔离每个容器有独立的网络命名空间可以通过端口映射控制对外暴露哪些服务。但它有一个隐含问题Docker 的 iptables 规则会拦截所有从外部访问容器映射端口的流量如果你同时在宿主机上跑了防火墙软件比如 ufw 或者云厂商的安全组双层过滤很容易导致端口明明放行了却不通。遇到这种情况不要急着关防火墙先看 Docker 端口映射有没有生效再看云控制台的安全组入站规则有没有放行。排查顺序很有讲究。host 模式则是直接共享宿主机的网络命名空间容器内的服务监听在宿主机 IP 上就没有端口映射这一层性能损耗更小。但也意味着不能运行两个监听同一端口的容器而且容器之间要通过宿主机 IP 端口访问网络隔离形同虚设。我的建议是常规 Web 服务用 bridge对网络性能极敏感的服务比如 Redis 这种如果实在在意延迟可以用 host但前提是你能确定端口不会冲突。2.3 目录挂载与数据卷规划容器是有生命周期的一句docker rm就能把你容器里产生的数据全部清除。所以凡是需要持久化的数据必须挂到宿主机或者命名卷里。我第一版部署时直接把数据库的数据文件放在了容器默认目录后来容器重建数据全没了只能从备份恢复。这个教训很深刻。Docker 提供了两种持久化方式bind mount直接把宿主机的一个目录挂到容器里比如./nginx/html:/usr/share/nginx/html。named volume由 Docker 管理的卷比如pg-data:/var/lib/postgresql/data数据保存在/var/lib/docker/volumes/下。bind mount 的好处是直接可见、可编辑适合配置文件和静态资源。named volume 的好处是 Docker 帮你管理备份和迁移更规范但不直接看得到文件路径。我的建议是数据库数据用 named volume业务代码或配置文件用 bind mount。这样既方便日常修改配置又能保证数据库数据不受容器生命周期干扰。目录规划上我在服务器上建了这么一个结构清晰好记/opt/projects/myapp/ ├── compose.yaml ├── .env ├── data/ │ ├── backups/ │ └── uploads/ └── logs/在 compose.yaml 里业务容器的日志文件通过 bind mount 挂到这个 logs 目录方便用工具统一分析。这里还有一个细节名称带点.的目录比如 .env在 compose 项目目录下默认会被 Docker 识别为环境变量文件自动加载进去不需要手动 --env-file 指定。这既是便利也是坑如果你有其他点开头命名的目录注意别把敏感信息放进去。3. 数据持久化与备份恢复实操3.1 权限问题容器用户与宿主机的 UID/GID用 bind mount 把目录挂进容器后经常遇到一个问题容器里写的文件在宿主机上打开提示没有权限或者在宿主机上创建的文件容器里读不了。这背后的本质是 UID/GID 不一致。容器内的 root 用户UID 是 0宿主机上你的用户 UID 可能是 1000。容器内进程以 root 运行写的文件文件属主就是 UID 0宿主机上的普通用户自然没法写。解决这个问题的方法也不是很复杂。第一种在 compose.yaml 里给容器指定 user用user: 1000:1000运行但这有个前提容器镜像里得有这个 UID 对应的用户或者你让进程直接以宿主机 UID 运行。第二种改宿主机目录的属主把挂载目录 chown 成容器内进程的 UID比如chown -R 1000:1000 ./data。第三种建一个和宿主机相同 UID 的用户再执行命令但容器里往往没有这个用户还得额外添加比较麻烦。我最常用的方案是在 Dockerfile 里显式创建一个和宿主机用户 UID 一致的用户然后用该用户运行应用。比如宿主机用户 uid 是 1000那 Dockerfile 里FROM node:18-alpine RUN addgroup -g 1000 app adduser -u 1000 -G app -s /bin/sh -D app USER app这样容器内进程创建的文件的属主在宿主机上正好是你的用户不会出现权限冲突。如果你用的是 Debian 系镜像命令换成groupadd --gid 1000 app useradd --uid 1000 --gid app app。做完之后挂载目录的权限问题基本就消停了。3.2 数据库备份与恢复的落地脚本上云之后数据一旦丢失麻烦不是本地开发能比的。云服务商一般会提供快照功能但快照是针对整个磁盘的恢复起来动静大不适合做高频备份。所以我额外搭了一层定时备份直接备份数据库数据简单有效。以 PostgreSQL 容器为例备份可以用容器内自带的 pgdump 工具也可以直接用文件系统级别的备份。文件系统备份最简单就是复制数据卷的内容但前提是数据库在一致状态如果数据库正在写直接复制文件可能导致数据不一致。稳妥的做法还是用数据库自己的导出工具docker exec -i postgres-container pgdump -U appuser -d appdb --clean --if-exists backup.sql这一步会把数据库导出成 SQL 文件。恢复的时候把这个文件喂回去cat backup.sql | docker exec -i postgres-container psql -U appuser -d appdb也可以不通过容器执行命令把数据存到宿主机docker cp postgres-container:/var/lib/postgresql/data/dump.tar.gz ./backups/这种方法适合把备份文件统一收集到宿主机上。接下来用 crontab 实现定时0 3 * * * /opt/scripts/backup_pg.sh /var/log/backup.log 21脚本里面除了执行备份还要清理过期备份不然磁盘会被塞满。我倾向保留最近 7 天的日备、最近 4 周的周备超出就删掉。备份文件最好传一份到对象存储或者单独的服务器上否则服务器本身挂了备份也跟着没了那备份就失去意义了。3.3 日志处理与文件描述符泄漏容器里的进程把日志打到 stdoutDocker 会把它接到宿主机上。如果容器从来没有配置日志轮转那 log 文件会无限增长。尤其是 Web 服务和数据库线上访问一多日志一晚上几个 G 不是开玩笑。磁盘被日志撑满之后容器和宿主机都会出现各种诡异现象数据库写不进去、docker 命令卡住、系统警告磁盘剩余空间不足。解决方案有两个层面。第一个层面在 Docker 引擎层设置日志轮转在/etc/docker/daemon.json里加上{ log-driver: json-file, log-opts: { max-size: 20m, max-file: 5 } }这表示每个容器最多保留 5 个日志文件每个文件 20M超过的自动滚动。这是全局配置对所有容器生效是最省心的做法。第二个层面在容器内部日志输出的时候尽量用日志框架自带的轮转功能。比如 Node.js 里的 pino 或 winston都可以配置按大小或按时间切割。两个层面结合日志管理才算完整。文件描述符泄漏的问题我也遇到过一次。某个容器跑了一周之后突然报too many open files错误重启容器就好了但过几天又犯。排查到最后发现是宿主机的ulimit -n设得太低只有 1024容器内进程创建的连接数一多就碰到上限。在 systemd 管理 docker 的情况下需要修改/etc/systemd/system/docker.service.d/limits.conf[Service] LimitNOFILE1048576然后systemctl daemon-reload systemctl restart docker生效。这个参数对高并发服务很重要尤其是大量长连接、WebSocket 服务建议提前调好。4. 常见故障排查与性能优化实录4.1 经典故障清单与快速定位方法我把自己和身边朋友踩过的高频问题整理成一个速查表排障时就照着表逐步定位症状可能原因快速排查方法容器启动后秒退启动命令不对、环境变量缺失、依赖服务未就绪看 docker logs 容器名定位退出前最后一条日志外部访问不通但本地 curl 通端口映射写错、安全组未放行、防火墙拦截docker ps 看映射ss -lntp 看监听云控制台看安全组容器内无法访问外网DNS 配置问题或宿主机网络受限docker exec 容器内 ping 8.8.8.8再看 /etc/resolv.conf磁盘被逐步占满镜像缓存过多、日志未轮转、数据卷膨胀df -h 看整体ncdu 或 du -sh /var/lib/docker/* 定位大文件CPU 飙高容器未限制资源某个进程占用全核docker stats 看容器 CPUhost top 定位进程容器间互相访问不了网络配置不一致服务名解析失败composer 建同一 network用服务名访问检查 DNS这些排障操作在 docker 里都有对应的命令我做几个演示。镜像缓存占满磁盘的问题用docker system df可以看总结再用docker image prune -af清理所有未使用的镜像。注意这样会把你手动拉下来但暂时没用的镜像也清掉下次启动时会重新拉取。如果磁盘告急但不敢删镜像至少可以用docker system prune --volumes把悬空数据卷清掉。内存问题比较隐蔽。docker stats只能看到容器的实时占用如果某个容器内存持续增长不回落多半有内存泄漏。定位方法在宿主机上用top -o %MEM确认哪个进程吃内存然后看这个进程对应哪个容器再进容器里用ps aux看具体是哪个子进程。遇到这种情况临时方案是增容治本方案是查代码里的全局缓存、未关闭的连接等泄漏点。4.2 时区与系统时间问题容器默认使用 UTC 时区这会导致日志时间、定时任务、数据库时间跟本地时间差 8 个小时排障的时候很容易混淆。我第一次部署时没注意这个登录容器看日志发现记录的时间比实际时间晚了 8 小时还以为是服务器时钟问题折腾了很久。后来才意识到容器本身跑的是 UTC。解决方案很简单在 compose.yaml 里给每个容器都加上environment: - TZAsia/Shanghai如果容器内没有预置时区文件也可以用挂载方式把宿主机的时区文件映射进去。其实大多数主流镜像都内置了 tzdata只要设置 TZ 环境变量就行。另外宿主机本身的时间也要检查用timedatectl确认是否开启了 NTP 同步。否则宿主机时间漂移所有容器时间都会跟着错乱。4.3 容器限流与资源隔离的调优实践上云服务器资源有限如果没有容器级限流某个容器内存泄漏或 CPU 死循环会把宿主机拖垮。我遇到过最严重的一次Redis 容器因为做过大的 KEYS 扫描CPU 瞬间飙满宿主机整个失去响应只能重启服务器。之后就学乖了所有容器必须设置 limits。在 compose.yaml 里每个服务都加 resource.limitsdeploy: resources: limits: cpus: 0.50 memory: 256M这种语法的资源限制实际的约束力度比较强。CPU 限制 0.50 表示最多用半个核心内存限制 256M 后容器内存超过部分直接 OOM kill。被 OOM kill 的容器Docker 会根据 restart 策略重新拉起业务短暂不可用但至少宿主机不会被拖死。对数据库这种核心服务内存限制要稍微宽裕一点否则频繁 OOM数据一致性会有风险。调优经验上我用过一段时间的docker stats命令观察容器日常占用率然后按照观察到的峰值乘以 1.5 或 2 倍给自己留降解压阀。比如 Web 容器日常占用 128M那限制内存就设置为 256MCPU 日常 0.2那限制就给 0.5。这样既不会浪费资源也有容错空间。不要拍脑袋设置一个很小的值不然业务一冲上来容器直接被杀服务雪崩。4.4 升级与回滚的稳定策略Docker 部署的另一个优势是升级回滚方便。旧版本镜像还在本地缓存的话切换就是一瞬间的事。我的升级流程先用docker compose pull拉取新镜像。修改 compose.yaml 里的镜像版本 tag。docker compose up -d增量更新变化的服务。更新后马上看日志确认没有报错。如果出问题docker compose up -d --build之前可以先备份当前版本标签然后直接把 compose.yaml 改回旧 tag再执行 up -d 回滚。但这里有个大坑如果新版本容器启动失败compose 会把镜像标成 unhealthy旧版本镜像可能被后续的自动清理任务清掉回滚就拉不回来了。所以升级前确认旧镜像 tag 还在本地最好手动docker image tag一个 backup 标签。更保险的做法是镜像上传到私有镜像仓库回滚时直接从仓库拉旧 tag不依赖本地缓存。很多云厂商都提供免费的私有镜像仓库服务自建也行。我的建议是凡是上生产的项目镜像一定要推送到私有仓库本地只留一个稳定版 tag。写在最后Docker 上云这件事第一次部署总是问题最多服务器配置、镜像加速、网络隔离、数据持久化、日志清理、资源限制每一项背后都有坑。走过的这些弯路让我最大的体会是任何服务上生产环境之前先想清楚“数据存哪里、挂了怎么恢复、日志怎么切、内存吃满怎么办”这四个问题。这四个问题思考清楚了大部分故障就能预防在发生之前。最后再分享一个小习惯每次改完 Docker 配置并验证没问题之后我会把改动的 diff 和踩过的坑记到一个简单的变更文档里。这个文档不需要多精美哪怕一个 todo list 都行关键是记录下当时为什么这么改。下次再遇到类似问题一翻记录就能少走很多弯路省掉的排查时间远比记录时间多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑