没事造轮子没事造轮子

没事造轮子

Nextjs 网站部署

  • W_Z_C
  • 阅读约 14 分钟
Nextjs 网站部署

本文主要讲解 Nextjs 网站部署的流程以及在此期间遇见的问题和解决方案。Nextjs 部署的方法很多,最好用的方式是直接将程序部署到 vercel,毕竟是官方出品,部署的步骤非常简单,可以直接拉取 github 代码,整个部署步骤甚至无需做任何修改,并且 vercel 还提供免费套餐可供使用,对于一般的个人博客来说足够使用了。

遗憾的是我们的程序需要持久化,如果你想部署到 vercel 需要做一些额外的工作,最主要的是你需要找一个后端数据库来存储博客中的元数据,vercel 官方推荐的方法是使用更高级的抽象 CMS 系统或者使用第三方平台的数据库系统,AWS、Google Cloud 都有类似的服务。

我原来的博客是部署在腾讯云的,配上它们的 CDN 国内访问非常快,但是如果国外访问,就会经常出现图片加载不出来的现象。这一次我准备使用 remix 文档中推荐的另一个服务商 fly.io。fly.io 同样提供提供免费的基础套餐,并且它们会提供 3GB 大小的免费硬盘给你使用,非常适合我这种穷逼。

我想把部署区域设置在香港看看效果。唯一蛋疼的是官方文档没有对 Nextjs 程序的支持,所以你需要手动使用 Docker 的方式创建镜象,并编写自己的部署脚本,相比于 vercel 麻烦的不是一点半点。接下来是我部署网站的所有流程,我尽量还原所有部署的坑,但是有时候顺手解决的 bug,可能就忘记记录了…… 😂

1. 部署设置

准备部署程序之前,首先需要做一些额外的设置,毕竟对于部署的程序来说,安全必须要有所保证。Nextjs 官方推荐你在最终的程序上增加一些 安全头设置。这里强烈推荐大家阅读一下官方文档,找到自己所需要的设置添加到 next.config.js 中,下面是我目前的设置:

module.exports = [
    {
        key: 'X-DNS-Prefetch-Control',
        value: 'on',
    },
    {
        key: 'Strict-Transport-Security',
        value: 'max-age=63072000; includeSubDomains; preload',
    },
    {
        key: 'Server',
        value: 'Apache', // phony server value
    },
    {
        key: 'X-Content-Type-Options',
        value: 'nosniff',
    },
    {
        key: 'X-Frame-Options',
        value: 'sameorigin',
    },
    {
        key: 'X-XSS-Protection',
        value: '1; mode=block',
    },
    {
        key: 'Referrer-Policy',
        value: 'same-origin',
    },
    {
        key: 'Permissions-Policy',
        value: 'geolocation=*', // allow specified policies here
    },
];

2. 程序自检

配置好程序选项后,需要运行程序看看是否存在错误,在开发模式下,先把测试脚本跑一遍,像咱们这种啥都没有的那就启动程序多点点页面,观察每个页面是否都符合自己的预期,最后需要运行 npm run build 确保可以编译通过。

如果你按照教程走下来,大概率会碰见很多警告以及几类错误,按照控制台反馈的信息,尽量修改。我最开始编译的原始错误没有记录错过了,但是基本上都是一些常见的问题,类似下面这种:

./src/components/Footer.js
10:21  Warning: Do not use . Use Image from 'next/image' instead. See: https://nextjs.org/docs/messages/no-img-element  @next/next/no-img-element

./src/components/Header.js
108:8  Warning: Do not use . Use Image from 'next/image' instead. See: https://nextjs.org/docs/messages/no-img-element  @next/next/no-img-element
187:25  Warning: Do not use . Use Image from 'next/image' instead. See: https://nextjs.org/docs/messages/no-img-element  @next/next/no-img-element

./src/components/Loading.js
18:21  Warning: Do not use . Use Image from 'next/image' instead. See: https://nextjs.org/docs/messages/no-img-element  @next/next/no-img-element

