最新公告
  • 欢迎您光临码农资源网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!加入我们
  • 如何使用 SST 和 Docker 将 Nextjs 应用程序部署到 Hetzner VPS

    我的原创博文:https://www.prudkohliad.com/articles/deploy-next-js-to-vps-using-sst-2024-08-11

    sst 是一个框架,可以让您轻松在自己的基础设施上构建现代全栈应用程序。 sst v3 使用 pulumi 和 terraform
    – sst 文档

    在本指南中,我们将使用 sst 和 docker 在 hetzner vps 上部署 next.js 应用程序。本指南是我上一篇文章的后续内容。如果您在这里发现一些没有意义的内容,您很有可能会在那里找到答案 – 如何使用 docker 和 github actions 将 next.js 应用程序部署到 hetzner 上的 vps。

    将sst添加到项目中

    要将 sst 添加到项目中,请运行以下命令:

    pnpx sst@ion init
    

    这将显示一个交互式提示。选择“是”,然后选择“aws”:

    如何使用 SST 和 Docker 将 Nextjs 应用程序部署到 Hetzner VPS

    sst 初始化输出 – 终端

    确保安装了所有必需的软件包:

    pnpm install
    

    这将创建 sst.config.ts 文件,我们将在其中添加所有配置。

    此外,还会创建一些其他文件/目录。让我们将它们添加到 .dockerignore,我们不希望它们最终成为 docker 镜像:

    # sst
    .sst
    sst.config.ts
    tsconfig.json
    

    这就是 sst 配置文件当前的样子:

    /// <reference path="./.sst/platform/config.d.ts"></reference>
    
    export default $config({
      app(input) {
        return {
          name: "next-self-hosted",
          removal: input?.stage === "production" ? "retain" : "remove",
          home: "aws",
        };
      },
      async run() {},
    });
    

    我们不打算使用aws,所以让我们将home参数设置为“local”:

    /// <reference path="./.sst/platform/config.d.ts"></reference>
    
    export default $config({
      app(input) {
        return {
          name: "next-self-hosted",
          removal: input?.stage === "production" ? "retain" : "remove",
          home: "local",
        };
      },
      async run() {},
    });
    

    现在可以开始向 run() 函数添加东西​​了。

    在 hetzner 上创建 api 令牌

    为了使用 sst 在 hetzner 上创建 vps,我们需要一个 hetzner api 令牌。让我们生成一个新的。

    在 hetzner 控制台中打开项目,导航到“安全”选项卡:

    如何使用 SST 和 Docker 将 Nextjs 应用程序部署到 Hetzner VPS

    项目安全设置 – hetzner cloud ui

    生成 api 令牌:

    如何使用 SST 和 Docker 将 Nextjs 应用程序部署到 Hetzner VPS

    生成 api 令牌 – hetzner cloud ui

    新的代币将添加到您的项目中:

    如何使用 SST 和 Docker 将 Nextjs 应用程序部署到 Hetzner VPS

    api 令牌 – hetzner cloud ui

    令牌只会显示一次,请确保不要丢失。

    添加 tls 和 hetzner 提供商:

    pnpm sst add tls
    pnpm sst add hcloud
    pnpm install
    

    生成 ssh 密钥

    为了在创建 hetzner vps 后执行进一步的命令,我们需要确保在创建过程中添加了 ssh 密钥。为此,我们将在本地创建一个 ssh 令牌,然后将其公共部分添加到 hetzner。在run函数中添加以下代码:

    // in the run() function:
    
    // generate an ssh key
    const sshkeylocal = new tls.privatekey("ssh key - local", {
      algorithm: "ed25519",
    });
    
    // add the ssh key to hetzner
    const sshkeyhetzner = new hcloud.sshkey("ssh key - hetzner", {
      publickey: sshkeylocal.publickeyopenssh,
    });
    

    部署应用程序:

    pnpm sst deploy
    
    sst ❍ ion 0.1.90  ready!
    
    ➜  app:        next-self-hosted
       stage:      antonprudkohliad
    
    ~  deploy
    
    |  created     ssh key - local tls:index:privatekey
    |  created     ssh key - hetzner hcloud:index:sshkey
    
    ✓  complete
    

    您将看到一个新的 ssh 密钥已添加到 hetzner 中:

    如何使用 SST 和 Docker 将 Nextjs 应用程序部署到 Hetzner VPS

    ssh 密钥 – hetzner cloud ui

    现在我们可以继续创建 vps 了。

    创建服务器

    以下命令将确保在您的项目中创建新的 vps:

    // in the run() function:
    
    // create a server on hetzner
    const server = new hcloud.server("server", {
      image: "docker-ce",
      servertype: "cx22",
      location: "nbg1",
      sshkeys: [sshkeyhetzner.id],
    });
    

    这里我使用 docker-ce 镜像,因为它已经安装了 docker。您可以使用 hetzner cloud api 列出所有可用的图像、服务器类型和数据中心。

    验证服务器是否正确创建:

    pnpm sst deploy
    sst ❍ ion 0.1.90  ready!
    
    ➜  app:        next-self-hosted
       stage:      antonprudkohliad
    
    ~  deploy
    
    |  created     server hcloud:index:server (34.5s)
    
    ✓  complete
    

    您还应该能够在控制台中看到新创建的服务器:

    如何使用 SST 和 Docker 将 Nextjs 应用程序部署到 Hetzner VPS

    服务器 – hetzner cloud ui

    连接到 vps 上的 docker 服务器

    为了在 vps 上构建应用程序 docker 镜像并能够创建网络、卷和容器,我们需要在本地计算机和 vps 上的 docker server 之间建立一座桥梁。为此,我们需要 docker 提供商:

    pnpm sst add docker
    pnpm install
    

    将 ssh 私钥存储在磁盘上,以便 ssh 客户端可以访问它。创建与 vps 上 docker 服务器的连接:

    // at the top of the file:
    import { resolve as pathresolve } from "node:path";
    import { writefilesync as fswritefilesync } from "node:fs";
    
    // in the run() function:
    
    // store the private ssh key on disk to be able to pass it to the docker
    // provider
    const sshkeylocalpath = sshkeylocal.privatekeyopenssh.apply((k) => {
      const path = "id_ed25519_hetzner";
      fswritefilesync(path, k, { mode: 0o600 });
      return pathresolve(path);
    });
    
    // connect to the docker server on the hetzner server
    const dockerserverhetzner = new docker.provider("docker server - hetzner", {
      host: $interpolate`ssh://root@${server.ipv4address}`,
      sshopts: ["-i", sshkeylocalpath, "-o", "stricthostkeychecking=no"],
    });
    

    确保还将 ssh 私钥 id_ed25519_hetzner 添加到 .gitignore 和 .dockerignore,这样它就不会进入您的 github 存储库和 docker 镜像。

    触发部署以验证更改:

    pnpm sst deploy
    sst ❍ ion 0.1.90  ready!
    
    ➜  app:        next-self-hosted
       stage:      antonprudkohliad
    
    ~  deploy
    
    |  created     docker server - hetzner pulumi:providers:docker
    
    ✓  complete
    

    构建 docker 镜像

    现在我们可以在删除的 docker 服务器上构建 docker 镜像了:

    // in the run() function:
    
    // build the docker image
    const dockerimagehetzner = new docker.image(
      "docker image - app - hetzner",
      {
        imagename: "next-self-hosted/next-self-hosted:latest",
        build: {
          context: pathresolve("./"),
          dockerfile: pathresolve("./dockerfile"),
          target: "production",
          platform: "linux/amd64",
        },
        skippush: true,
      },
      {
        provider: dockerserverhetzner,
        dependson: [server],
      }
    );
    

    让我们触发部署看看一切是否正常:

    pnpm sst deploy
    sst ❍ ion 0.1.90  ready!
    
    ➜  app:        next-self-hosted
       stage:      antonprudkohliad
    
    ~  deploy
    
    |  log         starting docker build
    
    |  log         image built successfully, local id "sha256:629a6cdfc298c74599a3056278e31c64197a87f6d11aab09573bc9171d2f3362"
    |  created     docker image - app - hetzner docker:index:image (36.0s)
    
    ✓  complete
    

    现在,让我们检查 docker 镜像是否已到达服务器:

    ssh root@116.203.183.180 -i ./id_ed25519_hetzner -o stricthostkeychecking=no -c "docker image ls"
    repository                          tag       image id       created              size
    next-self-hosted/next-self-hosted   latest    629a6cdfc298   about a minute ago   712mb
    

    太棒了!

    docker 网络

    我们将创建两个网络:公共网络和内部网络。公共网络用于 nginx 连接的服务,即必须暴露于外部的服务(例如 next.js 应用程序或 api 服务器)。内部网络用于不应该暴露给外部的服务,例如postgres数据库、redis缓存:

    // in the run() function:
    
    // setup docker networks
    const dockernetworkpublic = new docker.network(
      "docker network - public",
      { name: "app_network_public" },
      { provider: dockerserverhetzner, dependson: [server] }
    );
    const dockernetworkinternal = new docker.network(
      "docker network - internal",
      { name: "app_network_internal" },
      { provider: dockerserverhetzner, dependson: [server] }
    );
    

    触发部署:

    pnpm sst deploy
    sst ❍ ion 0.1.90  ready!
    
    ➜  app:        next-self-hosted
       stage:      antonprudkohliad
    
    ~  deploy
    
    |  created     docker network - public docker:index:network (2.3s)
    |  created     docker network - internal docker:index:network (3.1s)
    
    ✓  complete
    

    检查网络 app_network_internal 和 app_network_public 是否存在于远程:

    ssh root@116.203.183.180 -i ./id_ed25519_hetzner -o stricthostkeychecking=no -c "docker network ls"
    network id     name                   driver    scope
    0590360bd4ae   app_network_internal   bridge    local
    e3bd8be72506   app_network_public     bridge    local
    827fa5ca5de2   bridge                 bridge    local
    dc8880514199   host                   host      local
    f1481867db18   none                   null      local
    

    docker 卷

    我们将创建一个卷来存储应用程序构建文件(.next 文件夹):

    // in the run() function:
    
    // setup docker volumes
    const dockervolumeappbuild = new docker.volume(
      "docker volume - app build",
      { name: "app_volume_build" },
      { provider: dockerserverhetzner, dependson: [server] }
    );
    

    部署并验证 docker 卷 app_volume_build 是否存在于 vps 上:

    pnpm sst deploy
    sst ❍ ion 0.1.90  ready!
    
    ➜  app:        next-self-hosted
       stage:      antonprudkohliad
    
    ~  deploy
    
    |  created     docker volume - app build docker:index:volume
    
    ✓  complete
    
    ssh root@116.203.183.180 -i ./id_ed25519_hetzner -o stricthostkeychecking=no -c "docker volume ls"
    driver    volume name
    local     app_volume_build
    

    构建容器

    我们将运行一个一次性容器(也称为 init 容器)来构建 next.js 应用程序并将结果存储在 .next 文件夹中,该文件夹将通过我们上面创建的卷与主应用程序容器共享:

    // in the run() function:
    
    // run a one-off container to build the app
    const dockerappbuildcontainer = new docker.container(
      "docker container - app build",
      {
        name: "app_container_build",
        image: dockerimagehetzner.imagename,
        volumes: [
          {
            volumename: dockervolumeappbuild.name,
            containerpath: "/app/.next",
          },
        ],
        command: ["pnpm", "build"],
        mustrun: true,
      },
      {
        provider: dockerserverhetzner,
      }
    );
    

    部署并通过日志验证构建是否成功:

    pnpm sst deploy
    sst ❍ ion 0.1.90  ready!
    
    ➜  app:        next-self-hosted
       stage:      antonprudkohliad
    
    ~  deploy
    
    |  created     docker container - app build docker:index:container (1.1s)
    
    ✓  complete
    
    ssh root@116.203.183.180 -i ./id_ed25519_hetzner -o stricthostkeychecking=no -c "docker logs -f app_container_build"
    
    > next-self-hosted@ build /app
    > next build
    
      ▲ next.js 14.2.5
    
       creating an optimized production build ...
     ✓ compiled successfully
       linting and checking validity of types ...
       collecting page data ...
       generating static pages (0/4) ...
       generating static pages (1/4)
       generating static pages (2/4)
       generating static pages (3/4)
     ✓ generating static pages (4/4)
       finalizing page optimization ...
       collecting build traces ...
    
    route (app)                              size     first load js
    ┌ ○ /                                    142 b          87.2 kb
    └ ○ /_not-found                          871 b          87.9 kb
    + first load js shared by all            87 kb
      ├ chunks/52d5e6ad-40eff88d15e66edb.js  53.6 kb
      ├ chunks/539-e1fa9689ed3badf0.js       31.5 kb
      └ other shared chunks (total)          1.84 kb
    
    ○  (static)  prerendered as static content
    

    应用程序容器

    现在我们将添加一个“runner”容器,它将使用构建容器的构建输出,并在下次启动时运行:

    // in the run() function:
    
    const dockerappcontainer = new docker.container(
      "docker container - app",
      {
        name: "app",
        image: dockerimagehetzner.imagename,
        volumes: [
          {
            volumename: dockervolumeappbuild.name,
            containerpath: "/app/.next",
          },
        ],
        networksadvanced: [
          { name: dockernetworkpublic.id },
          { name: dockernetworkinternal.id },
        ],
        command: ["pnpm", "start"],
        restart: "always",
      },
      { provider: dockerserverhetzner, dependson: [dockerappbuildcontainer] }
    );
    

    部署并验证应用是否启动成功:

    pnpm sst deploy
    sst ❍ ion 0.1.90  ready!
    
    ➜  app:        next-self-hosted
       stage:      antonprudkohliad
    
    ~  deploy
    
    |  created     docker container - app docker:index:container (1.1s)
    
    ✓  complete
    
    ssh root@116.203.183.180 -i ./id_ed25519_hetzner -o stricthostkeychecking=no -c "docker logs -f app"
    
    > next-self-hosted@ start /app
    > next start
    
      ▲ next.js 14.2.5
      - local:        http://localhost:3000
    
     ✓ starting...
     ✓ ready in 497ms
    

    应用程序容器可能会失败,因为构建容器尚未完成构建,但它很快就会恢复并正常运行。

    添加 cloudflare 证书

    为了将文件上传到vps,我们需要安装命令提供程序和polumi包:

    pnpm sst add @pulumi/command
    pnpm add -d @pulumi/pulumi
    pnpm install
    

    确保 vps 上存在 /root/app 和 /root/app/certs 目录并上传 cloudflare origin server 证书:

    // at the top of the file
    import { asset as pulumiasset } from "@pulumi/pulumi";
    
    // in the run() function:
    
    // make sure that app directory exists
    new command.remote.command("command - ensure app directory", {
      create: "mkdir -p /root/app",
      connection: {
        host: server.ipv4address,
        user: "root",
        privatekey: sshkeylocal.privatekeyopenssh,
      },
    });
    
    // make sure that app/certs directory exists
    new command.remote.command("command - ensure app/certs directory", {
      create: "mkdir -p /root/app/certs",
      connection: {
        host: server.ipv4address,
        user: "root",
        privatekey: sshkeylocal.privatekeyopenssh,
      },
    });
    
    // copy certificates to the vps
    new command.remote.copytoremote(
      "copy - certificates - key",
      {
        source: new pulumiasset.fileasset(
          pathresolve("./certs/cloudflare.key.pem")
        ),
        remotepath: "/root/app/certs/cloudflare.key.pem",
        connection: {
          host: server.ipv4address,
          user: "root",
          privatekey: sshkeylocal.privatekeyopenssh,
        },
      }
    );
    new command.remote.copytoremote(
      "copy - certificates - cert",
      {
        source: new pulumiasset.fileasset(
          pathresolve("./certs/cloudflare.cert.pem")
        ),
        remotepath: "/root/app/certs/cloudflare.cert.pem",
        connection: {
          host: server.ipv4address,
          user: "root",
          privatekey: sshkeylocal.privatekeyopenssh,
        },
      }
    );
    new command.remote.copytoremote(
      "copy - certificates - authenticated origin pull",
      {
        source: new pulumiasset.fileasset(
          pathresolve("./certs/authenticated_origin_pull_ca.pem")
        ),
        remotepath: "/root/app/certs/authenticated_origin_pull_ca.pem",
        connection: {
          host: server.ipv4address,
          user: "root",
          privatekey: sshkeylocal.privatekeyopenssh,
        },
      }
    );
    

    启动 nginx

    复制 nginx 配置文件到 vps 并启动 nginx 容器:

    // in the run() function:
    
    // copy nginx config to the vps
    const commandcopynginxconfig = new command.remote.copytoremote(
      "copy - nginx config",
      {
        source: new pulumiasset.fileasset(
          pathresolve("./nginx/production.conf")
        ),
        remotepath: "/root/app/nginx.conf",
        connection: {
          host: server.ipv4address,
          user: "root",
          privatekey: sshkeylocal.privatekeyopenssh,
        },
      }
    );
    
    // run the nginx container
    const dockernginxcontainer = new docker.container(
      "docker container - nginx",
      {
        name: "app_container_nginx",
        image: "nginx:1.27.0-bookworm",
        volumes: [
          {
            hostpath: "/root/app/nginx.conf",
            containerpath: "/etc/nginx/nginx.conf",
          },
          {
            hostpath: "/root/app/certs",
            containerpath: "/certs",
          },
        ],
        command: ["nginx", "-g", "daemon off;"],
        networksadvanced: [{ name: dockernetworkpublic.id }],
        restart: "always",
        ports: [
          {
            external: 443,
            internal: 443,
          },
        ],
        healthcheck: {
          tests: ["cmd", "service", "nginx", "status"],
          interval: "30s",
          timeout: "5s",
          retries: 5,
          startperiod: "10s",
        },
      },
      { provider: dockerserverhetzner, dependson: [dockerappcontainer] }
    );
    
    return { ip: server.ipv4address };
    

    部署并验证 nginx 容器是否正在运行:

    pnpm sst deploy
    sst ❍ ion 0.1.90  ready!
    
    ➜  app:        next-self-hosted
       stage:      antonprudkohliad
    
    ~  deploy
    
    |  deleted     docker container - app build docker:index:container
    |  created     command - ensure app/certs directory command:remote:command
    |  created     command - ensure app directory command:remote:command
    |  created     docker container - app build docker:index:container
    |  created     copy - certificates - cert command:remote:copytoremote (1.2s)
    |  created     copy - nginx config command:remote:copytoremote (1.2s)
    |  created     copy - certificates - key command:remote:copytoremote (1.2s)
    |  created     copy - certificates - authenticated origin pull command:remote:copytoremote (1.2s)
    |  deleted     docker container - app docker:index:container
    |  created     docker container - app docker:index:container (1.2s)
    |  created     docker container - nginx docker:index:container (7.1s)
    
    ✓  complete
       ip: 116.203.183.180
    
    ssh root@116.203.183.180 -i ./id_ed25519_hetzner -o stricthostkeychecking=no -c "docker ps -a"
    
    container id   image                                      command                  created         status                     ports                          names
    9c2cb18db304   nginx:1.27.0-bookworm                      "/docker-entrypoint.…"   3 minutes ago   up 3 minutes (healthy)     80/tcp, 0.0.0.0:443->443/tcp   app_container_nginx
    32e6a4cee8bc   next-self-hosted/next-self-hosted:latest   "docker-entrypoint.s…"   4 minutes ago   up 3 minutes               3000/tcp                       app
    f0c50aa32493   next-self-hosted/next-self-hosted:latest   "docker-entrypoint.s…"   4 minutes ago   exited (0) 3 minutes ago                                  app_container_build
    

    可以看到,nginx 和应用程序运行顺利。

    最后检查

    是时候确保 dns 记录指向正确的 ip 地址了(是的,也可以通过 cloudflare 提供商将其添加到 sst 配置中):

    dns settings – cloudflare ui

    dns 设置 – cloudflare ui

    然后,我们可以打开应用程序并验证它是否有效:

    如何使用 SST 和 Docker 将 Nextjs 应用程序部署到 Hetzner VPS

    浏览器中的应用程序

    恭喜!我们现在已经完成了 sst 潜水,可以享受新部署的应用程序了?

    清理

    sst 使得清理变得非常容易 – 只需运行 pnpm sst remove ,整个设置就会消失:

    pnpm sst remove
    SST ❍ ion 0.1.90  ready!
    
    ➜  App:        next-self-hosted
       Stage:      antonprudkohliad
    
    ~  Remove
    
    |  Deleted     Docker Container - Nginx docker:index:Container (1.9s)
    |  Deleted     Docker Container - App docker:index:Container
    |  Deleted     Docker Container - App Build docker:index:Container
    |  Deleted     Docker Image - App - Hetzner docker:index:Image
    |  Deleted     Docker Volume - App Build docker:index:Volume (2.1s)
    |  Deleted     Docker Network - Public docker:index:Network (3.1s)
    |  Deleted     Docker Network - Internal docker:index:Network (3.2s)
    |  Deleted     Copy - Nginx Config command:remote:CopyToRemote
    |  Deleted     Docker Server - Hetzner pulumi:providers:docker
    |  Deleted     Copy - Certificates - Authenticated Origin Pull command:remote:CopyToRemote
    |  Deleted     Command - Ensure app/certs directory command:remote:Command
    |  Deleted     Copy - Certificates - Key command:remote:CopyToRemote
    |  Deleted     Command - Ensure app directory command:remote:Command
    |  Deleted     Copy - Certificates - Cert command:remote:CopyToRemote
    |  Deleted     Server hcloud:index:Server (16.8s)
    |  Deleted     SSH Key - Hetzner hcloud:index:SshKey
    |  Deleted     SSH Key - Local tls:index:PrivateKey
    
    ✓  Removed
    
    想要了解更多内容,请持续关注码农资源网,一起探索发现编程世界的无限可能!
    本站部分资源来源于网络,仅限用于学习和研究目的,请勿用于其他用途。
    如有侵权请发送邮件至1943759704@qq.com删除

    码农资源网 » 如何使用 SST 和 Docker 将 Nextjs 应用程序部署到 Hetzner VPS
    • 7会员总数(位)
    • 25846资源总数(个)
    • 0本周发布(个)
    • 0 今日发布(个)
    • 294稳定运行(天)

    提供最优质的资源集合

    立即查看 了解详情