使用 Dokploy 部署 Next.js 项目
本文详细介绍如何使用 Dokploy 部署 Next.js 项目,包含服务器购买、Dokploy 安装配置、直接部署和 GitHub Actions 自动化部署两种方案,帮助你快速完成项目上线。
前置准备
在开始部署之前,建议先在 next.config.mjs 中添加以下配置:
const nextConfig = {
output: "standalone", // 添加这行
// other code...
}由于 Dokploy 基于 Docker 运行,使用 standalone 模式可以显著减小构建镜像的体积。
什么是 Dokploy
Dokploy 是一个开源的自托管 PaaS(Platform as a Service)平台,可作为 Vercel、Netlify、Railway、Zeabur 等服务的开源替代方案。

服务器准备与 Dokploy 安装
购买服务器
使用 Dokploy 需要自行购买服务器。如果你不确定选择哪家服务商,可以考虑 hostinger。对于项目初期,2核8G 的 VPS 就足够使用,月费仅需 $6.49。

配置服务器
完成付款后,按照页面提示完成 VPS 设置。配置完成后,控制台会显示 VPS 已启动。
接下来需要配置防火墙规则。Hostinger 默认没有防火墙规则,这意味着所有端口都处于开放状态,存在安全风险。我们需要创建防火墙规则,开放 22、80、443、3000 端口的访问权限。


安装 Dokploy
打开 VPS 的 Terminal

使用 SSH 登录 VPS,执行 Dokploy 安装命令:
curl -sSL https://dokploy.com/install.sh | sh
安装完成后,访问命令行输出的地址即可进入 Dokploy 管理后台。
配置 Dokploy
注册并登录后,首先为管理后台设置自定义域名:

然后在域名解析平台(以 CloudFlare 为例)添加该自定义域名的解析记录,选择 A 类型解析,IP 地址填写服务器地址。

解析生效后,就可以通过自定义域名访问 Dokploy 管理后台了。
最后,绑定你的 Git 账号,如下图所示:

部署方案一:直接部署
Dokploy 提供了类似 Vercel 的可视化部署界面,但在性能较弱的服务器上容易因资源不足导致服务器崩溃或重启。因此,直接部署方式仅适合小型项目。
创建 Project,然后创建 Service:


进入 Service 页面,设置 Provider,依次选择 Github Account、Repository、Branch,然后点击 Save:

接着点击上方的 Deploy 按钮:

设置环境变量,每次修改后需要重新部署项目:

查看构建进度

配置自定义域名


创建完成后,需要在 Cloudflare 设置 DNS:

添加两条记录:
A 记录:
your-domain.com -> 你的服务器 IP
开启 Proxy
A 记录:
www.your-domain.com -> 你的服务器 IP
开启 Proxy然后打开 SSL/TLS 设置,选择 Full 或 Flexible:

设置重定向,进入 Advanced - Redirects


我习惯将 www 域名重定向到不带 www 的域名,所以选择了 Redirect to non-www

完成以上配置后,项目就可以在 Dokploy 上成功运行了。之后每次提交代码都会自动触发部署。
注意:该方案不是推荐做法,实际部署建议使用下面介绍的方案二。
部署方案二:搭配 Github Actions 部署(推荐)
该方案使用 Github Actions 构建 Docker 镜像,构建完成后 Dokploy 直接拉取镜像启动,可以大幅降低服务器压力。
前置配置
前置配置只需设置一次,即可永久使用。
首先到 GitHub 创建 Personal access token,点此直达,创建一个新的 Token

创建完成后,会看到 token,要保存下来,等下要用。
如果忘了保存,可以回到这里,点击蓝色字进去重新生成(Regenerate token)


回到 Dokploy 管理后台,进入 Registry 模块,添加 Registry

- Registry Name:自定义名称
- Username:你的 GitHub ID
- Password:上面生成的 token
- Registry URL:https://ghcr.io

部署步骤
进入 Service 的页面,打开 Advanced,设置 Cluster Settings

选择刚才创建的 registry,然后点击 Save:

接着点击 General,Provider 选择 Docker,Docker Image 输入 ghcr.io/[GitHub ID]/[Repo Name]:[Branch],然后 Save:

打开 Deployments,复制显示的 Webhook URL:

