☰
从零开始:用Nginx容器化部署入门Docker实践
2026/10/21 6:31:34 网站建设 项目流程

1. 为什么第一个容器化目标选了Nginx

1.1 从“装完就跑”到“跑完还能复现”的思维转变

很多人第一次接触容器技术,脑子里想的都是“这玩意儿能干嘛”。我当初也一样,看了一堆概念——镜像、容器、仓库、编排——每个字都认识,连起来就不知道在说什么。后来一个前辈跟我说了句话,我记到现在:别管那些名词,你就把它当成一个“装好了就不想再动”的软件盒子。

Nginx恰好就是那个最适合拿来练手的“盒子”。为什么?三个原因。第一,它足够简单,配置文件结构清晰,没有数据库那种复杂的初始化流程;第二,它足够典型,Web服务器是后端开发、运维、甚至前端都会碰到的组件,学会了不浪费;第三,它足够轻量,镜像体积小,拉取快,对新手机器友好,不会因为下载一个几个G的镜像就把耐心耗光。

我见过不少人上来就拿MySQL或者Redis练手,结果卡在数据卷挂载、权限配置、环境变量传递这些坑里,还没摸到容器的门道就被劝退了。Nginx不一样,它跑起来就能看到一个欢迎页,这种即时反馈对建立信心特别重要。

提示:如果你之前只在物理机或者虚拟机上装过Nginx,那容器化部署的体验会让你有种“原来可以这么干净”的感觉。卸载的时候一条命令删掉容器和镜像,系统里不留任何残留文件,这对有洁癖的开发者来说简直是福音。

1.2 容器化Nginx到底解决了什么实际问题

说个我自己的真实场景。早些年做项目,测试环境、预发布环境、生产环境各装了一遍Nginx,版本还不太一样。测试环境是1.18,生产环境是1.20,配置文件里有些指令在低版本上不认,每次上线前都要手动改来改去。更头疼的是,新同事入职配开发环境,光装Nginx加调配置就得折腾小半天,还经常因为系统差异出各种幺蛾子。

容器化之后,这些问题基本消失了。镜像即环境,我把配置好的Nginx打成一个镜像,推到内部仓库,谁需要就直接拉下来跑。版本统一、配置统一、行为统一。新同事入职,装好容器运行时,一条命令启动,三十秒进入开发状态。这就是容器化最朴素也最实在的价值——消除环境差异带来的不确定性。

另外还有一个隐性好处:资源隔离。Nginx跑在容器里,它占用的CPU、内存、文件描述符都是受控的。以前在宿主机上直接装,万一配置写错导致连接数暴涨,整台机器都可能被拖垮。容器有资源限制机制,你可以在启动时指定最多用多少内存、多少CPU,相当于给服务加了一层保险。

1.3 适合哪些人读这篇内容

这篇内容面向的是完全没有容器使用经验,但有一定Linux基础的读者。你不需要懂Go语言,不需要了解内核命名空间,甚至不需要知道镜像分层是怎么实现的。你只需要会基本的命令行操作,知道什么是端口,能看懂简单的配置文件,那就够了。

如果你已经是老手,日常在用编排工具管理几百个服务,那这篇内容可能对你来说太基础了。但如果你身边有刚入行的同事或者带的学生,可以把这篇丢给他们,省得你从头讲一遍。我写东西的习惯是,假设读者什么都不懂,但假设读者很聪明,所以不会啰嗦一些常识性的东西,但会把每个操作背后的原因讲清楚。

2. 动手之前的准备工作

2.1 容器运行时的安装与验证

不管你用的是哪种Linux发行版,安装容器运行时的步骤都差不多。以常见的包管理方式为例,先更新软件源,然后安装对应的包,最后启动服务并设置开机自启。具体命令因发行版而异,但逻辑是统一的。

安装完成后,第一件事是验证。运行一个最简单的命令,比如查看版本信息。如果能看到客户端和服务端的版本号,说明安装成功。如果报错说连不上守护进程,那多半是服务没启动,或者当前用户没有权限。