./src/components/PageRating.js
48:8  Warning: React Hook useEffect has missing dependencies: 'dynamicData' and 'locale'. Either include them or remove the dependency array.  react-hooks/exhaustive-deps

./src/components/RecentPosts.js
46:26  Error: Component definition is missing display name  react/display-name
60:8  Warning: React Hook useEffect has a missing dependency: 'recentPosts'. Either include it or remove the dependency array. You can also do a functional update 'setRecentPosts(r => ...)' if you only need 'recentPosts' in the 'setRecentPosts' call.  react-hooks/exhaustive-deps

对于警告请尽量修正,这样做可以避免很多潜在的错误,除非你特意为之。例如我没想使用 next/image 组件,因为该组件太重,最后生成的代码在 img 标签外面竟然包裹了两层 span 标签,所以转而使用了一个第三方惰性加载的库(哦,最终我还是妥协切换回 next/image 组件了,因为它出了个实验性的功能 raw)。

编译过程中的警告并不会影响你最终的编译结果,但是如果你遇见错误则必须进行修改,否则无法生成最终程序。例如上面的消息告诉我没有对函数组件名命,其实如果你改正 ESLine 的选项一样可以编译通过,但是我还是妥协去改了代码,毕竟改起来很容易。

3. 页面优化

解决了程序的编译问题后,你可能会在控制台看到下面的提示:

info  - Checking validity of types  
info  - Creating an optimized production build  
info  - Compiled successfully
info  - Collecting page data  
[=== ] info  - Generating static pages (123/319)Unrecognised language: prisma
Unrecognised language: prisma
Unrecognised language: prisma
info  - Generating static pages (319/319)
info  - Finalizing page optimization  

Page                                                          Size     First Load JS
┌   /_app                                                     0 B            84.5 kB
├ ● /[[...slug]] (91035 ms)                                   216 kB          333 kB
├   ├ /zh-Hans (2942 ms)
├   ├ /zh-Hant (2941 ms)
├   ├ /zh-Hans/cha-zhao-comzu-jian-jie-kou (2468 ms)
├   ├ /zh-Hant/cha-zhao-comzu-jian-jie-kou (2449 ms)
├   ├ /zh-Hans/chang-jian-de-cuo-wu-chu-li-fang-fa (2287 ms)
├   ├ /en (2265 ms)
├   ├ /zh-Hans/nextjs-i18n (889 ms)
├   └ [+299 more paths]
├ ○ /404                                                      1 kB            112 kB
├ ● /about                                                    7.2 kB          124 kB
├ λ /api/categoryposts                                        0 B            84.5 kB
├ λ /api/recentposts                                          0 B            84.5 kB
├ λ /api/score                                                0 B            84.5 kB
├ ● /download                                                 3.67 kB         115 kB
└ λ /sitemap.xml                                              258 B          84.7 kB
+ First Load JS shared by all                                 84.5 kB
  ├ chunks/framework-a87821de553db91d.js                      45 kB
  ├ chunks/main-249b2ef55eeabbab.js                           29.5 kB
  ├ chunks/pages/_app-b32ea690ed1d29c9.js                     8.04 kB
  ├ chunks/webpack-9fd989a159f5d01f.js                        1.9 kB
  └ css/dde9638aff957461.css                                  8.06 kB

λ  (Server)  server-side renders at runtime (uses getInitialProps or getServerSideProps)
○  (Static)  automatically rendered as static HTML (uses no initial props)
●  (SSG)     automatically generated as static HTML + JSON (uses getStaticProps)

你可以明显看到编译结果中的 First Load JS 列可能会显示红色,控制台可能会打印一个链接让你去查看帮助文档:

See more info here: https://nextjs.org/docs/messages/large-page-data`

文档中说你需要减少页面数据,并保持在 128KB 以内,显然目前的页面是超过了这个范围才会显示红色。你可以完全不理会这个警告,但是因为页面最终显示的时候需要进行水合,因此可能会导致页面长时间无法显示,进而降低用户体验。

