This article provides a comprehensive guide on deploying Next.js projects using Dokploy, covering server purchase, Dokploy installation and configuration, direct deployment, and GitHub Actions automated deployment to help you quickly launch your project.
Prerequisites
Before starting deployment, it's recommended to add the following configuration to next.config.mjs:
const nextConfig = {
  output: "standalone", // Add this line
  // other code...
}
Since Dokploy runs on Docker, using standalone mode can significantly reduce the build image size.
This article is based on the deployment steps for my Next.js SaaS boilerplate Nexty.dev, and is the most comprehensive tutorial on the internet for deploying Next.js projects with Dokploy. I hope it helps everyone.
Nexty.dev is one of the top 3 Next.js SaaS boilerplates, loved by many developers for its robust infrastructure and detailed documentation. If you're looking for an excellent Next.js SaaS boilerplate, you definitely shouldn't miss Nexty.dev.
Original article: Deploying Next.js Projects with Dokploy
What is Dokploy
Dokploy is an open-source self-hosted PaaS (Platform as a Service) that serves as an alternative to services like Vercel, Netlify, Railway, and Zeabur.
Server Setup and Dokploy Installation
Purchase a Server
Using Dokploy requires purchasing your own server.
Install Dokploy
Open the VPS Terminal
Log in to the VPS via SSH and execute the Dokploy installation command:
curl -sSL https://dokploy.com/install.sh | sh
After installation completes, visit the address shown in the command output to access the Dokploy dashboard.
Configure Dokploy
After registration and login, first set up a custom domain for the dashboard:
Then add a DNS record for this custom domain in your domain resolution platform (using CloudFlare as an example), select A type resolution, and enter the server IP address.
Once the DNS propagates, you can access the Dokploy dashboard through the custom domain.
Finally, connect your Git account as shown:
Deployment Option 1: Direct Deployment
Dokploy provides a Vercel-like visual deployment interface. However, on resource-constrained servers, large builds can cause the server itself to crash or restart due to memory exhaustion. Therefore, direct deployment is only suitable for small projects.
Create a Project, then create a Service:
Enter the Service page, set the Provider by selecting Github Account, Repository, and Branch in sequence, then click Save:
Next, click the Deploy button at the top:
Configure environment variables. You need to redeploy the project after each modification:
View build progress
Configure custom domain
After the build completes, you need to set up DNS in Cloudflare:
Add two records:
A record:
your-domain.com -> Your server IP
Enable Proxy
A record:
www.your-domain.com -> Your server IP
Enable Proxy
Then go to SSL/TLS settings and select Full or Flexible:
Set up redirects by going to Advanced - Redirects
I prefer redirecting the www domain to the non-www domain, so I selected Redirect to non-www
After completing these configurations, the project will be deployed and run successfully on Dokploy. Every code push will automatically trigger a new deployment.
Note: This approach is not recommended. For actual deployment, use Option 2 described below.
Deployment Option 2: Deploy with GitHub Actions (Recommended)
This approach uses GitHub Actions to build Docker images. After the build completes, Dokploy directly pulls the image and starts it, significantly reducing server load.
Initial Configuration
Initial configuration only needs to be set up once and can be used permanently.
First, create a personal access token on GitHub, click here, and create a new Token
After creation, you'll see the token. Save it as you'll need it later.
If you forgot to save it, you can go back here, click the blue text to regenerate the token (Regenerate token)
Return to the Dokploy dashboard, go to the Registry page, and add a Registry
- Registry Name: Custom name
- Username: Your GitHub ID
- Password: Token generated above
- Registry URL: https://ghcr.io
Deployment Steps
Go to the Service page, open Advanced, and set Cluster Settings
Select the registry you just created, then click Save:
Then click General, select Docker for Provider, enter ghcr.io/[GitHub ID]/[Repo Name]:[Branch] for Docker Image, then Save:
Open Deployments and copy the displayed Webhook URL:
Now return to your code and create the file .github/workflows/docker-image.yml in the root directory. This is a GitHub Actions workflow configuration file for automating Docker image building and publishing:
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
          # ⚠️
          # It's recommended to only include environment variables needed for the build
          # Environment variables starting with NEXT_PUBLIC_ use vars
          # Other environment variables use 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
        # ⚠️ Use the Webhook URL from Dokploy deployments
        run: |
          curl -X GET https://xxxxx