注意:默认情况下,只有root用户或者docker组的成员才能直接操作容器运行时。普通用户直接运行命令会报权限错误。解决办法有两个:一是每次都用sudo,二是把当前用户加入docker组。我推荐第二种,但加完组之后需要重新登录才能生效,这个细节很多人会忽略,然后纳闷为什么加了组还是报权限错误。

验证安装是否彻底成功,可以跑一个官方的测试镜像。这个镜像很小,运行后会打印一段信息然后退出。如果能看到那段信息,说明从镜像拉取到容器运行再到输出回显,整条链路都是通的。

2.2 镜像加速与网络环境调优

在国内网络环境下,直接从官方仓库拉取镜像可能会很慢,甚至超时。这不是容器技术本身的问题,而是网络链路的问题。解决办法是配置镜像加速器,把拉取请求指向国内的缓存节点。

配置方法很简单,编辑或创建一个配置文件,填入加速器地址,然后重启服务。不同的容器运行时配置文件路径不一样,但思路是一致的。配置完之后,可以拉取一个小镜像测试一下速度,如果明显变快,说明配置生效了。

这里有个经验:加速器地址不是越多越好。有些人喜欢一次性配五六个,觉得这样容错率高。实际上,客户端会按顺序尝试,如果第一个地址响应慢,反而会拖累整体速度。我一般只配两个,一个主用一个备用,够用了。

另外,如果你在公司内网环境,可能有自己的私有仓库。这种情况下,需要把私有仓库地址也加到配置里,并且可能需要配置证书或者跳过证书验证。跳过证书验证这个操作有安全风险,只建议在完全可控的内网环境中使用。

2.3 目录规划与文件准备

在启动容器之前,我习惯先在宿主机上把目录结构规划好。虽然容器内的文件系统是独立的,但重要的配置、日志、静态资源这些内容,通常需要持久化到宿主机上,否则容器一删,数据就没了。

我的习惯是在用户目录下建一个专门的工作目录,比如叫container-workspace,然后在里面按服务名建子目录。以Nginx为例,我会建三个子目录:conf放配置文件,html放静态页面,logs放日志文件。这样结构清晰,以后要备份或者迁移,直接打包整个目录就行。

提示:目录权限要注意。容器内的进程通常以非root用户运行,如果宿主机上的目录权限设置得太严格,容器内的进程可能没有写入权限,导致日志写不进去或者配置文件读不了。最简单的办法是把目录权限设为755,文件权限设为644,先跑通再根据安全需求调整。

文件准备方面,至少需要一个主配置文件。如果你不想从头写,可以先从容器里把默认配置复制出来,然后基于默认配置修改。这个操作在后面会详细讲。

3. 拉取镜像与启动第一个容器

3.1 镜像拉取与版本选择策略

拉取镜像的命令很直接,指定镜像名称和标签即可。标签不写的话默认是latest,但我强烈不建议在生产环境使用latest。原因很简单:latest是一个浮动标签,今天拉到的和明天拉到的可能不是同一个版本。对于需要稳定复现的环境来说,这是灾难性的。

我的做法是明确指定版本号,比如1.24或者1.24.0。版本号越具体,复现性越强。如果追求极致的稳定性,甚至可以用镜像的摘要值来拉取,那个是内容寻址的,绝对唯一。但摘要值太长不好记,日常用版本号就够了。

拉取完成后,可以用列表命令查看本地已有的镜像。你会看到镜像的名称、标签、镜像ID、创建时间和大小。Nginx的镜像通常在一百多兆,算是很小的了。对比一下,一个带完整运行时的Java应用镜像动辄几百兆甚至上G,Nginx在这方面确实友好。

3.2 启动命令的每个参数都值得说清楚

启动一个Nginx容器,最基础的命令是docker run加上一堆参数。我见过很多人直接复制粘贴网上的命令,跑通了就完事,从来不关心每个参数是什么意思。这样遇到问题就抓瞎。所以这里我把常用参数逐个拆开讲。

