我的原創部落格文章: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 加入專案中,請執行以下命令:
這將顯示一個互動式提示。選擇“是”,然後選擇“aws”:
確保安裝了所有必需的軟體包:
這將創建 sst.config.ts 文件,我們將在其中添加所有配置。
此外,還會建立一些其他檔案/目錄。讓我們將它們添加到 .dockerignore,我們不希望它們最終成為 Docker 映像:
這就是 sst 設定檔目前的樣子:
我們不打算使用AWS,所以讓我們將home參數設定為「local」:
現在可以開始在 run() 函數中加入東西了。
為了使用 SST 在 Hetzner 上建立 VPS,我們需要一個 Hetzner API 令牌。讓我們產生一個新的。
在 Hetzner 控制台中開啟項目,導航至「安全」標籤:
產生 API 令牌:
新的代幣將添加到您的項目中:
令牌只會顯示一次,請確保不要遺失。
新增 TLS 和 Hetzner 提供者:
為了在創建 Hetzner VPS 後執行進一步的命令,我們需要確保在創建過程中添加了 SSH 金鑰。為此,我們將在本地創建一個 SSH 令牌,然後將其公共部分新增至 Hetzner。在run函數中加入以下程式碼:
部署應用程式:
您將看到一個新的 SSH 金鑰已新增至 Hetzner 中:
現在我們可以繼續創建 VPS 了。
以下命令將確保在您的專案中建立新的 VPS:
這裡我使用 docker-ce 映像,因為它已經安裝了 Docker。您可以使用 Hetzner Cloud API 列出所有可用的映像、伺服器類型和資料中心。
驗證伺服器是否正確建立:
您還應該能夠在控制台中看到新建立的伺服器:
為了在 VPS 上建置應用程式 Docker 映像並且能夠建立網路、磁碟區和容器,我們需要在本機電腦和 VPS 上的 Docker Server 之間建立一座橋樑。為此,我們需要 Docker 提供者:
將 SSH 私鑰儲存在磁碟上,以便 SSH 用戶端可以存取它。建立與 VPS 上 Docker 伺服器的連線:
確保也將 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
Now we can build the Docker image on the remove Docker server:
// 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], } );
Let’s trigger the deployment to see if everything works:
pnpm sst deploy SST ❍ ion 0.1.90 ready! ➜ App: next-self-hosted Stage: antonprudkohliad ~ Deploy | Log Starting Docker build < ... A PRETTY LONG BUILD LOG HERE ... > | Log Image built successfully, local id "sha256:629a6cdfc298c74599a3056278e31c64197a87f6d11aab09573bc9171d2f3362" | Created Docker Image - App - Hetzner docker:index:Image (36.0s) ✓ Complete
Now, let’s check that Docker image made it to the server:
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
Awesome!
We will create two networks: Public and Internal. The Public network is for services to which NGINX is connected, i.e. for services that have to be exposed to the outside (e.g. the Next.js application or an API server). The Internal network is for services that are not meant to be exposed to the outside e.g. Postgres database, Redis cache:
// 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] } );
Trigger the deployment:
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
Check that networks app_network_internal and app_network_public are present on the remote:
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
We will create a volume to store the application build files (the .next folder):
// In the run() function: // Setup Docker Volumes const dockerVolumeAppBuild = new docker.Volume( "Docker Volume - App Build", { name: "app_volume_build" }, { provider: dockerServerHetzner, dependsOn: [server] } );
Deploy and verify that the docker volume app_volume_build is present on the 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
We are going to run a one-off container (a.k.a. Init Container) to build the Next.js application and store the result in the .next folder, that will be shared with the main application container through the volume that we have created above:
// 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, } );
Deploy and verify via logs that the build has been successful:
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
Now we will add a “runner” container, that will use the build output from the Build container, and run next start:
// 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] } );
Deploy and verify that the app has started successfully:
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
The app container might fail, because the build container has not finished building yet, but it will soon recover and function normally.
In order to upload files to the VPS, we need to install the Command provider and the Polumi package:
pnpm sst add @pulumi/command pnpm add -D @pulumi/pulumi pnpm install
Make sure that the /root/app and /root/app/certs directories exist on the VPS and upload Cloudflare Origin Server certificates:
// 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, }, } );
Copy Nginx configuration file to the VPS and start the Nginx container:
// 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 };
Deploy and verify that the Nginx container is running:
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
As you can see, Nginx and the application are running smoothly.
It’s time to make sure that the DNS record is pointing to the right IP address (yes, it is possible to add this to the SST config too, via the Cloudflare provider):
Then, we can open the application and verify that it works:
Congratulations! We have now completed out SST dive and can enjoy the freshly deployed application ?
SST makes it very easy to clean up – just run pnpm sst remove and the whole setup will go away:
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
以上是如何使用 SST 和 Docker 將 Next.js 應用程式部署到 Hetzner VPS的詳細內容。更多資訊請關注PHP中文網其他相關文章!