Notes:
- Environment variables starting with NEXT_PUBLIC_usevarsreference
- Other environment variables use secretsreference
- Replace the https address at the end with the Webhook URL shown in Dokploy deployments
Next, create a Dockerfile in the root directory that defines how to build the Docker image:
# ============================================
# Dependencies Stage
# ============================================
FROM node:20-alpine AS deps
WORKDIR /app
# Enable corepack (for managing package manager versions)
RUN corepack enable
# Copy package management files
COPY package.json pnpm-lock.yaml* ./
# Prefetch dependencies using cache mount
RUN --mount=type=cache,target=/root/.local/share/pnpm/store/v3 \
  pnpm fetch
# Install dependencies using cache mount (frozen lockfile, offline mode)
RUN --mount=type=cache,target=/root/.local/share/pnpm/store/v3 \
  pnpm install --frozen-lockfile --offline
# ============================================
# Build Stage
# ============================================
FROM node:20-alpine AS builder
WORKDIR /app
# Enable corepack
RUN corepack enable
# Copy node_modules from dependencies stage
COPY --from=deps /app/node_modules ./node_modules
# Copy all source code
COPY . .
# ============================================
# Build Arguments
# Only declare variables needed at build time
# ============================================
# NEXT_PUBLIC_* variables (will be embedded in client-side 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
# ============================================
# Build Environment Variables
# ============================================
# Set NEXT_PUBLIC_* as environment variables so Next.js can embed them in the bundle
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}
# Disable Next.js telemetry
ENV NEXT_TELEMETRY_DISABLED=1
# Build the application
RUN --mount=type=cache,target=/root/.local/share/pnpm/store/v3 \
  pnpm build
# ============================================
# Runtime Stage
# ============================================
FROM node:20-alpine AS runner
WORKDIR /app
# ============================================
# No build arguments or environment variables needed here
# All runtime secrets will be injected by the container runtime (dokploy)
# Next.js standalone mode reads environment variables at runtime
# ============================================
# Set production environment
ENV NODE_ENV=production
# Disable Next.js telemetry
ENV NEXT_TELEMETRY_DISABLED=1
# Create system user group and user (for secure execution)
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Copy necessary files from build stage
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
# Switch to non-root user
USER nextjs
# Expose port
EXPOSE 3000
# Set port and hostname
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
# Start the application
CMD ["node", "server.js"]
When configuring docker-image.yml, Dockerfile and GitHub Actions, you need to understand an important security principle:
- Private projects: Images are usually only accessible to you, so you can safely include all environment variables in the files and GitHub Actions without worrying about distinguishing between build-time and runtime variables
- Open-source projects or projects with public images: It's recommended to only include build-time environment variables in the files and GitHub Actions to ensure sensitive information isn't leaked
- You can check whether your image is public or private at https://github.com/users/[your Github username]/packages/container/[project repository name]/settings
For best practices, you can also add a .dockerignore file to the root directory:
.git
.github
node_modules
.next
After confirming the environment variables declared in docker-image.yml and Dockerfile, you need to define them in GitHub Actions' secrets and variables:
- Define environment variables starting with NEXT_PUBLIC_in Variables
- Define environment variables without the NEXT_PUBLIC_prefix in Secrets
Environment variables needed at runtime should be configured in Dokploy's Environment:
For convenience, you can directly copy all environment variables here.
Now push the code, which will trigger the GitHub Actions build process. After the build completes, Dokploy will automatically pull the latest image and complete the project update.
If you need to rebuild without code updates, you can manually trigger the build process in GitHub Actions:
When to Choose Dokploy for Deployment
Since using Dokploy requires maintaining your own server, although most situations won't have issues, if problems do occur, people without DevOps experience will find it difficult to resolve them. Therefore, I recommend considering Dokploy only when deploying on Vercel is not cost-effective, such as in the following scenarios:
- Non-critical products where security risks are acceptable
- Products that don't generate revenue or have low profit margins
- Products with many pages, such as directory sites or documentation sites
For my personal use, projects I currently deploy on Dokploy include:
- Third-party open-source tools, such as Plausible
- Blog and documentation sites, such as Nexty.dev documentation
- Directory sites, such as Dofollow.Tools
- Lightweight small products
- Scheduled task crawlers
 
 
              



































 
    
Top comments (0)