-d表示后台运行,不加这个参数容器会占着终端,你关掉终端容器就停了。--name给容器起个名字,方便后续管理。不指定名字的话,运行时会随机分配一个,过两天你根本记不住哪个是哪个。-p做端口映射,把宿主机的端口映射到容器内的端口。Nginx默认监听80,但宿主机上80端口可能被占用了,所以通常会映射到比如8080。

-v做目录挂载,把宿主机的目录挂到容器内。这个参数可以出现多次,分别挂载配置、静态文件、日志等。挂载的时候要注意路径写法,宿主机路径必须是绝对路径,容器内路径看镜像的约定。Nginx镜像的配置目录通常是/etc/nginx,静态文件目录是/usr/share/nginx/html,日志目录是/var/log/nginx。

还有一个参数--restart,指定容器的重启策略。常用的值有no、on-failure、always、unless-stopped。生产环境一般用always或者unless-stopped,这样宿主机重启后容器能自动起来。开发环境用默认的no就行,免得调试的时候容器自己重启把现场破坏了。

把这些参数组合起来,一个完整的启动命令就成型了。执行之后,用查看命令确认容器状态是Up,就说明启动成功了。

3.3 验证服务是否正常响应

容器起来了不代表服务就正常。我见过容器状态是Up但Nginx进程实际上已经挂掉的情况,因为主进程还在,但工作进程崩了。所以必须实际发请求验证。

验证方式有两种:在宿主机上用命令行工具请求映射的端口,或者在浏览器里访问。命令行的好处是可以看到响应头和状态码,信息更全。如果返回200,并且响应体里有Nginx的欢迎页内容,那基本就没问题了。

如果请求不通,先检查容器日志。日志里通常会告诉你哪里出了问题,比如配置文件语法错误、端口被占用、权限不足等。看日志是排查问题的第一手段,比瞎猜高效得多。

注意:如果你在云服务器上部署,除了容器层面的端口映射,还要检查云服务商的安全组规则。很多人容器跑得好好的,日志也正常,但外面就是访问不了,最后发现是安全组没放行端口。这个坑我踩过不止一次。

4. 配置文件挂载与自定义站点

4.1 从默认配置到自定义配置的过渡

容器跑起来之后,默认的欢迎页只能证明服务是活的,离实际可用还差得远。真实项目里,你需要配置反向代理、负载均衡、HTTPS、缓存策略等等。这些都需要改配置文件。

改配置文件有两种方式:一是进入容器内部直接改,二是把配置文件挂载到宿主机上改。我强烈推荐第二种。原因有三:第一,宿主机上的文件可以用你熟悉的编辑器打开,比容器内的vi好用得多;第二,配置文件在宿主机上可以做版本控制,改错了能回滚;第三,容器重建的时候配置不会丢。

具体操作是,先从容器里把默认配置复制到宿主机的挂载目录。复制命令很简单,用docker cp指定容器名和路径就行。复制出来之后,你就可以在宿主机上随意修改了。修改完不需要重启容器,Nginx支持热重载,发一个信号给它就行。

4.2 一个完整的反向代理配置示例

假设你有一个后端服务跑在宿主机的3000端口上,你想通过Nginx的80端口来访问它。配置文件大概长这样:定义一个上游服务器指向宿主机的地址和端口,然后在server块里配置location,把请求转发到上游。

这里有个关键点:容器内的localhost和宿主机的localhost不是一回事。容器内的localhost指向容器自己,要访问宿主机需要用宿主机的内网IP或者特殊的域名。在Linux上,可以用172.17.0.1这个地址,这是默认网桥的网关地址。但更推荐的做法是把后端服务也容器化,然后放在同一个自定义网络里,直接用容器名互相访问。

配置写完之后,用测试命令检查语法。Nginx提供了一个-t参数,专门用来做配置语法检查。这个命令会告诉你配置文件有没有语法错误,但不会检查逻辑错误。语法通过之后,再发重载信号。

4.3 静态资源服务与缓存策略