目前网站几乎所有的页面都是通过 [[...slug]].js 文件进行处理,这就导致了所有的模板全部编译进了该页面,最终造成页面大小超出阈值。为了缓解这个问题,可以使用 Nextjs 提供的 动态加载功能

打开 templates/layouts 目录下的 index.js 文件:

index.js
import { HomeLayout } from "./home"
import { CategoryLayout } from "./category"
import { PostLayout } from "./post"
import { ProjectLayout } from "./project"
import { TutorialLayout } from "./tutorial"
import Custom404 from "../../pages/404"

const Layouts = {
    home: HomeLayout,
    post: PostLayout,
    category: CategoryLayout,
    project: ProjectLayout,
    tutorial: TutorialLayout
}

代码中可以看到文件导入了很多模板,这些模板都会在编译的过程中加入到最终的 [[...slug]] 页面中,这显然是没有必要的,因为不同页面完全可以对应不同模板,它们之间应该是互斥关系,因此可以将这些模板的加载过程改为动态导入、按需加载,这样自然可以减少最终的代码数量。

修改方式很简单,就是将原来的静态导入代码移除,改为动态导入即可:

index.js
import { HomeLayout } from "./home"import { CategoryLayout } from "./category"import { PostLayout } from "./post"import { ProjectLayout } from "./project"import { TutorialLayout } from "./tutorial"
import dynamic from "next/dynamic"import Custom404 from "../../pages/404"
const Layouts = {    home: HomeLayout,    post: PostLayout,    category: CategoryLayout,    project: ProjectLayout,    tutorial: TutorialLayout}
const Layouts = {    home: dynamic(() => import('./home').then(mod => mod.HomeLayout)),    post: dynamic(() => import('./post').then(mod => mod.PostLayout)),    category: dynamic(() => import('./category').then(mod => mod.CategoryLayout)),    project: dynamic(() => import('./project').then(mod => mod.ProjectLayout)),    tutorial: dynamic(() => import('./tutorial').then(mod => mod.TutorialLayout)),}

运行 npm run build 编译程序,效果立竿见影!

Page                                                          Size     First Load JS
┌   /_app                                                     0 B            84.7 kB
├ ● /[[...slug]] (48284 ms)                                   4.2 kB          116 kB
├   ├ /zh-Hans (2588 ms)
├   ├ /zh-Hant (2582 ms)
├   ├ /zh-Hant/cha-zhao-comzu-jian-jie-kou (2102 ms)
├   ├ /zh-Hans/chang-jian-de-cuo-wu-chu-li-fang-fa (2029 ms)
├   ├ /zh-Hant/chang-jian-de-cuo-wu-chu-li-fang-fa (1993 ms)
├   ├ /en (1988 ms)
├   ├ /zh-Hans/express (588 ms)
├   └ [+299 more paths]
├ ○ /404                                                      1 kB            113 kB
├ ● /about                                                    12.9 kB         124 kB
├ λ /api/categoryposts                                        0 B            84.7 kB
├ λ /api/recentposts                                          0 B            84.7 kB
├ λ /api/score                                                0 B            84.7 kB
├ ● /download                                                 3.67 kB         115 kB
└ λ /sitemap.xml                                              258 B            85 kB
+ First Load JS shared by all                                 84.7 kB
  ├ chunks/framework-a87821de553db91d.js                      45 kB
  ├ chunks/main-249b2ef55eeabbab.js                           29.5 kB
  ├ chunks/pages/_app-b32ea690ed1d29c9.js                     8.04 kB
  ├ chunks/webpack-9c127197cec21c4c.js                        2.14 kB
  └ css/dde9638aff957461.css                                  8.06 kB

λ  (Server)  server-side renders at runtime (uses getInitialProps or getServerSideProps)
○  (Static)  automatically rendered as static HTML (uses no initial props)
●  (SSG)     automatically generated as static HTML + JSON (uses getStaticProps)

4. 数据库设置

