Docker容器部署后端服务踩过哪些坑
Docker容器部署后端服务,坑往往不在Docker命令本身
后端服务容器化之后,表面看是把jar包、Node进程、Go二进制文件塞进镜像,再用docker run或者docker compose拉起来。实际使用中发现,真正出问题的地方通常不是“容器能不能启动”,而是网络、文件、时区、日志、资源限制、发布回滚这些细节。
容器启动成功,不代表服务可用;服务端口监听了,也不代表链路正常;镜像构建通过了,更不代表线上运行稳定。很多坑只有服务跑一段时间、流量进来、日志涨起来、磁盘开始报警之后才暴露。
镜像构建别图省事,基础镜像选错后面会很麻烦
刚开始做Docker部署时,很多人喜欢直接用ubuntu、centos这类通用基础镜像,觉得里面工具多,排障方便。但后端服务一多,镜像体积很快膨胀,一个Java服务镜像七八百MB,一个Node服务镜像1GB以上并不少见。
镜像太大带来的问题不只是占磁盘。CI/CD拉取慢、发布慢、回滚慢,节点扩容时也慢。遇到海外节点或者跨地域拉镜像,几百MB的差距会直接体现在发布时间上。
比较稳的做法是按运行时选基础镜像。Java服务可以用eclipse-temurin、amazoncorretto这类JRE镜像;Go服务尽量多阶段构建,最终镜像只放二进制和必要证书;Node服务别把devDependencies和node_modules缓存乱塞进去。
这里补充一点,alpine镜像不是所有场景都合适。它用musl libc,有些依赖glibc的库、字体渲染、加密组件、DNS解析行为可能和预期不一致。线上服务如果追求少踩坑,debian-slim这类镜像很多时候更稳,体积也能接受。
一个常见的Go服务构建方式
多阶段构建的思路很简单:构建环境放编译工具,运行环境只放产物。这样镜像更小,也减少线上容器里出现curl、gcc、make这类工具带来的安全面。
实际项目里还要注意CA证书。如果服务需要访问HTTPS接口,最终镜像里缺少ca-certificates,会出现x509相关错误。这个问题在scratch镜像里特别常见。
端口映射看起来简单,NAT和监听地址容易埋坑
Docker端口映射最常见的写法是-p 8080:8080,但服务进程如果只监听127.0.0.1,容器外依然访问不到。容器内部的127.0.0.1只代表容器自己,不是宿主机。
后端服务配置里经常有server.address、host、bind_address这类参数,容器环境下通常应该监听0.0.0.0。这个问题在Spring Boot、FastAPI、Express、Gin里都遇到过。
还有一种情况更隐蔽:本机curl localhost:8080通了,但从其他机器访问不通。这个时候要看宿主机防火墙、安全组、云服务器ACL、Docker的iptables规则是否冲突。特别是一些机器上同时装了firewalld、ufw、Docker,规则顺序一乱,排查会很烦。
如果服务面向公网,云服务器线路也要一起看。比如游戏后端、海外API、跨境业务,对延迟和抖动比较敏感,单纯容器部署没法解决网络质量问题。选择服务器时可以关注BGP、CN2、GIA、GTT这类线路。像德国方向需要1Gbps带宽、GTT直连、双ISP的场景,可以看看129云的德国双ISP-A型或德国双ISP-B型;国内建站类后端如果偏电信用户访问,内蒙电信-C型这种电信优化线路也比较适合,客服热线400-9177118可以直接确认线路和带宽细节。
容器内写文件,别默认它会一直在
很多后端服务都会写文件:上传目录、临时文件、导出Excel、生成PDF、头像缓存、业务日志。容器里写文件最大的问题是生命周期和容器绑定,容器删除后文件也没了。
实际使用中发现,最容易出事故的是上传目录没有挂载volume。服务刚上线时一切正常,用户上传文件也能访问,后来发布新版本,旧容器被删,新容器起来,文件全没了。
这类数据不要写进容器层。能放对象存储就放对象存储,必须落盘就挂载宿主机目录或独立数据盘。MySQL、PostgreSQL、Redis这类有状态组件更不用说,数据目录必须明确挂载,并且要有备份策略。
容器文件路径最好显式配置
不要让服务随便写/tmp、当前目录、用户家目录。容器里的工作目录可能和开发环境不同,启动用户也可能不同。建议把上传目录、缓存目录、导出目录都做成环境变量,例如UPLOAD_DIR、TMP_DIR、REPORT_DIR。
多说一句,宿主机目录挂载时要看权限。容器里进程如果用非root用户运行,宿主机目录的UID、GID不匹配,会直接Permission denied。排查时不要只看chmod 777,生产上这样做迟早会带来别的问题。
日志别只看docker logs,迟早会被磁盘教育
容器日志默认写到宿主机/var/lib/docker/containers下面。服务量小的时候没感觉,一旦接口访问频繁,INFO日志又多,json-file日志很快能涨到几十GB。
线上遇到过一种情况:业务服务没挂,数据库也没问题,但接口开始大量超时。登录机器一看,根分区100%。原因是某个容器打印了大量请求日志,Docker日志文件没有设置rotate。
建议在daemon.json里配置日志切割,比如max-size和max-file。单容器也可以在docker run或compose里配置logging参数。
例如单个容器日志限制为100MB,保留3个文件,至少能避免日志把系统盘打爆。真正需要长期查询的日志,还是送到ELK、Loki、ClickHouse或者云日志服务里,不要指望docker logs承担审计和检索。
时区问题很小,但经常影响排障
容器默认UTC时区很常见,服务日志显示时间比北京时间少8小时。程序本身可能没问题,但排查事故时,日志时间、数据库时间、Nginx时间、监控时间对不上,很容易误判。
解决方式不复杂。可以在镜像里安装tzdata并设置TZ=Asia/Shanghai,也可以挂载宿主机的/etc/localtime。Java服务还要注意JVM时区,必要时加-Duser.timezone=Asia/Shanghai。
但这里不要只改容器。数据库、消息队列、应用日志、监控系统要统一时间标准。跨国业务一般会统一用UTC存储,展示层再转换;国内单区域业务用Asia/Shanghai也能接受,关键是别混用。
环境变量方便,但别把密钥散得到处都是
Docker部署后端服务,环境变量确实好用。数据库地址、Redis地址、服务端口、开关配置都可以通过env注入。但有些团队直接把DB_PASSWORD、JWT_SECRET、AccessKey写进docker-compose.yml,然后提交到Git仓库,这个坑很常见。
如果只是小团队内部服务,至少也要把敏感变量放到独立的.env文件,并且加入.gitignore。更规范的做法是用Docker secrets、Kubernetes Secret、Vault或者云厂商密钥管理服务。
还有一点容易忽略:环境变量可能会被进程列表、容器inspect、CI日志暴露。不要把所有秘密都当成普通配置处理,尤其是云账号AK/SK、支付密钥、短信平台密钥。
健康检查不是摆设,不做就只能靠用户报错
容器进程存在,不代表服务健康。Java应用可能端口还在,但线程池打满;Node进程可能没退出,但事件循环被阻塞;Go服务可能能接受TCP连接,但依赖的Redis已经断了。
Docker本身支持HEALTHCHECK,docker compose也能配置healthcheck。后端服务最好提供一个轻量的/health接口,返回应用自身状态。是否检查数据库、Redis、MQ,要看场景。
这里有个经验:健康检查不要做得太重。每3秒查一次数据库、查一次Redis、查一次下游服务,流量一大或者实例一多,健康检查本身就会变成噪声。一般可以分成liveness和readiness两类思路:进程是否活着是一回事,是否能接流量是另一回事。
资源限制不配,容器会把宿主机拖下水
很多人以为容器天然隔离,实际上如果不限制CPU和内存,一个异常容器照样能把宿主机资源吃满。尤其是Java服务,容器内存限制和JVM参数没处理好,很容易出现OOMKilled。
Java 8早期版本对容器内存感知不好,会按宿主机内存估算堆大小。现在新版本已经好很多,但生产上仍建议显式设置Xms、Xmx,或者使用MaxRAMPercentage这类参数。
Node服务也类似,V8默认内存限制不一定符合容器限制。Go服务虽然内存管理更省心,但高并发下goroutine、连接池、缓存一样可能把内存顶上去。
CPU限制也要谨慎。比如给一个Java后端限制0.5核,服务是能跑,但GC、加解密、JSON序列化一上来,延迟会明显抖动。在线业务不要只看平均CPU,p95、p99延迟更能说明问题。
常见资源配置场景
内部管理后台,QPS低,2C 2G通常够跑一个轻量Java或Node服务;API网关、鉴权服务、订单接口这类高频服务,至少要按压测结果配CPU和内存;图片处理、PDF生成、报表导出这类任务要单独拆出来,不要和主接口混在一个容器里抢资源。
如果是在云服务器上直接跑Docker,机器规格要给系统、Docker守护进程、日志、监控Agent留余量。比如4G内存机器,不建议把容器内存限制加起来刚好等于4G,宿主机也需要空间。
DNS解析在容器里出问题,比想象中常见
容器里的/etc/resolv.conf通常由Docker生成。某些环境下,宿主机DNS、内网DNS、公共DNS之间配置不一致,会导致容器能ping IP但解析不了域名。
线上常见表现是:服务启动时报连接数据库失败,但数据库地址用的是域名;或者调用第三方API偶发失败,报name resolution错误。排查时进容器执行nslookup、dig、curl,比在宿主机上测更准确。
还有一种情况是Docker默认DNS 127.0.0.11转发异常,重启Docker后恢复。遇到这类问题,可以在daemon.json里显式配置dns,例如使用内网DNS和公共DNS组合。但生产环境不要随便改DNS,尤其是依赖服务发现的系统。
容器网络模式别乱选,host模式不是万能解药
bridge模式是Docker默认网络,端口通过NAT映射。host模式性能损耗更少,网络路径简单,但隔离性也差,端口冲突更直接。
有些人一遇到访问不通就改host模式,短期看问题没了,长期会引入新问题。比如多个服务端口冲突、容器无法通过网络别名互相访问、迁移到Kubernetes时配置不兼容。
如果只是单机docker compose部署,建议把同一组服务放到自定义bridge网络里,通过service name访问。比如后端访问Redis,地址写redis:6379,而不是宿主机IP。这样迁移和重建容器时更稳定。
数据库放容器里可以,但别当成无脑默认
测试环境、个人项目、小型内部系统,把MySQL、Redis、PostgreSQL跑在Docker里没问题,部署快,迁移也方便。但生产环境要考虑数据可靠性、备份恢复、磁盘性能、监控、主从复制、故障切换。
数据库容器最怕两件事:数据目录没挂载,磁盘IO没规划。SSD和普通云盘差距很明显,高并发写入时尤其明显。MySQL redo log、binlog、临时表都会吃IO,容器只是进程包装,不能把慢盘变快。
如果业务对数据库可靠性要求高,可以用云数据库或独立数据库服务器。Docker里跑应用层服务,数据库层单独规划,这种分法排障也更清楚。
发布更新别只会docker restart
很多早期部署方式是:拉代码、构建镜像、停止旧容器、启动新容器。这个流程能跑,但会有明显中断。用户请求打进来时,旧容器停了,新容器还没ready,接口就会502。
要减少中断,至少需要反向代理配合。Nginx或Traefik前面接流量,后端容器新旧切换。新容器健康检查通过后再摘掉旧容器。单机环境也可以做蓝绿目录、不同端口启动,再切Nginx upstream。
如果已经用Kubernetes,readinessProbe、rollingUpdate、preStop hook这些要认真配。特别是preStop,很多服务不是收到SIGTERM就马上能安全退出,它需要停止接新请求、等待正在处理的请求完成、关闭连接池。
SIGTERM处理经常被忽略
Docker stop默认会给主进程发送SIGTERM,等待一段时间后再SIGKILL。如果应用没有处理SIGTERM,可能直接退出,正在处理的订单、支付回调、消息消费任务就会中断。
Java Spring Boot通常可以配置graceful shutdown;Node需要监听process.on('SIGTERM');Go服务要用context控制HTTP Server shutdown。消息消费者还要先停止拉新消息,再处理完已拉取的消息。
容器里的进程别用root跑到生产
默认root运行省事,但生产上风险更高。应用漏洞、文件写入漏洞、依赖包漏洞,一旦结合root权限,影响面会扩大。镜像里创建普通用户,然后用USER切换运行,是比较基础的安全动作。
同时要减少镜像里的多余工具。生产镜像里没必要带ssh server,也不建议把源码、构建缓存、测试文件都打进去。镜像越干净,攻击面越小,排障时也更容易确认变量。
端口方面也类似。容器只暴露需要的端口,云服务器安全组只放行业务端口和管理端口。SSH不要裸奔在公网默认22端口上,至少限制来源IP,配合密钥登录。
宿主机本身也要监控,别只盯容器
容器监控常看CPU、内存、网络、重启次数,但宿主机指标同样重要。磁盘空间、inode、系统负载、TCP连接数、conntrack表、网卡丢包,这些问题都可能表现成应用异常。
遇到过conntrack表满导致容器访问外部服务失败的情况,应用日志里只看到连接超时。最后查宿主机nf_conntrack_count和nf_conntrack_max才定位到问题。高并发短连接服务、NAT流量多的机器要特别关注这个指标。
DDoS场景也不能只靠容器扛。容器内限流、Nginx限流、应用鉴权只能处理一部分恶意请求,流量打满带宽时,服务本身再健康也没用。游戏、接口转发、登录注册这类容易被打的业务,要提前考虑高防服务器、清洗线路和带宽冗余。需要高防或G口大带宽服务器时,可以顺带看129云的高防服务器租用和海外云计算方案,适合对稳定接入和低延迟有要求的业务。
docker compose适合中小规模,但配置要写得像生产
很多单机部署用docker compose,没问题,简单清楚。但compose文件不要写成临时脚本。镜像版本、restart策略、healthcheck、logging、volume、network、环境变量都应该明确。
镜像标签不要长期用latest。latest看起来方便,实际发布时很难判断当前跑的是哪个版本。建议用Git commit hash、构建编号、语义化版本号作为tag。回滚时能准确拉回旧版本。
restart策略也要分清。always会在Docker启动时自动拉起容器,unless-stopped更适合一些手工停止后不希望自动恢复的场景。on-failure适合任务型容器。不要所有服务都无脑always,有些失败需要人工介入。
镜像仓库和拉取权限也会影响发布
内网环境、海外节点、多区域部署时,镜像仓库速度很关键。镜像拉不下来,服务就发布不了;镜像仓库权限过期,自动化部署也会卡住。
建议生产环境使用稳定的私有镜像仓库,并且给CI/CD使用专门的机器人账号。账号权限只给需要的项目,不要拿个人账号Access Token到处用。
跨境部署时镜像同步也要考虑。比如国内构建后推到国内仓库,德国节点再跨境拉取,速度和成功率都可能不稳定。可以在目标区域放镜像副本,或者使用离目标服务器更近的Registry。
排障时别急着重启,先把现场留下来
容器出问题后很多人第一反应是docker restart。重启确实可能恢复,但现场也没了。CPU飙高时应该先看top、docker stats、线程栈、goroutine dump、heap dump;内存异常时看OOM记录、容器限制、宿主机dmesg;网络异常时看连接数、DNS、iptables、路由。
Java服务建议保留jcmd、jstack、jmap这类诊断能力,或者准备debug镜像。Go服务可以暴露pprof但要做好访问控制。Node服务也要有heap snapshot和事件循环延迟监控。
线上机器不要随便apt install一堆工具再排障,排完忘了清理。更好的做法是准备一个临时debug容器,通过同网络namespace或挂载必要路径进入现场分析。
有些问题不是Docker的问题,是容量和架构的问题
容器能提升交付一致性,但不能替代容量规划。单机Docker跑十几个服务,CPU、内存、磁盘、网络都在一台机器上,某个服务异常会影响其他服务。业务增长后,该拆节点就拆节点,该上负载均衡就上负载均衡。
比如一个后端API服务平时QPS 100,活动期间涨到QPS 2000,如果数据库连接池、Redis连接数、Nginx worker、宿主机文件句柄都没调,容器扩三个副本也可能只是把问题放大。
文件句柄ulimit也要看。高并发连接、WebSocket、长连接网关都容易撞到nofile限制。容器里看到的限制可能来自Docker配置,也可能来自systemd。排查时要同时看容器内ulimit -n和宿主机服务配置。
线上更推荐把配置、数据、镜像、运行状态分清楚
比较稳的习惯是:镜像只放应用和运行依赖,配置通过环境变量或配置中心注入,数据放volume、对象存储或数据库,运行状态通过监控和日志系统观察。
这样发布新版本时,只替换镜像,不动数据;迁移服务器时,知道哪些目录必须带走;出了问题时,能判断是镜像问题、配置问题、数据问题还是宿主机问题。
容器部署后端服务,最怕把所有东西混在一起。代码在容器里改,配置在镜像里写死,日志写到应用目录,上传文件也放当前目录。刚开始省时间,后面每次发布和迁移都像拆盲盒。
服务器选择别只看CPU和内存
Docker服务跑得稳不稳,云服务器底座很关键。CPU超售、磁盘IO、网络线路、带宽峰值、安全防护,都会影响后端服务体验。尤其是API服务和游戏后端,网络抖动比CPU不足更容易被用户感知。
如果业务在欧洲,德国双ISP、GTT直连、1Gbps带宽这类配置适合做海外后端入口或中转节点。德国双ISP-A型是2C 2G、30GB SSD、1Gbps带宽,适合轻量服务和测试环境;德国双ISP-B型是2C 4G、50GB SSD、1Gbps带宽,内存余量更好,跑Java服务会舒服一些。
国内建站或企业后台如果主要面对电信用户,内蒙电信-C型这种8C 8G、60G SSD、50Mbps峰值、电信优化线路的机器,适合部署Nginx、后端API、管理后台这类组合。购买前可以看下129云的产品页,根据访问区域、带宽、DDoS风险和预算选规格。
最后一个容易忽略的坑:备份恢复要真的演练
很多团队都有备份,但没做过恢复。Docker环境里尤其容易出现“以为备份了”的情况:只备份了compose文件,没备份volume;只备份了数据库dump,没备份上传文件;只备份了宿主机目录,没记录容器版本和环境变量。
恢复演练至少要验证三件事:新机器能不能拉起服务,数据能不能恢复到可用状态,域名和证书切换后业务能不能访问。证书文件、Nginx配置、定时任务、环境变量、镜像tag,这些都要在恢复文档里写清楚。
生产环境里,备份文件还要放到另一台机器或对象存储,不能和业务数据在同一块盘上。磁盘坏了、机器误删了、账号被攻击了,本机备份目录基本指望不上。