Nginx最常用的功能之一就是伺服静态文件。配置起来很简单,一个location块加上root指令就行。但要做好,有几个细节值得注意。

首先是try_files指令,它决定了当请求的文件不存在时怎么办。对于单页应用,通常需要把所有找不到的路径都回退到index.html,让前端路由来处理。配置写成try_files $uri $uri/ /index.html就行。

其次是缓存策略。对于带哈希值的静态资源,比如app.a1b2c3.js,可以设置很长的缓存时间,比如一年。因为文件内容变了哈希值就会变,不存在缓存失效的问题。对于index.html这种不带哈希的,要设置不缓存或者很短的缓存时间,否则用户永远看到的是旧版本。

还有gzip压缩,对于文本类资源,开启gzip能显著减少传输体积。但图片、视频这类已经压缩过的资源,再gzip就是浪费CPU。所以通常只对文本类型开启gzip。

5. 数据持久化与日志管理

5.1 为什么容器里的数据不能当真

容器的文件系统是临时的,这是它设计上的特性,不是缺陷。容器删除的时候,里面写入的所有数据都会消失。如果你把日志、上传的文件、数据库数据放在容器内部,那容器一重建,数据就全没了。

我见过一个真实的案例:某团队把用户上传的图片存在容器内的目录里,结果有一次做版本更新,直接删旧容器起新容器,所有用户上传的图片全部丢失。这个教训太惨痛了。凡是需要保留的数据,必须挂载到宿主机或者网络存储上。

Nginx场景下,需要持久化的主要是三类:配置文件、日志文件、静态资源文件。配置文件前面已经讲了挂载方法,日志和静态资源的挂载方式是一样的,都是通过-v参数把宿主机目录映射到容器内对应目录。

5.2 日志切割与轮转的容器化方案

Nginx默认会把访问日志和错误日志写到文件里,文件会一直增长。如果不做切割,几个月后日志文件可能几十个G,不仅占磁盘,排查问题的时候打开都费劲。

传统的做法是在宿主机上配logrotate,定时切割日志文件然后发信号让Nginx重新打开日志文件。容器化之后,这个方案依然可行,但需要把日志目录挂载出来,让logrotate能访问到。

另一种方案是把日志直接输出到标准输出,然后由容器运行时来管理日志。这种方案更“云原生”,适合配合日志收集系统使用。但如果你只是单机部署,没有日志收集系统,那还是挂载出来用logrotate更直观。

提示:不管用哪种方案,都要设置日志保留策略。我一般保留最近7天的日志,更早的自动删除。磁盘空间是有限的,日志无限增长迟早会出问题。

5.3 备份与迁移的实操思路

容器化之后,备份和迁移变得异常简单。因为所有需要保留的东西都在宿主机的挂载目录里,你只需要备份那几个目录就行。配置文件、静态资源、日志(如果需要的话),打包压缩,传到新机器上,然后在新机器上启动一个同样镜像的容器,挂载同样的目录,服务就恢复了。

迁移的时候要注意镜像的可用性。如果新机器拉不到原来的镜像,那就白搭。所以生产环境最好把用到的镜像推送到私有仓库,或者导出成文件随身携带。导出命令很简单,把镜像保存成一个tar文件,到新机器上再加载进去。

我个人的习惯是,每个项目的部署目录里放一个脚本,记录启动容器的完整命令。这样迁移的时候不用回忆参数,直接跑脚本就行。脚本里还可以加上健康检查的逻辑,启动后自动验证服务是否正常。

6. 常见问题与排查技巧实录

6.1 容器启动失败的五种典型原因

容器启动失败是最常见的问题,表现是执行启动命令后容器状态不是Up,或者Up了几秒就变成Exited。根据我的经验,原因基本逃不出下面五种。