prisma 对于数据库部署有专门的流程,看过文档后感觉有的也是一知半解,下面按照我个人的理解和真实部署的体会简单的介绍一下。

部署数据库的时候可能会遇见两种情况,一种是全新的环境,这种最简单,prisma 推荐你先从开发环境导出部署需要的 SQL 文件,然后在运行程序之前执行它。另一种是数据库已经存在了,新版的程序会面临两个问题,一个是新版程序是否改动了数据库的结构,另一个是数据库中的老数据的迁移问题。

如果只是改动了数据库的结构,可以尝试按照 prisma 的数据库迁移流程,生成增量 SQL 文件,然后再执行该文件改变数据库的结构。不过 prisma 并没有提到对历史数据的迁移问题(可能我没找到),因为数据迁移可能会存在各种各样的麻烦,可能单纯的 prisma 标准流程不太够用,prisma 负责更多是对数据库结构的变化记录,有点类似 git,它会专门用一张表记录变化的历史,并对每次变化都生成对应的变更语句,具体的情况推荐大家去看官方文档。

目前我面临的问题是有老的数据库,并且新版的数据库结构存在变更。接下来是我对数据库迁移做的工作,仅供参考:

首先,按照 prisma 的文档指导,生成迁移数据:

npx prisma migrate dev --name init

该命令会在 prisma 目录生成迁移的 SQL 语句:

prisma                     
  ├─ migrations              
  │  ├─ 20220605003526_init  
  │  │  └─ migration.sql     
  │  └─ migration_lock.toml  
  └─ schema.prisma           

并且在数据库中会创建一个名称为 _prisma_migrations 的表,内部记录迁移的信息。有了这些数据,我们可以在生产环境下运行 npx prisma generate && npx prisma migrate deploy 来部署数据库,这些工作会放到编写 Dockerfile 的章节中。

执行上面的命令之后只是会生成新的数据库结构,但是数据迁移却无法搞定,我自己是使用 fly.io 提供的登陆命令,直接登录到后台,然后将老的数据库转为 SQL 文件导入的。

5. Dockerfile

现代程序部署一般都使用 Docker 的形式,不过前提要编写 Dockerfile 文件。Dockerfile 文件主要负责创建镜象,并安装程序在运行过程中可能涉及到的依赖,例如 nodejs、g++、python、sharp 等等,除此之外可能还要规划加入到镜象的文件结构以及运行环境的相关配置工作。

下面是我目前使用的配置:

# Install dependencies only when needed
FROM node:alpine AS deps

# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat

# better-sqlite need python, Install python/pip
RUN apk add --no-cache g++ make python3 py3-pip

WORKDIR /app
COPY package.json ./
RUN yarn add sharp && yarn install --frozen-lockfile

# Rebuild the source code only when needed
FROM node:alpine AS builder

WORKDIR /app
COPY . .
COPY --from=deps /app/node_modules ./node_modules