现在回到我们的代码,在根目录创建文件 .github/workflows/docker-image.yml,这是一个 GitHub Actions 工作流配置文件,用于自动化构建和发布 Docker 镜像:
name: Create and publish a Docker image
on:
push:
branches: ["main"]
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push-image:
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
packages: write
attestations: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Log in to the Container registry
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push Docker image
id: push
timeout-minutes: 25
uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# ⚠️
# 建议只填写构建需要的环境变量
# NEXT_PUBLIC_ 开头的环境变量使用 vars
# 其他环境变量使用 secrets
build-args: |
NEXT_PUBLIC_SITE_URL=${{ vars.NEXT_PUBLIC_SITE_URL }}
NEXT_PUBLIC_PRICING_PATH=${{ vars.NEXT_PUBLIC_PRICING_PATH }}
NEXT_PUBLIC_OPTIMIZED_IMAGES=${{ vars.NEXT_PUBLIC_OPTIMIZED_IMAGES }}
NEXT_PUBLIC_LOGIN_MODE=${{ vars.NEXT_PUBLIC_LOGIN_MODE }}
NEXT_PUBLIC_GITHUB_CLIENT_ID=${{ vars.NEXT_PUBLIC_GITHUB_CLIENT_ID }}
NEXT_PUBLIC_GOOGLE_CLIENT_ID=${{ vars.NEXT_PUBLIC_GOOGLE_CLIENT_ID }}
NEXT_PUBLIC_TURNSTILE_SITE_KEY=${{ vars.NEXT_PUBLIC_TURNSTILE_SITE_KEY }}
NEXT_PUBLIC_ENABLE_STRIPE=${{ vars.NEXT_PUBLIC_ENABLE_STRIPE }}
NEXT_PUBLIC_DEFAULT_CURRENCY=${{ vars.NEXT_PUBLIC_DEFAULT_CURRENCY }}
NEXT_PUBLIC_GOOGLE_ID=${{ vars.NEXT_PUBLIC_GOOGLE_ID }}
NEXT_PUBLIC_PLAUSIBLE_DOMAIN=${{ vars.NEXT_PUBLIC_PLAUSIBLE_DOMAIN }}
NEXT_PUBLIC_PLAUSIBLE_SRC=${{ vars.NEXT_PUBLIC_PLAUSIBLE_SRC }}
NEXT_PUBLIC_DISCORD_INVITE_URL=${{ vars.NEXT_PUBLIC_DISCORD_INVITE_URL }}
NEXT_PUBLIC_AUTO_FILL_AI_PROVIDER=${{ vars.NEXT_PUBLIC_AUTO_FILL_AI_PROVIDER }}
NEXT_PUBLIC_AUTO_FILL_AI_MODEL_ID=${{ vars.NEXT_PUBLIC_AUTO_FILL_AI_MODEL_ID }}
NEXT_PUBLIC_DAILY_AI_AUTO_FILL_LIMIT=${{ vars.NEXT_PUBLIC_DAILY_AI_AUTO_FILL_LIMIT }}
NEXT_PUBLIC_DAILY_SUBMIT_LIMIT=${{ vars.NEXT_PUBLIC_DAILY_SUBMIT_LIMIT }}
NEXT_PUBLIC_DAILY_IMAGE_UPLOAD_LIMIT=${{ vars.NEXT_PUBLIC_DAILY_IMAGE_UPLOAD_LIMIT }}
R2_PUBLIC_URL=${{ secrets.R2_PUBLIC_URL }}
R2_ACCOUNT_ID=${{ secrets.R2_ACCOUNT_ID }}
R2_ACCESS_KEY_ID=${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY=${{ secrets.R2_SECRET_ACCESS_KEY }}
R2_BUCKET_NAME=${{ secrets.R2_BUCKET_NAME }}
DATABASE_URL=${{ secrets.DATABASE_URL }}
BETTER_AUTH_SECRET=${{ secrets.BETTER_AUTH_SECRET }}
BETTER_AUTH_GITHUB_CLIENT_SECRET=${{ secrets.BETTER_AUTH_GITHUB_CLIENT_SECRET }}
GOOGLE_CLIENT_SECRET=${{ secrets.GOOGLE_CLIENT_SECRET }}
STRIPE_SECRET_KEY=${{ secrets.STRIPE_SECRET_KEY }}
STRIPE_PUBLISHABLE_KEY=${{ secrets.STRIPE_PUBLISHABLE_KEY }}
STRIPE_WEBHOOK_SECRET=${{ secrets.STRIPE_WEBHOOK_SECRET }}
STRIPE_CUSTOMER_PORTAL_URL=${{ secrets.STRIPE_CUSTOMER_PORTAL_URL }}
RESEND_API_KEY=${{ secrets.RESEND_API_KEY }}
RESEND_AUDIENCE_ID=${{ secrets.RESEND_AUDIENCE_ID }}
ADMIN_EMAIL=${{ secrets.ADMIN_EMAIL }}
ADMIN_NAME=${{ secrets.ADMIN_NAME }}
UPSTASH_REDIS_REST_URL=${{ secrets.UPSTASH_REDIS_REST_URL }}
UPSTASH_REDIS_REST_TOKEN=${{ secrets.UPSTASH_REDIS_REST_TOKEN }}
OPENROUTER_API_KEY=${{ secrets.OPENROUTER_API_KEY }}
PLAUSIBLE_API_KEY=${{ secrets.PLAUSIBLE_API_KEY }}
PLAUSIBLE_URL=${{ secrets.PLAUSIBLE_URL }}
DISCORD_SUBMIT_WEBHOOK_URL=${{ secrets.DISCORD_SUBMIT_WEBHOOK_URL }}
FIRECRAWL_API_KEY=${{ secrets.FIRECRAWL_API_KEY }}
- name: Trigger dokploy redeploy
# ⚠️ 使用 Dokploy deployments 的 Webhook URL
run: |
curl -X GET https://xxxxx注意事项:
NEXT_PUBLIC_开头的环境变量使用vars引用- 其他环境变量使用
secrets引用 - 末尾的 https 地址填写 Dokploy deployments 显示的 Webhook URL
接着在根目录创建 Dockerfile 文件,定义如何构建 Docker 镜像:
# ============================================
# 依赖阶段
# ============================================
FROM node:20-alpine AS deps
WORKDIR /app
# 启用 corepack(用于管理包管理器版本)
RUN corepack enable
# 复制包管理文件
COPY package.json pnpm-lock.yaml* ./
# 使用缓存挂载预取依赖
RUN --mount=type=cache,target=/root/.local/share/pnpm/store/v3 \
pnpm fetch
# 使用缓存挂载安装依赖(冻结锁文件,离线模式)
RUN --mount=type=cache,target=/root/.local/share/pnpm/store/v3 \
pnpm install --frozen-lockfile --offline
# ============================================
# 构建阶段
# ============================================
FROM node:20-alpine AS builder
WORKDIR /app
# 启用 corepack
RUN corepack enable
# 从依赖阶段复制 node_modules
COPY --from=deps /app/node_modules ./node_modules
# 复制所有源代码
COPY . .
# ============================================
# 构建参数
# 仅声明构建时需要的变量
# ============================================
# NEXT_PUBLIC_* 变量(会被嵌入到客户端 JavaScript 中)
ARG NEXT_PUBLIC_SITE_URL
ARG NEXT_PUBLIC_PRICING_PATH
ARG NEXT_PUBLIC_OPTIMIZED_IMAGES
ARG NEXT_PUBLIC_LOGIN_MODE
ARG NEXT_PUBLIC_GITHUB_CLIENT_ID
ARG NEXT_PUBLIC_GOOGLE_CLIENT_ID
ARG NEXT_PUBLIC_TURNSTILE_SITE_KEY
ARG NEXT_PUBLIC_ENABLE_STRIPE
ARG NEXT_PUBLIC_DEFAULT_CURRENCY
ARG NEXT_PUBLIC_GOOGLE_ID
ARG NEXT_PUBLIC_PLAUSIBLE_DOMAIN
ARG NEXT_PUBLIC_PLAUSIBLE_SRC
ARG NEXT_PUBLIC_DISCORD_INVITE_URL
ARG NEXT_PUBLIC_AUTO_FILL_AI_PROVIDER
ARG NEXT_PUBLIC_AUTO_FILL_AI_MODEL_ID
ARG NEXT_PUBLIC_DAILY_AI_AUTO_FILL_LIMIT
ARG NEXT_PUBLIC_DAILY_SUBMIT_LIMIT
ARG NEXT_PUBLIC_DAILY_IMAGE_UPLOAD_LIMIT
# R2_PUBLIC_URL is needed by next.config.mjs for image remotePatterns
ARG R2_PUBLIC_URL
ARG R2_ACCOUNT_ID
ARG R2_ACCESS_KEY_ID
ARG R2_SECRET_ACCESS_KEY
ARG R2_BUCKET_NAME
# DATABASE_URL is needed for static site generation (SSG) at build time
ARG DATABASE_URL
# BETTER_AUTH
ARG BETTER_AUTH_SECRET
ARG BETTER_AUTH_GITHUB_CLIENT_SECRET
ARG GOOGLE_CLIENT_SECRET
# STRIPE
ARG STRIPE_SECRET_KEY
ARG STRIPE_PUBLISHABLE_KEY
ARG STRIPE_WEBHOOK_SECRET
ARG STRIPE_CUSTOMER_PORTAL_URL
# RESEND
ARG RESEND_API_KEY
ARG RESEND_AUDIENCE_ID
ARG ADMIN_EMAIL
ARG ADMIN_NAME
# UPSTASH
ARG UPSTASH_REDIS_REST_URL
ARG UPSTASH_REDIS_REST_TOKEN
# AI
ARG OPENROUTER_API_KEY
ARG FIRECRAWL_API_KEY
# PLAUSIBLE
ARG PLAUSIBLE_API_KEY
ARG PLAUSIBLE_URL
# DISCORD
ARG DISCORD_SUBMIT_WEBHOOK_URL
# ============================================
# 构建环境变量
# ============================================
# 将 NEXT_PUBLIC_* 设置为环境变量,以便 Next.js 可以将它们嵌入到打包文件中
ENV NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL}
ENV NEXT_PUBLIC_PRICING_PATH=${NEXT_PUBLIC_PRICING_PATH}
ENV NEXT_PUBLIC_OPTIMIZED_IMAGES=${NEXT_PUBLIC_OPTIMIZED_IMAGES}
ENV NEXT_PUBLIC_LOGIN_MODE=${NEXT_PUBLIC_LOGIN_MODE}
ENV NEXT_PUBLIC_GITHUB_CLIENT_ID=${NEXT_PUBLIC_GITHUB_CLIENT_ID}
ENV NEXT_PUBLIC_GOOGLE_CLIENT_ID=${NEXT_PUBLIC_GOOGLE_CLIENT_ID}
ENV NEXT_PUBLIC_TURNSTILE_SITE_KEY=${NEXT_PUBLIC_TURNSTILE_SITE_KEY}
ENV NEXT_PUBLIC_ENABLE_STRIPE=${NEXT_PUBLIC_ENABLE_STRIPE}
ENV NEXT_PUBLIC_DEFAULT_CURRENCY=${NEXT_PUBLIC_DEFAULT_CURRENCY}
ENV NEXT_PUBLIC_GOOGLE_ID=${NEXT_PUBLIC_GOOGLE_ID}
ENV NEXT_PUBLIC_PLAUSIBLE_DOMAIN=${NEXT_PUBLIC_PLAUSIBLE_DOMAIN}
ENV NEXT_PUBLIC_PLAUSIBLE_SRC=${NEXT_PUBLIC_PLAUSIBLE_SRC}
ENV NEXT_PUBLIC_DISCORD_INVITE_URL=${NEXT_PUBLIC_DISCORD_INVITE_URL}
ENV NEXT_PUBLIC_AUTO_FILL_AI_PROVIDER=${NEXT_PUBLIC_AUTO_FILL_AI_PROVIDER}
ENV NEXT_PUBLIC_AUTO_FILL_AI_MODEL_ID=${NEXT_PUBLIC_AUTO_FILL_AI_MODEL_ID}
ENV NEXT_PUBLIC_DAILY_AI_AUTO_FILL_LIMIT=${NEXT_PUBLIC_DAILY_AI_AUTO_FILL_LIMIT}
ENV NEXT_PUBLIC_DAILY_SUBMIT_LIMIT=${NEXT_PUBLIC_DAILY_SUBMIT_LIMIT}
ENV NEXT_PUBLIC_DAILY_IMAGE_UPLOAD_LIMIT=${NEXT_PUBLIC_DAILY_IMAGE_UPLOAD_LIMIT}
# R2
ENV R2_PUBLIC_URL=${R2_PUBLIC_URL}
ENV R2_ACCOUNT_ID=${R2_ACCOUNT_ID}
ENV R2_ACCESS_KEY_ID=${R2_ACCESS_KEY_ID}
ENV R2_SECRET_ACCESS_KEY=${R2_SECRET_ACCESS_KEY}
ENV R2_BUCKET_NAME=${R2_BUCKET_NAME}
# DATABASE_URL for static site generation
ENV DATABASE_URL=${DATABASE_URL}
# BETTER_AUTH
ENV BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET}
ENV BETTER_AUTH_GITHUB_CLIENT_SECRET=${BETTER_AUTH_GITHUB_CLIENT_SECRET}
ENV GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
# Stripe
ENV STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY}
ENV STRIPE_PUBLISHABLE_KEY=${STRIPE_PUBLISHABLE_KEY}
ENV STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET}
ENV STRIPE_CUSTOMER_PORTAL_URL=${STRIPE_CUSTOMER_PORTAL_URL}
# RESEND
ENV RESEND_API_KEY=${RESEND_API_KEY}
ENV RESEND_AUDIENCE_ID=${RESEND_AUDIENCE_ID}
ENV ADMIN_EMAIL=${ADMIN_EMAIL}
ENV ADMIN_NAME=${ADMIN_NAME}
# UPSTASH
ENV UPSTASH_REDIS_REST_URL=${UPSTASH_REDIS_REST_URL}
ENV UPSTASH_REDIS_REST_TOKEN=${UPSTASH_REDIS_REST_TOKEN}
# AI
ENV OPENROUTER_API_KEY=${OPENROUTER_API_KEY}
ENV FIRECRAWL_API_KEY=${FIRECRAWL_API_KEY}
# PLAUSIBLE
ENV PLAUSIBLE_API_KEY=${PLAUSIBLE_API_KEY}
ENV PLAUSIBLE_URL=${PLAUSIBLE_URL}
# DISCORD
ENV DISCORD_SUBMIT_WEBHOOK_URL=${DISCORD_SUBMIT_WEBHOOK_URL}
# 禁用 Next.js 遥测
ENV NEXT_TELEMETRY_DISABLED=1
# 构建应用程序
RUN --mount=type=cache,target=/root/.local/share/pnpm/store/v3 \
pnpm build
# ============================================
# 运行阶段
# ============================================
FROM node:20-alpine AS runner
WORKDIR /app
# ============================================
# 此处不需要构建参数或环境变量
# 所有运行时密钥将由容器运行时(dokploy)注入
# Next.js standalone 模式在运行时读取环境变量
# ============================================
# 设置生产环境
ENV NODE_ENV=production
# 禁用 Next.js 遥测
ENV NEXT_TELEMETRY_DISABLED=1
# 创建系统用户组和用户(用于安全运行)
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# 从构建阶段复制必要文件
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
# 切换到非 root 用户
USER nextjs
# 暴露端口
EXPOSE 3000
# 设置端口和主机名
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
# 启动应用程序
CMD ["node", "server.js"]在配置 docker-image.yml、Dockerfile 和 GitHub Actions 时,你需要了解一个重要的安全原则:
- 私人项目,镜像通常只有自己有权限访问,可以放心地把所有环境变量都写到文件和加入 Github Actions,这样就不用判断环境变量属于构建时还是运行时
- 开源项目或镜像公开的项目,建议只将构建时环境变量写入文件和 Github Actions,这样可以保证敏感信息不泄漏
- 镜像是否仅自己有权限,可以在
https://github.com/users/[你的 Github 用户名]/packages/container/[项目仓库名]/settings查看