第一种是端口冲突。宿主机上要映射的端口已经被别的进程占用了。排查方法是查看端口占用情况,找到占用进程然后决定是换端口还是停掉占用进程。第二种是配置文件语法错误。Nginx对配置语法很严格,少一个分号都会启动失败。排查方法是看容器日志,日志里会明确指出错误在哪一行。第三种是挂载目录权限问题。容器内的进程没有权限读取配置文件或者写入日志目录。排查方法是检查宿主机目录的权限和所有者。第四种是镜像本身的问题,比如拉取不完整或者镜像损坏。排查方法是删除本地镜像重新拉取。第五种是资源限制,比如内存给得太少,Nginx启动过程中被系统杀掉了。排查方法是查看系统日志里有没有OOM记录。

6.2 访问不通的分层排查法

容器状态正常但访问不通,这个问题比启动失败更让人头疼,因为可能性更多。我总结了一个分层排查法,从外到内逐层检查。

最外层是网络连通性。在客户端用ping或者telnet测试宿主机的IP和端口是否可达。如果不可达,问题在防火墙或者安全组。第二层是端口映射。在宿主机上用命令行工具请求映射的端口,如果通,说明容器层面的映射没问题,问题在更外层。如果不通,进入第三层,检查容器内部服务是否在监听。进入容器内部,用命令行工具请求localhost:80,如果通,说明Nginx正常,问题在端口映射配置。如果不通,进入第四层,检查Nginx进程是否在运行,配置文件是否加载正确。

这个分层法能帮你快速定位问题在哪一层,避免像无头苍蝇一样乱试。

6.3 性能问题的常见诱因

Nginx本身性能很好,但在容器里跑,有些默认配置可能成为瓶颈。最常见的是文件描述符限制。容器默认的文件描述符数量可能只有1024,对于高并发场景远远不够。解决办法是在启动容器时通过参数调大限制,或者在宿主机层面调整。

另一个常见问题是DNS解析。容器内的DNS解析走的是宿主机的配置,如果宿主机DNS服务器响应慢,Nginx在代理请求时做域名解析就会卡顿。解决办法是在Nginx配置里直接写IP地址,或者配置本地hosts。

还有一个容易被忽略的是日志写入。如果日志级别设得太低,比如debug,高并发下大量写日志会拖慢整体性能。生产环境一般用warn或者error级别,只在排查问题时临时调低。

问题现象可能原因排查命令解决方向
容器启动后立即退出配置语法错误查看容器日志修正配置文件
端口映射后访问不通防火墙未放行检查防火墙规则放行对应端口
静态文件404挂载路径错误进入容器查看目录修正挂载路径
反向代理502后端地址不可达容器内测试连通性修正上游地址
日志文件不写入目录权限不足检查目录所有者调整权限

6.4 几个让我印象深刻的踩坑经历

说几个我实际踩过的坑,都是文档里不会写的。

第一个坑:有一次我挂载配置文件的时候,用了一个不存在的宿主机路径。容器运行时不会报错,它会自动创建一个空目录挂载进去。结果Nginx启动时发现配置目录是空的,用了内置的默认配置,我改的配置根本没生效。我排查了半天才发现这个问题。教训是:挂载前确认宿主机路径存在,并且里面有正确的文件。

第二个坑:我用-v挂载了一个单个文件,后来在宿主机上用编辑器修改这个文件。有些编辑器保存时是先删除原文件再创建新文件,这导致容器内的挂载点失效,Nginx读到的还是旧内容。教训是:尽量挂载目录而不是单个文件,如果必须挂载文件,修改后用重载命令让Nginx重新读取。

第三个坑:我在一台机器上跑了多个Nginx容器,都映射到宿主机的不同端口。有一次重启宿主机后,容器启动顺序不确定,导致某个容器先占用了端口,另一个容器启动失败。教训是:给容器设置合理的重启策略和依赖关系,或者干脆用编排工具来管理。

7. 从单容器到多容器的演进思路

7.1 什么时候该考虑容器编排

单容器跑Nginx很简单,但真实项目往往不止一个服务。前端静态资源、后端API、数据库、缓存,这些都需要容器化。当容器数量超过三五个,手动管理就开始吃力了。你需要记住每个容器的启动参数、端口映射、挂载目录、依赖关系,脑子不够用。