COPY ./buildenv ./.env
RUN rm -rf /app/dbs/*
RUN yarn prisma generate && yarn prisma migrate deploy

RUN yarn build && yarn install --production --ignore-scripts --prefer-offline


# Production image, copy all the files and run next
FROM node:alpine AS runner

WORKDIR /app

## 编译的时候就需要设置
ENV NODE_ENV production
ENV TZ Asia/Shanghai
ENV NEXT_SHARP_PATH /app/node_modules/sharp

RUN apk update && apk add sqlite && apk add --no-cache tzdata
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001

COPY --from=builder /app/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/docker-entrypoint.sh

COPY --from=builder /app/init_tables.sh /usr/local/bin/init_tables.sh
RUN chmod +x /usr/local/bin/init_tables.sh

# You only need to copy next.config.js if you are NOT using the default configuration
COPY --from=builder /app/.env ./.env
COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json

COPY --from=builder /app/src/backend ./src/backend
COPY --from=builder /app/i18n/locales ./i18n/locales

COPY --from=builder /app/next.config.js ./next.config.js
COPY --from=builder /app/next-seo.config.js ./next-seo.config.js
COPY --from=builder /app/safe-headers.js ./safe-headers.js

# 数据拷贝过来
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/dbs ./_dbs

# USER nextjs

EXPOSE 8080

ENV PORT 8080

# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry.
ENV NEXT_TELEMETRY_DISABLED 1

ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]

CMD ["node_modules/.bin/next", "start"]

其中 docker-entrypoint.sh 文件主要用于将默认生成的数据库拷贝到生产数据库的位置,如果存在则跳过,再有就是将最新的文章数据导入到数据库中。init_tables.sh 文件调用 sqlite 命令行导出最新的文章数据,然后将它们转移到生产数据库中。

这些内容其实没啥好介绍的,都是 docker 相关的基础知识,不过我还是简单的梳理一下可能会遇见的几个坑。

5.1 no such file or directory

standard_init_linux.go:190: exec user process caused "no such file or directory"

你在运行镜象的时候遇见上述错误,请检查脚本文件的换行符是 CRLF 还是 LF,请确保 bash 脚本文件的换行符是 LF,除此之外就是检测用户是否具有该脚本文件的执行权限。

5.2 国内镜象编译缓慢

在本地编译镜象的时候经常会出现网络问题,解决方案只能是更换国内源,或者干脆在国外的环境编译,fly.io 平台支持这一点,具体情况下个章节有介绍,这个章节讲一下如何换源,国内使用 Linux 的小伙伴应该很熟练才对。

首先是 Linux 操作系统的源:

RUN sed -i 's/https\:\/\/dl-cdn.alpinelinux.org/https\:\/\/mirrors.ustc.edu.cn/g' /etc/apk/repositories

接着是 npm 源:

RUN yarn config set registry "http://registry.npm.taobao.org/"

最后还有一个 sharp 程序,该程序是 Nextjs 的依赖之一,主要用于处理图像:

RUN yarn config set sharp_binary_host "https://npmmirror.com/mirrors/sharp"
RUN yarn config set sharp_libvips_binary_host "https://npmmirror.com/mirrors/sharp-libvips"

6. fly.io

如果你不使用 fly.io,那么这一步就是编写 docker-compose.yml,但是 fly.io 不需要该文件,只需要 Dockerfile 配合它们自定义的 fly.toml 文件。fly.toml 相当于 docker-compose.yml,不过它自身也可以添加一些镜像编译期间的配置。

注册 fly.io 可以免费使用一定的资源,但真实的体验如何还需要验证,这里先简单介绍一下部署的基本流程。

首先需要创建账号,可以 fly.io 登陆官方网站注册即可。注册成功以后,需要 安装 flyctl 程序,以后程序的部署、状态查看、主机操作都需要用到它。

下面是 Windows 中的安装方法:

PS C:\Users\W_Z_C> iwr https://fly.io/install.ps1 -useb | iex

flyctl was installed successfully to C:\Users\W_Z_C\.fly\bin\flyctl.exe
Run 'flyctl --help' to get started

可以使用 flyctl -h 命令验证是否安装成功。要想操作线上主机,首先需要登陆账户:

flyctl auth login

执行该命令后,浏览器会弹出一个页面,然你进行授权:

fly.io 登陆授权

点击授权,需要你输入信用卡:

fly.io 信用卡输入

成功后,弹出欢迎界面和 帮助文档

欢迎界面

接下来可以参考官方网站中从 Dockerfile 部署程序的方式来执行。

fly launch

该命令会检测 Dockerfile 文件并编译它们,如果本地没有安装 docker,那么它会在远程开启一个免费的主机来编译镜象,编译成功后直接部署。不过第一次运行该命令,不要直接部署,输入 N,来修改本地生成 fly.toml 文件。

Creating app in C:\projects\meishizaolunzi
Scanning source code
Detected a Dockerfile app
? App Name (leave blank to use an auto-generated name): meishizaolunzi
Automatically selected personal organization: W_Z_C
? Select region: hkg (Hong Kong, Hong Kong)
Created app meishizaolunzi in organization personal
Wrote config file fly.toml
? Would you like to setup a Postgresql database now? No
? Would you like to deploy now? No
Your app is ready. Deploy with `flyctl deploy`

fly.toml 的配置过程可以 参考官网,我主要额外添加了静态目录和数据库挂载位置:

fly.toml
[env]
  DATABASE_URL = "file:/app/dbs/dev.db"
  NEXT_PUBLIC_WEB_DOMAIN = "https://meishizaolunzi.com"
  NODE_ENV = "production"

[[statics]]
  guest_path = "/app/public/assert"
  url_prefix = "/assert"

[mounts]
  destination = "/app/dbs"
  source = "mszlz_db"

env 就是程序中需要用到的一些动态配置,这些环境变量都是公开的,如果是类似密钥之类的非公开环境变量可以使用下面的命令:

fly secrets set NEXT_PUBLIC_GOOGLE_ADSENSE=xxxxxxxxxxx
fly secrets set NEXT_PUBLIC_GOOGLE_ANALYTICS=xxxxxxxxxxxx

代码中使用 fly 提供的 secrets 命令设置加密的环境变量,你可以使用 list 选项来查看已有的加密环境变量:

C:\projects\mszlz_i18n>flyctl secrets list
NAME                         DIGEST                           DATE
NEXT_PUBLIC_GOOGLE_ADSENSE   d4265a095a98dd974b397d32865b847f 1m32s ago
NEXT_PUBLIC_GOOGLE_ANALYTICS 59dbbce879e7bcab087cd3305d574cc5 50s ago

statics 是程序静态目录的位置,fly.io 文档说它们不会提供 CDN,该配置只是为了方便访问而已。最重要的是 mounts,该配置记录了需要挂载的卷。

代码中将名称为 mszlz_db 的卷挂载到了容器内部的 /app/dbs 目录,该目录下保存了生产环境中使用的数据库。要使用该卷之前,首先要创建它:

fly volumes create mszlz_db --region hkg --size 1

上面的命令是在香港区域创建一个 1G 大小的磁盘卷,卷的名称为 mszlz_db,创建成功后控制台会给出磁盘卷的信息:

        ID: vol_5podq4qog8drg8w1
      Name: mszlz_db
       App: meishizaolunzi
    Region: hkg
      Zone: 4eed
   Size GB: 1
 Encrypted: true
Created at: 06 Jun 22 06:42 UTC

fly.toml 配置编写好后,可以使用 flyctl deploy 部署线上程序。

C:\projects\mszlz_i18n>fly deploy
==> Verifying app config
--> Verified app config
==> Building image
WARN Error connecting to local docker daemon: error during connect: This error may indicate that the docker daemon is not running.: Get "http://%2F%2F.%2Fpipe%2Fdocker_engine/_ping": open //./pipe/docker_engine: The system cannot find the file specified.
Remote builder fly-builder-long-morning-1513 ready
==> Creating build context
--> Creating build context done
==> Building image with Docker
--> docker host: 20.10.12 linux x86_64
[+] Building 318.8s (0/1)
[+] Building 283.0s (34/34) FINISHED
 => [internal] load remote build context                                                                                                                      0.0s
 => copy /context /                                                                                                                                          10.1s
 => [internal] load metadata for docker.io/library/node:alpine                                                                                                4.2s
 => [runner  1/19] FROM docker.io/library/node:[email protected]:7ab82182ec72ea2042e29f40fd2d7badf3023302928600803e2c158be85aee94                                 6.6s
 => => resolve docker.io/library/node:[email protected]:7ab82182ec72ea2042e29f40fd2d7badf3023302928600803e2c158be85aee94                                          0.0s
 => => sha256:ab43cc156f237710086329e05aa4b76f6d2097e918f6423a89c80324e2b24b14 6.67kB / 6.67kB                                                                0.0s
 => => sha256:8301355a1841488d8ac67d83203b803da7e1f3f137b84a5247e54d5fe13c7d07 46.85MB / 46.85MB                                                              1.4s
 => => sha256:b718db4754e5b95cb62b38149c02889ab616f1ce49880851217d5a1d0aacf7d3 2.35MB / 2.35MB                                                                1.4s
 => => sha256:22e946329298566bc47ba7aea843105d853489561de526a60886efde5278f412 449B / 449B                                                                    1.4s
 => => sha256:7ab82182ec72ea2042e29f40fd2d7badf3023302928600803e2c158be85aee94 1.43kB / 1.43kB                                                                0.0s
 => => sha256:678a564b95875da820fae3beaf29459bad1238690fb68abefbfb79d62b933ad4 1.16kB / 1.16kB                                                                0.0s
 => => extracting sha256:8301355a1841488d8ac67d83203b803da7e1f3f137b84a5247e54d5fe13c7d07                                                                     2.7s
 => => extracting sha256:b718db4754e5b95cb62b38149c02889ab616f1ce49880851217d5a1d0aacf7d3                                                                     0.2s
 => => extracting sha256:22e946329298566bc47ba7aea843105d853489561de526a60886efde5278f412                                                                     0.0s
 => [deps 2/6] RUN apk add --no-cache libc6-compat                                                                                                            4.1s
 => [runner  2/19] WORKDIR /app                                                                                                                               0.2s
 => [runner  3/19] RUN apk update && apk add sqlite && apk add --no-cache tzdata                                                                              6.2s
 => [builder 3/8] COPY . .                                                                                                                                    7.8s
 => [deps 3/6] RUN apk add --no-cache g++ make python3 py3-pip                                                                                                5.2s
 => [runner  4/19] RUN addgroup -g 1001 -S nodejs                                                                                                             1.1s
 => [runner  5/19] RUN adduser -S nextjs -u 1001                                                                                                              1.1s
 => [deps 4/6] WORKDIR /app                                                                                                                                   0.0s
 => [deps 5/6] COPY package.json ./                                                                                                                           0.0s
 => [deps 6/6] RUN yarn add sharp && yarn install --frozen-lockfile                                                                                          81.2s
 => [builder 4/8] COPY --from=deps /app/node_modules ./node_modules                                                                                           7.5s
 => [builder 5/8] RUN echo "DATABASE_URL="file:../dbs/dev.db"" > .env                                                                                         0.5s
 => [builder 6/8] RUN rm -rf /app/dbs/*                                                                                                                       0.4s
 => [builder 7/8] RUN yarn prisma generate && yarn prisma migrate deploy                                                                                     10.3s
 => [builder 8/8] RUN yarn build && yarn install --production --ignore-scripts --prefer-offline                                                             122.6s
 => [runner  6/19] COPY --from=builder /app/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh                                                          0.0s
 => [runner  7/19] RUN chmod +x /usr/local/bin/docker-entrypoint.sh                                                                                           0.6s
 => [runner  8/19] COPY --from=builder /app/init_post_table.sh /usr/local/bin/init_post_table.sh                                                              0.0s
 => [runner  9/19] RUN chmod +x /usr/local/bin/init_post_table.sh                                                                                             0.5s
 => [runner 10/19] COPY --from=builder /app/.env ./.env                                                                                                       0.0s
 => [runner 11/19] COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next                                                                               1.6s
 => [runner 12/19] COPY --from=builder /app/node_modules ./node_modules                                                                                       5.2s
 => [runner 13/19] COPY --from=builder /app/package.json ./package.json                                                                                       0.0s
 => [runner 14/19] COPY --from=builder /app/src/backend ./src/backend                                                                                         0.0s
 => [runner 15/19] COPY --from=builder /app/next.config.js ./next.config.js                                                                                   0.0s
 => [runner 16/19] COPY --from=builder /app/next-seo.config.js ./next-seo.config.js                                                                           0.0s
 => [runner 17/19] COPY --from=builder /app/safe-headers.js ./safe-headers.js                                                                                 0.0s
 => [runner 18/19] COPY --from=builder /app/public ./public                                                                                                   0.2s
 => [runner 19/19] COPY --from=builder --chown=nextjs:nodejs /app/dbs ./_dbs                                                                                  0.1s
 => exporting to image                                                                                                                                        6.6s
 => => exporting layers                                                                                                                                       6.6s
 => => writing image sha256:609b73878a6fc728fd45145e9d75b59607e8b4ca12e7a61e57c66bb14a0ba334                                                                  0.0s
 => => naming to registry.fly.io/meishizaolunzi:deployment-1654566124                                                                                         0.0s
--> Building image done
==> Pushing image to fly
The push refers to repository [registry.fly.io/meishizaolunzi]
8d37b689cf7d: Pushed
3234b3d2f913: Pushed
a5eb6b80d009: Pushed
a96136d78408: Pushed
2ffe682f5216: Pushed
95a8c0d0883e: Pushed
e823b4450cfd: Pushed
751a35158fcd: Pushed
8d5927e9be22: Pushed
01544559d53a: Pushed
40cd1a702c02: Pushed
d62a725a0c0f: Pushed
2c5ca691b365: Pushed
8cf118a3a462: Pushed
b4fbc66e906f: Pushed
731bac844128: Pushed
4c99585d4d82: Pushed
2efaeee721f9: Pushed
ae7472715687: Pushed
85bbd449b3e9: Pushed
02f331903ee1: Pushed
4fc242d58285: Layer already exists
deployment-1654566124: digest: sha256:9263582bf09f352f0d70e85616463e4fec4ed87a64413f955750f12951594cc1 size: 4912
--> Pushing image done
image: registry.fly.io/meishizaolunzi:deployment-1654566124
image size: 1.1 GB
==> Creating release
--> release v3 created

--> You can detach the terminal anytime without stopping the deployment
==> Monitoring deployment

 1 desired, 1 placed, 1 healthy, 0 unhealthy [health checks: 1 total, 1 passing]
--> v3 deployed successfully

如果中途失败,你可以使用 flyctl logs 来查看线上日志。如果一切顺利,你可以在网页中的后台界面看到给程序自动分配的二级域名和 IP 地址。

如果你的程序成功运行起来,你可以远程连接到线上主机:

flyctl ssh console -s

这对线上程序调试非常有帮助,我的数据库迁移就是直接使用这种方式操作的,和本地操作 Linux 的体验一样。下面是线上镜象运行的型号:

NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.15.4
PRETTY_NAME="Alpine Linux v3.15"
HOME_URL="https://alpinelinux.org/"
BUG_REPORT_URL="https://bugs.alpinelinux.org/"

7. 使用体会

7.1 unexpected EOF 错误

fly.io 偶尔在发布镜象的时候,特别是短时间重复部署同一个测试镜象经常遇 unexpected EOF 错误:

Error failed to fetch an image or build from source: error building: unexpected EOF

不知道是网络问题,还是什么问题,我目前的解决办法是删除现有远程编译镜像后重新部署。

7.2 remote builder app unavailable

fly.io 远程部署无法链接:

C:\code\mszlz_docs>flyctl deploy --remote-only
==> Verifying app config
--> Verified app config
==> Building image
WARN Remote builder did not start in time. Check remote builder logs with `flyctl logs -a fly-builder-old-glade-4379`
Error failed to fetch an image or build from source: error connecting to docker: remote builder app unavailable

尝试设置日志等级,查看具体原因:

set LOG_LEVEL=debug
flyctl deploy

因为下载其它开发依赖,设置代理导致的。使用 set 命令查看当期具体的环境变量。

8. 总结

至此算是搞定了 Nextjs 这个系列的文章,有很多内容其实我都删除了,感觉确实没有必要记录的那么详细,例如显示博客文章墨迹了一万多字,主要是个人比较啰嗦,文字不简炼,没办法都是看网络小说影响的🤪。 接下来看看,等添加其它功能的时候再更新……