Show HN: A Complete Dokploy Deployment Guide for Next.js Project
3 hours ago
1
This article will provide a detailed guide on how to deploy projects using Dokploy, covering the complete process from purchasing a server to completing deployment.
Dokploy is an open-source self-hosted PaaS (Platform as a Service) platform that can serve as an open-source alternative to services like Vercel, Netlify, Railway, and Zeabur.
Using Dokploy requires purchasing your own server. If you're unsure which provider to choose, consider hostinger. For early-stage projects, a 2-core 8GB VPS is sufficient, with monthly fees starting at just $6.49.
After completing payment, follow the on-screen prompts to complete VPS setup. Once configured, the dashboard will show that the VPS is running.
Next, you need to configure firewall rules. Hostinger has no default firewall rules, meaning all ports are open, which poses security risks. We need to create firewall rules to allow access to ports 22, 80, 443, and 3000.
After registering and logging in, first set a custom domain for the management dashboard:
Then add a DNS record for this custom domain in your domain resolution platform (using CloudFlare as an example), select A record type, and fill in your server IP address.
Once the resolution takes effect, you can access the Dokploy management dashboard through the custom domain.
Dokploy provides a visual deployment interface similar to Vercel, but on servers with weaker performance, it's prone to server crashes or restarts due to insufficient resources. Therefore, direct deployment is only suitable for small projects.
Create a Project, then create a Service:
Enter the Service page, set up the Provider by selecting Github Account, Repository, and Branch in sequence, then click Save:
Next, click the Deploy button at the top:
Set environment variables. You need to redeploy the project after each modification:
View build progress
Configure custom domain
After creation, you need to set up DNS in Cloudflare:
Add two records:
A Record:your-domain.com -> Your server IPEnable ProxyCNAME Record:www.your-domain.com -> Your server IPEnable Proxy
Then open SSL/TLS settings and select Full or Flexible:
Set up redirects by going to Advanced - Redirects
I prefer to redirect the www domain to the non-www domain, so I selected Redirect to non-www
After completing the above configuration, the project can run successfully on Dokploy. Each subsequent code commit will automatically trigger deployment.
Attention: This approach is not the recommended method. For actual deployment, it's advised to use Option 2 described below.
This approach uses Github Actions to build Docker images. After the build completes, Dokploy directly pulls the image to start, which can significantly reduce server pressure.
Go to the Service page, open Advanced, and set Cluster Settings
Select the registry you just created, then click Save:
Next, 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 imageon: 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 build # Use vars for environment variables starting with NEXT_PUBLIC_ # Use secrets for other environment variables 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 }} DATABASE_URL=${{ secrets.DATABASE_URL }} - name: Trigger dokploy redeploy # ⚠️ Use the Webhook URL from Dokploy deployments run: | curl -X GET https://xxxxx
Important notes:
It's recommended to only include environment variables needed during build in build-args
Use vars for environment variables starting with NEXT_PUBLIC_
Use secrets for other environment variables
Nexty series boilerplates also require R2_PUBLIC_URL and DATABASE_URL during the build stage, so these two environment variables need to be included as well
Fill in the Webhook URL displayed in Dokploy deployments for the https URL at the end
Next, create a Dockerfile in the root directory to define how to build the Docker image:
# ============================================# Dependencies stage# ============================================FROM node:20-alpine AS depsWORKDIR /app# Enable corepack (for managing package manager versions)RUN corepack enable# Copy package management filesCOPY package.json pnpm-lock.yaml* ./# Prefetch dependencies using cache mountRUN --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 builderWORKDIR /app# Enable corepackRUN corepack enable# Copy node_modules from dependencies stageCOPY --from=deps /app/node_modules ./node_modules# Copy all source codeCOPY . .# ============================================# Build arguments# Only declare variables needed during build# ============================================# NEXT_PUBLIC_* variables (will be embedded into client JavaScript)ARG NEXT_PUBLIC_SITE_URLARG NEXT_PUBLIC_PRICING_PATHARG NEXT_PUBLIC_OPTIMIZED_IMAGESARG NEXT_PUBLIC_LOGIN_MODEARG NEXT_PUBLIC_GITHUB_CLIENT_IDARG NEXT_PUBLIC_GOOGLE_CLIENT_IDARG NEXT_PUBLIC_TURNSTILE_SITE_KEYARG NEXT_PUBLIC_ENABLE_STRIPEARG NEXT_PUBLIC_DEFAULT_CURRENCYARG NEXT_PUBLIC_GOOGLE_IDARG NEXT_PUBLIC_PLAUSIBLE_DOMAINARG NEXT_PUBLIC_PLAUSIBLE_SRCARG NEXT_PUBLIC_DISCORD_INVITE_URLARG NEXT_PUBLIC_AUTO_FILL_AI_PROVIDERARG NEXT_PUBLIC_AUTO_FILL_AI_MODEL_IDARG NEXT_PUBLIC_DAILY_AI_AUTO_FILL_LIMITARG NEXT_PUBLIC_DAILY_SUBMIT_LIMITARG NEXT_PUBLIC_DAILY_IMAGE_UPLOAD_LIMIT# R2_PUBLIC_URL is used by next.config.mjs to configure image remote patternsARG R2_PUBLIC_URL# DATABASE_URL is used during build for static site generation (SSG)ARG DATABASE_URL# ============================================# Build environment variables# ============================================# Set NEXT_PUBLIC_* as environment variables so Next.js can embed them into bundle filesENV 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}# Set R2_PUBLIC_URL for next.config.mjsENV R2_PUBLIC_URL=${R2_PUBLIC_URL}# Set DATABASE_URL for static site generationENV DATABASE_URL=${DATABASE_URL}# Disable Next.js telemetryENV NEXT_TELEMETRY_DISABLED=1# Build the applicationRUN --mount=type=cache,target=/root/.local/share/pnpm/store/v3 \ pnpm build# ============================================# Runtime stage# ============================================FROM node:20-alpine AS runnerWORKDIR /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 environmentENV NODE_ENV=production# Disable Next.js telemetryENV NEXT_TELEMETRY_DISABLED=1# Create system user group and user (for secure operation)RUN addgroup --system --gid 1001 nodejsRUN adduser --system --uid 1001 nextjs# Copy necessary files from build stageCOPY --from=builder /app/public ./publicCOPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static# Switch to non-root userUSER nextjs# Expose portEXPOSE 3000# Set port and hostnameENV PORT=3000ENV HOSTNAME="0.0.0.0"# Start the applicationCMD ["node", "server.js"]
This way, the built image only contains environment variables necessary for the build stage, avoiding leakage of important secrets.
For best practices, you can also add a .dockerignore file in the root directory:
.git.githubnode_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 not starting with NEXT_PUBLIC_ in Secrets
Environment variables needed for the runtime stage need to be configured in Dokploy's Environment:
For convenience, you can directly copy all environment variables here.
Now commit your 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:
Since using Dokploy deployment requires maintaining your own server, although most situations won't have issues, if problems do occur, people without operations experience will find it very difficult to resolve them. Therefore, I recommend only considering Dokploy for websites where deploying on Vercel has poor cost-effectiveness, such as the following scenarios:
Non-critical products where security risks don't need to be considered
Products that don't make money or have low profit margins
Products with many pages, such as directory sites and documentation sites
For my personal use, projects I currently deploy using Dokploy fall into several categories:
Third-party open-source tools, such as Plausible
Blog and documentation sites, such as Nexty documentation