简要介绍
本视频将演示如何使用 Docker,将一个 Next.js 应用、一个 PostgreSQL 数据库和一个 Nginx 反向代理服务器,部署到一台每月 4 美元的 Linux 虚拟专用服务器 (VPS) 上。视频将详细讲解 Next.js 的各项功能配置,包括图片优化、缓存与 ISR、流式传输、中间件、服务器组件等,并探讨自主部署与托管服务的优劣,以及其他部署方案。
目录
-
开场介绍
-
演示应用
-
搭建 Linux 服务器
-
VPS 与独立服务器的对比
-
垂直扩展与水平扩展
-
部署到 VPS
-
数据获取
-
图片优化
-
流式传输 (Streaming)
-
PostgreSQL 数据库
-
ISR 与缓存
-
中间件 (Middleware)
-
读取环境变量
-
服务器启动时运行代码
-
调用路由处理器的定时任务 (Cron)
-
请求限流
-
与托管服务的权衡
-
其他方案
开场介绍
让我们来看看如何将 Next.js 应用自主部署到我们自己的服务器上。我们将部署一个 Next.js 应用、一个 PostgreSQL 数据库、一个 Nginx 反向代理以及更多服务,所有这些都将运行在一台每月 4 美元的 Linux VPS (虚拟专用服务器) 上。要完成这个教程,你只需要一个域名、一台 VPS (我会演示如何购买),以及在你的电脑上安装 Docker (在 Mac 上可以通过 Homebrew 安装),基本上就这些了。
我们会通过 SSH 连接到服务器,并在上面部署很多东西,包括 Next.js 应用、反向代理和数据库。我会带你从头到尾完成整个过程,并讨论其中的一些利弊权衡。这台服务器可以容纳非常多的容器。
简单介绍一下流程:首先,我们会搭建服务器;然后,讨论 VPS 和独立服务器的区别和选择;接着,我们会详细分析我创建的部署脚本;我还会介绍我构建的演示应用中的一些功能,这些功能基本涵盖了大家在自主部署 Next.js 时最常问到的配置问题;我们还会探讨自己搭建和管理服务器与使用云服务之间的权衡;最后,会介绍一些其他的自主部署方案。和往常一样,所有代码都是开源的,你可以随时查看并自行部署。
演示应用
在开始之前,我想先展示一下我们即将部署的演示应用。这个应用包含了一些大家要求的功能:在 Next.js 服务器上优化图片、在 App Router 中使用服务器组件和 Suspense 实现流式传输。我们还有一个 PostgreSQL 数据库,里面有一些数据。
我们还设置了缓存和 ISR (增量静态再生)。比如,当我访问这个 ISR 演示页面时,它显示内容已经缓存了 130 多秒。刷新一下,好了,现在我们得到了在后台重新验证过的新内容。我们还有中间件,可以在服务器启动时运行特定代码,还可以加载环境变量等等。接下来我们会逐一深入讲解这些功能和代码。
搭建 Linux 服务器
好的,首先我们需要一台服务器,一台虚拟专用服务器。有很多地方可以购买硬件,在这个教程里,我选择使用 DigitalOcean。我之前其实不知道他们提供每月 4 美元的 VPS。它的硬件配置很低,512MB 内存确实不多,但对于入门来说是个不错的选择。如果你对 DigitalOcean 已经很熟悉了,也可以考虑 Hetzner,它的云服务器性价比非常高,有些配置和价格简直让人难以置信。当然,这其中也有一些权衡,我们稍后会讨论。
在这里,我已经从 DigitalOcean 购买了一个 Droplet (也就是 VPS),并且已经在我的账户里启动并设置好了。购买后,你会得到很多管理功能。首先你会看到一些非常美观的图表,可以用来监控 CPU 使用率、内存占用和磁盘大小。你还可以在这里购买额外的功能或了解更多关于服务器的信息。但现在,我们最需要的是这个 IP 地址。通过这个 IP 地址,我们才能连接到服务器并执行代码。
我们复制这个 IP 地址,然后回到编辑器。我使用的是 Neovim,但你可以用任何你喜欢的编辑器。我的演示应用代码在上方,下方有两个终端窗口。首先,我们来连接服务器。我输入 ssh root@ 然后粘贴服务器的 IP 地址。接着会提示输入密码,我粘贴密码后按回车,就成功连接了。这样,我们就进入了这台 Linux 服务器,它将成为我们搭建应用的场地。
VPS 与独立服务器的对比
在开始之前,我想简单谈谈 VPS 和 独立服务器 之间的一些区别。最大的不同在于,VPS 是多个用户共享同一台物理服务器的硬件资源,而独立服务器则是你独享整台服务器。这也是为什么 VPS 的价格能如此便宜,因为服务商可以通过共享基础设施来降低成本。
当然,VPS 和独立服务器在可扩展性、成本、性能以及对硬件的控制程度上都有各自的优缺点。这并不是说 VPS 不能扩展,我们稍后会讨论一些方案。但总的来说,独立服务器更贵,硬件性能更好,控制权也更大。但这不意味着 VPS 就是个坏选择,只是有时候你可能需要更强大的性能。
另外,我们将在 VPS 中部署一个 PostgreSQL 数据库。值得一提的是,很多关于服务器的讨论也同样适用于数据库。比如,你是将数据库部署在共享服务器上、专用服务器上,还是使用托管服务。这些选择在可靠性、易用性和投入的时间成本上都有很大差异。如果你的数据库负载不大,读写操作不多,那么将它和应用部署在同一台服务器上是完全可行的。有些 VPS 提供商会提供数据库备份和快照功能,而有些则需要你使用专用的硬件或服务来处理。更进一步的托管数据库服务则会提供自动备份、监控工具等一系列高级功能。我之所以提这个,是因为 PostgreSQL 相对来说是比较消耗资源的数据库。如果你追求极致的轻量,可以考虑 SQLite。
垂直扩展与水平扩展
扩展服务器架构主要有两种方式:垂直扩展和水平扩展。垂直扩展是指为你的服务器增加更多资源,比如更多的 CPU、内存或存储空间。现在的硬件性能越来越强,所以这种方式在很多情况下都够用。另一种是水平扩展,即增加更多的应用容器,并在前端使用负载均衡器将流量分发到不同的容器。
垂直扩展最大的优点是操作简单,但缺点是存在单点故障。如果你的容器出了问题,整个服务就都挂了。而水平扩展通过部署多个容器,可以避免单点故障,并且能实现零停机部署。你可以先将所有流量导向一个容器,然后在另一个容器上部署新版本,等新版本部署完成后,再将流量切换过去。我们会进一步讨论这两种方式,但我想先让大家对扩展架构有个基本的了解。
部署到 VPS
好的,回到你的编辑器,确保已经通过 SSH 连接到你的 Linux 服务器。我们将使用 Docker 来搭建应用所需的所有基础设施。你可以跟着 Readme 文件里的步骤操作,我也会在这里现场演示并讲解部署脚本的每一部分。首先,我运行这个 curl 命令,把它复制到终端,这个命令会把部署脚本下载到我们的服务器上。然后,我们修改脚本的权限,让它可以被执行。
现在可以运行部署脚本了,但在运行之前,我们先来看一下这个脚本的内容。这是一个 Bash 脚本,你不一定非要用它,它更多的是一个教学工具,帮助你理解整个工作流程。我们最后也会讨论其他方案。脚本的开头是一些环境变量,比如 PostgreSQL 的用户名、密码和数据库名。我们为你生成了一个随机密码。还有一些是演示应用需要的密钥。你需要修改的是 域名 和 邮箱 这两项。
在服务器上,你可以用 Vim 很方便地修改这个文件。在 SSH 终端里,我输入 vi deploy.sh 打开文件。用 j 和 k 上下移动,用 w 左右移动。找到要修改的值,输入 ci" 就可以修改引号内的内容。我现在把它改成 myfancydomain.com。修改完成后按 ESC (我把它映射到了 Caps Lock) 回到普通模式。同样的方法修改邮箱地址。如果你改错了,可以按 u 撤销。完成后,输入 :wq 保存并退出。
现在我们可以运行部署脚本了。在它运行的时候,我来解释一下后台发生了什么。脚本首先会克隆我们的代码仓库。顶部的变量设置好后,我们会更新所有软件包。你可以把 apt 理解成 Linux 上的 npm,它是一个包管理器。接下来,我们设置了交换空间 (swap space)。这里正好谈谈那个 4 美元的 VPS。它运行得还不错,但最终因为只有 512MB 内存而耗尽了。设置交换空间可以帮助我在构建时避免内存不足的问题。
然后,我们安装 Docker 和 Docker Compose,并设置它们在服务器启动时自动运行。接着是克隆 Git 仓库,如果你看下方的终端,这一步刚刚完成,现在 Docker 的构建已经开始了。我们还会创建一些环境变量并输出到一个 .env 文件中。然后是安装 Nginx 作为反向代理,并使用 certbot 设置 SSL 证书。接下来,我们生成 Nginx 的配置文件,其中包含了请求限流的设置,SSL 证书的路径,以及禁用代理缓冲以支持流式传输。
现在,脚本进入了我们克隆的应用目录,并开始运行 docker-compose。你可以看到它正在构建我们的应用。构建完成后,脚本会确认服务是否正常启动,然后输出“部署完成”的消息以及所有创建的环境变量。
回过头来看,我们先克隆了仓库,然后安装了所有依赖,现在 Docker 镜像正在构建。我们来谈谈这个 Dockerfile。这是一个多阶段构建 (multi-stage Dockerfile)。第一阶段安装依赖,多阶段的好处是,如果第一阶段没有变化,它会被缓存,下次构建时就不用重新运行。第二阶段是构建应用。我们复制 node_modules 并运行构建命令。最后是生产环境阶段,我们复制构建好的静态资源和独立的服务文件,然后暴露端口并启动服务器。
回到左下角的终端,可以看到构建过程快完成了。我想特别提一下 outputStandalone 这个配置。在 next.config.js 中,我强烈建议你开启这个选项。它可以将你的 Docker 镜像大小减少约 80%,因为它只包含运行所必需的文件。这也是为什么在 Dockerfile 中,我们的文件路径都来自 .next/standalone 目录。在 package.json 里,我的启动命令也是指向这个独立服务器。
看,终端显示一切都完成了,数据库、Next.js 应用和定时任务都成功启动。我们还看到生成了包含所有所需值的 .env 文件。
你可能想知道我是如何设置这三个容器的。答案是使用 docker-compose.yml 文件。这个文件列出了我应用中的所有服务。首先是 Next.js 应用,它依赖于数据库。你会注意到,Web 和数据库容器都使用了同一个网络,这样它们才能互相通信。然后是 PostgreSQL 数据库,它也有一些环境变量和端口设置。最后是一个定时任务 (cron)。它使用的镜像是预装了 curl 的,所以我们不需要手动安装。它会利用系统自带的 cron.d 功能,每 10 分钟调用一次 web/db-clear 这个接口来清空数据库。
回到服务器,一切看起来都很成功。我们进入应用目录,然后用 docker ps 查看正在运行的进程,可以看到三个容器都在几分钟前启动了。我还想确认一下 .env 文件里的值是否正确。用 cat .env 查看,可以看到 PostgreSQL 的用户、密码、数据库名以及完整的数据库连接 URL 都已经生成好了。
现在回到浏览器,访问 next-self-host.dev,可以看到网站已经正常运行了。每次刷新页面都会显示一个新的宝可梦,说明一切工作正常。我们不仅运行了 Next.js 应用和数据库,还配置了反向代理。在此之前,我已经将我的域名 DNS A 记录指向了服务器的 IP 地址,你也需要做这一步。
这就是部署脚本的全部内容。因为我们使用了 Docker,整个部署过程是高度可移植的。你不一定非要用 VPS,也可以用自己的独立服务器,或者使用托管的容器服务,比如 Google Cloud Run。Docker 是一个非常值得学习的技能,它能让你更好地控制你的基础设施,并在不同平台间轻松迁移。
数据获取
好的,我们已经搭建了 Linux 服务器,讨论了 VPS 和独立服务器的区别,并成功运行了部署脚本。现在,我们来详细讲解演示应用中的各项功能,以及在自主部署 Next.js 时如何配置它们。首先是数据获取。这个页面能够获取一个随机的宝可梦,它是服务器端渲染的,所以每次请求都会从 API 获取一个新的宝可梦。这是通过服务器组件动态实现的,这里不需要任何特殊的配置,只是作为一个演示。
图片优化
第二个功能是图片优化。默认情况下,Next.js 可以在其服务器上优化图片。如果我打开网络面板并刷新页面,你会看到一个向 /next/image 发出的请求。这个请求会回到它自己的服务器,并传入图片的源 URL。在这个例子中,是一张来自 Unsplash 的远程图片。Next.js 会获取这张原始的 JPEG 或 PNG 图片,将其优化成更高效的格式,比如 WebP 或 AVIF,并添加合适的缓存头,从而加快页面加载速度。
如果你不想使用这个功能,也有其他选择。值得一提的是,在 Next.js 15 中,我们不再需要你手动在服务器上安装 Sharp 来进行图片优化。在之前的版本中,我们推荐安装 Sharp,因为它在内存效率上比基于 WebAssembly 的版本更好。现在你少了一件需要操心的事。但如果你不想使用 Next.js 内置的图片优化功能,该怎么办呢?在我的首页代码里,我使用了来自 Unsplash 的图片,并且没有做任何额外配置,用的是默认的图片加载方式。在 next.config.js 的 images 配置下,我使用了 remotePatterns 来告诉 Next.js 允许优化哪些来源的图片。你应该让这个规则尽可能具体,以防服务器被滥用。
如果你不想用默认的图片优化,可以提供一个自定义加载器 (custom loader)。你可以指定一个加载器文件,比如 imageLoader.ts。在这个文件里,你可以自定义如何构建图片 URL。你可以拿到图片的源地址、宽度和质量,然后返回最终传递给图片组件的 URL。你可以用这个功能来集成第三方的图片服务,比如 Cloudinary,或者调用你自己部署在其他容器或服务器上的图片优化服务。
流式传输 (Streaming)
接下来我们看流式服务器组件。在这个演示中,我们有一个异步组件,并用 Suspense 包裹它,让内容每隔一秒加载一部分。如果你还记得,在我们的部署脚本中,我们禁用了 Nginx 的代理缓冲,这样我们就可以流式传输响应,将内容分块发送到浏览器。在代码中,我们用多个 Suspense 边界包裹异步组件,每个组件都会去获取数据并显示。为了模拟数据获取,我只是用了一个一秒后 resolve 的 Promise。
需要注意的是,在 next.config.js 文件中,我们禁用了 Next.js 的压缩功能,让 Nginx 来负责压缩,这样可以防止流式响应被缓冲。
PostgreSQL 数据库
现在我们来看看如何连接我们设置的 PostgreSQL 数据库。在这个演示中,我使用 Drizzle ORM 来连接数据库。点击这个页面,可以看到一些已经保存的数据。我可以添加新内容,刷新页面,也可以删除条目,都是基本的增删改查操作。在代码中,我们获取所有的待办事项并展示出来,表单则使用服务器操作 (Server Action) 来添加新的待办事项。
为了验证数据库是否正常工作,我们可以进入 Docker 容器。在 Readme 文件里,我提供了一些有用的命令。比如这个 docker exec 命令,可以让我们进入数据库容器并使用 psql 连接到数据库。我执行这个命令,连接成功后,我可以看到已有的用户和表,以及待办事项表中的数据。这说明数据库工作正常。
ISR 与缓存
接下来是 ISR (增量静态再生) 和 Next.js 中的缓存。这部分可能是大家问题最多的地方。我想先解释一下它的默认工作方式,然后再讲如何自定义。默认情况下,Next.js 的 ISR 使用的是最近最少使用 (LRU) 缓存,数据被缓存在内存中,你不需要任何配置就能直接使用。
举个例子,这个页面的内容已经缓存了 1200 秒,但它的重新验证时间是 10 秒。所以当我访问这个页面时,它已经过期了。当我刷新页面时,会得到新的数据。当然,我也可以手动点击按钮来触发重新验证。在我的 ISR 页面代码中,我获取了一个宝可梦的数据和生成时间,并设置了一个定时器来显示新鲜度。我可以通过在 fetch 请求中设置 revalidate 时间,或者在页面级别导出 export const revalidate = 10 来控制缓存时间。
默认的内存缓存只在单个容器中有效。如果你想自定义缓存行为,可以在 next.config.js 中提供一个自定义缓存处理器 (cache handler)。你可以指定一个处理器文件,并禁用内存缓存。这样你就可以完全控制缓存的存储方式,比如存到 Redis 或其他持久化存储中。有一个社区开发的包可以很方便地帮你用 Redis 实现这个功能。在我这个应用里,我实现了一个非常简单的版本,将缓存保存到一个 .cache 目录中,并加入了一些日志输出,方便观察。这个处理器主要包含 get、set 和 revalidateTag 三个方法。值得注意的是,revalidatePath 实际上也是通过 revalidateTag 实现的,因为在 Next.js 的缓存系统中,路径也被视为一种标签。
为了演示,我启用了自定义缓存处理器并禁用了内存缓存,然后重新构建并启动了应用。在本地 localhost:3000 上访问演示页面,可以看到内容在 10 秒内是缓存的,超过 10 秒后刷新就会获取新数据。通过后台的日志,我们可以清楚地看到缓存的未命中、设置和命中过程,以及过期后缓存被清除并重新设置的过程。你不是必须这么做,我只是想让大家更清楚地了解缓存的内部工作机制。
中间件 (Middleware)
接下来是中间件 (Middleware)。我们的应用中有一个 /protected 路由,它受一个 Cookie 保护。我的中间件逻辑很简单:如果请求的路径是 /protected,并且存在一个值为 1 的 protected Cookie,就重定向到首页。现在我直接访问这个受保护的页面,会被重定向回来。但如果我在浏览器中设置 protected Cookie 的值为 1,再访问时就能看到受保护页面的内容了。
读取环境变量
在这个受保护的页面上,我还展示了如何读取环境变量。在这个客户端组件中,我读取并显示了一个环境变量。你会注意到这个变量名以 NEXT_PUBLIC_ 开头,这表示我们有意将它暴露给客户端浏览器,并打包到最终的构建文件中。如果你查看页面源代码,可以看到这个值是直接包含在 HTML 里的。
与此相反的是只在服务器端使用的环境变量。比如,我在一个服务器组件中读取了 MY_SECRET 这个环境变量。因为它运行在服务器端,而不是客户端组件,所以我们可以在不将它打包到浏览器的情况下动态读取和使用这些值。
服务器启动时运行代码
还有一个很常见的需求,就是在服务器启动时运行一些代码。Next.js 15 中有一个即将稳定的功能叫做 instrumentation。它主要用于可观测性工具,但在自主部署时也非常有用。你可以在服务器启动时运行代码,比如注册或调用一些外部 API。在这个演示中,我设置了一个全局变量,并从 HashiCorp Vault 获取了一个 API 密钥,然后将其保存在这个全局变量中。这样,在我的页面里就可以读取和使用这个值了,比如把它加到请求头里。
调用路由处理器的定时任务 (Cron)
最后两项演示,一个是定时任务 (cron),另一个是请求限流。对于定时任务,如果你还记得,我们的 PostgreSQL 数据库每 10 分钟会清空一次数据。现在数据应该已经被清除了。我访问页面,可以看到里面是空的,但我仍然可以添加新条目。这是通过一个路由处理器实现的,我们的定时任务服务会调用它,执行删除操作并重新验证路径。
请求限流
最后是请求限流。在部署脚本中,我们为 Nginx 配置了限流规则。我用一个负载测试工具向我的网站发送大量请求,在 5 秒内发起了 760 次请求,其中 691 次被阻止了。这说明限流规则生效了。当然,你可以用更复杂的方式来防范恶意访问,但这是一个使用 Nginx 实现基础限流的简单例子。
与托管服务的权衡
好的,我们已经讲完了所有演示功能。现在我想谈谈这种自主部署方案与使用托管服务之间的一些权衡,并介绍一些其他选择。为了更形象地说明,我们用宝可梦的进化来打个比方。
我们最初的 VPS 就像是小火龙,一个 Docker 容器里包含了 Next.js 的所有功能:渲染、图片优化、缓存等等。还有一个数据库容器和一个定时任务容器。当你的应用流量增长时,你可能需要将这些功能拆分。你不希望渲染、图片优化和缓存操作互相竞争服务器资源。下一步的进化可能是垂直扩展硬件,同时水平扩展 Docker 容器,就像火恐龙一样。你可以有一个专门负责渲染的容器,把图片优化拆分成一个独立的服务 (比如用 IPX),把 ISR 缓存放到一个独立的 Redis 实例中。
如果你需要继续扩展,就可能进化成喷火龙。你可能会用一台独立服务器,运行 Kubernetes 来管理多个 Next.js 容器,实现水平扩展。比如,用五个容器负责渲染,一个负责图片优化,几个负责数据库,还有一个 Redis。Kubernetes 可以帮你管理这些服务,并通过负载均衡器分发流量,实现零停机部署。
当然,这只是扩展的一种方式。我想说的是,小火龙的版本并没有错。如果你的目标是省钱和高效运行,这是一个非常好的选择。自主部署只是意味着你需要自己花时间来配置和维护基础设施。也许你很享受这个过程,那也完全没问题。
很多人问我,既然我在 Vercel 工作,为什么还要教大家如何自主部署。我认为 Next.js 社区的开发者应该有多种选择,应该了解基础设施是如何工作的,并知道这个开源框架是可移植的。同时,这也能帮助大家更好地理解 Vercel 在后台为我们做了什么。我们目前讨论的都是单容器、单区域的部署,这对于很多应用来说已经足够了。
而 Vercel 所做的,即使是在免费套餐上,也是帮助你的应用在全球范围内实现快速访问。用户的请求首先会经过我们的全球网络防火墙,然后进入离用户最近的区域,比如美国东部或欧洲。在那里,我们会为你运行一个托管的 Kubernetes 实例。所以,Vercel 的 DevOps 团队在后台为你做的事情,就像是管理着一只超级喷火龙。我们处理 HTTPS、TLS、DDoS 防护,你可以添加自定义防火墙规则。我们有自己的路由系统,可以在边缘运行中间件。我们将页面响应缓存、ISR、图片优化等都拆分成了独立的服务,并可以根据流量自动进行垂直和水平扩展。
Vercel 的目标是让你专注于开发应用,而无需关心底层的基础设施。但有时,你可能并不需要超级喷火龙,一只小火龙就够了。我希望通过这个视频,大家能更好地理解这些基础设施,知道如何自己搭建,并最终找到最适合自己的方案。
回到托管服务和自主部署的区别,我认为 Vercel 最大的优势在于框架定义的基础设施 (framework-defined infrastructure)。当你向 Vercel 部署一个 Next.js 应用时,你不需要写任何部署脚本或 IaC (基础设施即代码) 配置,你只需要写你的 Next.js 代码。Vercel 会自动处理好 ISR 缓存、页面渲染、函数部署等所有事情。另一个优势是成本控制。我们最近推出了开销管理 (spend management) 功能,你可以为你的账户设置软上限和硬上限,避免超出预算。这和 VPS 因为资源耗尽而宕机有些类似,但你能更好地控制开销。
其他方案
另外,如果你不想运行服务器,也可以将你的 Next.js 应用部署为一套静态资源,就像一个单页应用一样。这比 VPS 更具可移植性,你可以把它部署在任何地方。但这样做会限制你使用某些功能,比如服务器端渲染、ISR 和图片优化。
社区里还有一些其他的适配器,可以将 Next.js 的输出部署到其他云平台或 AWS 的无服务器架构上。还有一些工具可以提供类似 Vercel 的体验,比如 Coolify,它支持 Git 集成。另一个我很喜欢的工具是 Kamal,它是由 37 Signals 团队开发的,可以帮你实现零停机部署到你自己的 VPS 上。
最后,感谢 Shayen 提出的关于将图片优化拆分成独立服务的建议,也感谢 IPX 团队让这个实现变得非常简单。还要感谢 Flightcontrol 的 Brandon,他写了一篇关于自主部署 Next.js 的好文章,并给了我们很多改进建议。我们已经根据他的反馈,在图片优化、缓存控制等方面做出了改进。
我想我讲完了所有内容。如果你看到了这里,非常感谢。希望这个视频能全面解答你关于自主部署 Next.js 的所有问题。如果你想看更多关于 Next.js 的内容,欢迎在评论区留言。下次再见!