出于规范,还可以在根目录添加 .dockerignore 文件:
.git
.github
node_modules
.next确认 docker-image.yml 和 Dockerfile 声明的环境变量后,需要在 GitHub Actions 的 secrets and variables 中进行定义:

- 将
NEXT_PUBLIC_开头的环境变量定义到 Variables - 将不带
NEXT_PUBLIC_开头的环境变量定义到 Secrets


运行阶段需要的环境变量需要配置到 Dokploy 的 Environment 中:

为了方便,可以直接将所有环境变量复制到这里。
现在提交代码,就会触发 GitHub Actions 构建流程。构建完成后,Dokploy 会自动拉取最新镜像并完成项目更新。

如果需要在没有代码更新的情况下重新构建,可以在 GitHub Actions 中手动触发构建流程:

什么时候选择 Dokploy 部署
因为使用 Dokploy 部署需要自己维护服务器,虽然大部分情况下不会出问题,但万一出问题,没有运维经验的人会很难解决好问题,所以我建议只有部署在 Vercel 性价比很低的网站才考虑使用 Dokploy,例如以下场景:
- 不重要的产品,无需考虑安全风险
- 不赚钱或利润低的产品
- 页面多的产品,如:导航站、文档站
就我个人使用,现在使用 Dokploy 部署的项目有几类:
- 第三方开源工具,如:Plausible
- 博客和文档网站,如:Nexty 文档
- 导航站,如:Dofollow.Tools
- 轻量的小产品
- 定时任务爬虫