这时候就该考虑编排工具了。编排工具解决的核心问题是:声明式地描述你想要的最终状态,然后由工具来保证实际状态和期望状态一致。你不需要关心容器怎么启动、怎么调度、怎么恢复,只需要写一个配置文件说明你要什么。

当然,如果你的项目就是一个小网站,一个Nginx容器加一个后端容器,那手动管理完全没问题,没必要为了用编排而用编排。工具是解决问题的,不是制造问题的。

7.2 多容器网络通信的基本原理

多个容器之间要通信,需要放在同一个网络里。默认情况下,容器运行时会创建一个网桥网络,所有未指定网络的容器都连在这个网桥上。但默认网桥有个限制:容器之间只能用IP互相访问,不能用容器名。而IP是动态分配的,容器重启后可能变化。

解决办法是创建自定义网络。在自定义网络里,容器之间可以通过容器名做DNS解析,就像在同一个局域网里用主机名互相访问一样。这个特性非常实用,配置Nginx反向代理的时候,上游地址直接写后端容器的名字就行,不用关心IP是多少。

创建自定义网络的命令很简单,指定一个网段和网络名就行。启动容器的时候用--network参数指定加入哪个网络。同一个网络里的容器天然互通,不同网络的容器默认隔离,需要额外配置才能互通。

7.3 配置分离与环境差异管理

开发、测试、生产环境的配置往往不一样。开发环境可能连的是本地数据库,生产环境连的是远程数据库。如果每次部署都手动改配置,迟早会出错。

好的做法是把配置分离出来,通过环境变量或者外部配置文件来注入。Nginx本身不支持环境变量替换,但可以通过模板工具在启动前生成最终配置。或者更简单的做法是,为每个环境准备一份配置文件,部署的时候挂载对应环境的目录。

我个人的习惯是,在项目目录下建env/dev、env/test、env/prod三个子目录,每个里面放该环境的Nginx配置。部署脚本根据传入的环境参数选择挂载哪个目录。这样既清晰又不容易出错。

8. 一些让部署更顺手的个人习惯

8.1 把启动命令写成脚本

我从来不手敲启动命令,都是写成脚本。脚本里包含完整的启动参数、健康检查逻辑、失败重试机制。这样做的好处是,半年后你还能一键启动,不用回忆当初用了什么参数。脚本还可以纳入版本控制,团队里谁都能用。

脚本的基本结构是:先检查容器是否已存在,存在就先停掉删掉;然后执行启动命令;最后循环检查服务是否正常响应,超时则报错退出。这个模式我用了很多年,稳定可靠。

8.2 给容器起有意义的名字

容器名不要用默认的随机字符串,要起有意义的名字。我的命名规则是项目名-服务名-环境,比如myapp-nginx-dev。这样一眼就能看出这个容器是哪个项目的哪个服务在哪个环境跑的。容器多了之后,这个习惯能省很多时间。

8.3 定期清理不用的镜像和容器

用久了之后,本地会积累大量停止的容器和悬空镜像,占磁盘空间。定期清理是个好习惯。清理命令可以一次性删除所有停止的容器和未被使用的镜像。但执行前要确认没有需要保留的容器处于停止状态,否则误删了就找不回来了。

我一般每个月清理一次,清理前先用列表命令看一眼,确认没有重要的东西再执行。这个习惯让我的开发机始终保持清爽,不会因为磁盘满了导致各种奇怪的问题。

8.4 保持镜像版本与配置的对应关系

最后说一个容易被忽略的点:镜像版本和配置文件是配套的。Nginx 1.24的配置语法和1.18可能有细微差别,如果你升级了镜像但没更新配置,可能会启动失败。所以升级镜像的时候,一定要同步检查配置文件,看看有没有需要调整的地方。

我的做法是在配置文件的注释里写明适用的Nginx版本范围,升级镜像时先看注释,确认配置兼容再操作。这个习惯帮我避免了好几次因为版本不匹配导致的服务中断。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询