Deployment

Backend Deployment

Deploy your Nuda Kit backend to DigitalOcean App Platform.

This guide walks you through deploying your Nuda Kit backend to DigitalOcean using App Platform, Container Registry, and managed databases.

Get $200 in free credits! Sign up using this link to receive $200 in credit for your first 60 days.

Overview

The deployment architecture consists of:

ComponentServicePurpose
Web ServiceApp PlatformServes the API
Email WorkerApp Platform WorkerProcesses email queue
User Deletions WorkerApp Platform WorkerProcesses user deletion queue
PostgreSQLManaged DatabasePrimary database
ValkeyMarketplace DropletRedis-compatible queue storage

Prerequisites

Before starting, ensure you have:


Required Files

Before deploying, ensure you have the following files in your backend repository.

Dockerfile

Create a Dockerfile in your backend folder:

# syntax=docker/dockerfile:1

# ========================================
# Stage 1: Build
# ========================================
FROM node:22-alpine AS builder

WORKDIR /app

# Install dependencies needed for native modules
RUN apk add --no-cache python3 make g++

# Copy package files
COPY package.json package-lock.json ./

# Install all dependencies (including devDependencies for build)
RUN npm ci

# Copy source code
COPY . .

# Build the application
RUN node ace build

# ========================================
# Stage 2: Production
# ========================================
FROM node:22-alpine AS production

WORKDIR /app

# Set production environment
ENV NODE_ENV=production

# Create non-root user for security
RUN addgroup -g 1001 -S nodejs && \
    adduser -S adonis -u 1001

# Copy package files
COPY package.json package-lock.json ./

# Install production dependencies only
RUN npm ci --omit=dev && \
    npm cache clean --force

# Copy built application from builder stage
COPY --from=builder /app/build ./

# Copy entrypoint script
COPY docker-entrypoint.sh ./
RUN chmod +x docker-entrypoint.sh

# Change ownership to non-root user
RUN chown -R adonis:nodejs /app

# Switch to non-root user
USER adonis

# Expose the application port
EXPOSE 3333

# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD wget --no-verbose --tries=1 --spider http://localhost:3333/health || exit 1

# Start the application
CMD ["./docker-entrypoint.sh"]

GitHub Actions Workflow

Create .github/workflows/build.yml in your backend repository:

name: Deploy to DigitalOcean

on:
  push:
    branches:
      - main
  workflow_dispatch:

env:
  REGISTRY: registry.digitalocean.com
  IMAGE_NAME: nudakit-backend

jobs:
  build-and-push:
    name: Build and Push Image
    runs-on: ubuntu-latest
    environment: production

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Install doctl
        uses: digitalocean/action-doctl@v2
        with:
          token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}

      - name: Log in to DigitalOcean Container Registry
        run: doctl registry login --expiry-seconds 1200

      - name: Retag current latest as previous (for rollback)
        run: |
          # Pull current latest and retag as previous (ignore if doesn't exist yet)
          docker pull ${{ env.REGISTRY }}/${{ secrets.DIGITALOCEAN_REGISTRY_NAME }}/${{ env.IMAGE_NAME }}:latest || true
          docker tag ${{ env.REGISTRY }}/${{ secrets.DIGITALOCEAN_REGISTRY_NAME }}/${{ env.IMAGE_NAME }}:latest \
            ${{ env.REGISTRY }}/${{ secrets.DIGITALOCEAN_REGISTRY_NAME }}/${{ env.IMAGE_NAME }}:previous || true
          docker push ${{ env.REGISTRY }}/${{ secrets.DIGITALOCEAN_REGISTRY_NAME }}/${{ env.IMAGE_NAME }}:previous || true

      - name: Build Docker image
        run: |
          docker build \
            -t ${{ env.REGISTRY }}/${{ secrets.DIGITALOCEAN_REGISTRY_NAME }}/${{ env.IMAGE_NAME }}:latest \
            .

      - name: Push image to DigitalOcean Container Registry
        run: |
          docker push ${{ env.REGISTRY }}/${{ secrets.DIGITALOCEAN_REGISTRY_NAME }}/${{ env.IMAGE_NAME }}:latest

      - name: Delete untagged manifests
        run: |
          doctl registry repository list-manifests ${{ secrets.DIGITALOCEAN_REGISTRY_NAME }}/${{ env.IMAGE_NAME }} --output json | \
            jq -r '.[] | select(.tags == null or .tags == []) | .digest' | \
            xargs -I {} doctl registry repository delete-manifest ${{ secrets.DIGITALOCEAN_REGISTRY_NAME }}/${{ env.IMAGE_NAME }} {} --force || true

      - name: Run garbage collection to free up space
        run: |
          doctl registry garbage-collection start --include-untagged-manifests --force

This workflow:

  • Triggers on every push to main or manually via workflow_dispatch
  • Builds your Docker image
  • Pushes it to DigitalOcean Container Registry
  • Keeps the previous image tagged for easy rollbacks
  • Cleans up untagged images to save storage

Step 1: Create a Container Registry

DigitalOcean Container Registry stores your Docker images.

  1. Log in to your DigitalOcean Dashboard
  2. Click Container Registry in the left sidebar
  3. Click Create Registry
  4. Choose a name (e.g., your-company)
  5. Select the Starter plan (free, 500MB) or Basic for more storage
  6. Click Create Registry
Note your registry name — you'll need it for GitHub secrets.

Step 2: Create a DigitalOcean API Token

Generate an API token for GitHub Actions to push images.

  1. Go to API in the left sidebar
  2. Click Generate New Token
  3. Name it (e.g., GitHub Actions)
  4. Select Full Access for scopes
  5. Click Generate Token
  6. Copy the token immediately — you won't see it again

Step 3: Configure GitHub Secrets

Add the required secrets to your GitHub repository.

  1. Go to your repository on GitHub
  2. Click SettingsSecrets and variablesActions
  3. Click New repository secret and add:
Secret NameValue
DIGITALOCEAN_ACCESS_TOKENYour API token from Step 2
DIGITALOCEAN_REGISTRY_NAMEYour registry name (e.g., your-company)

Step 4: Push Your First Image

The included GitHub Actions workflow automatically builds and pushes your Docker image when you push to main.

Trigger a deployment:

git add .
git commit -m "Deploy to production"
git push origin main

Verify the image was pushed:

  1. Go to Container Registry in DigitalOcean
  2. Click on your registry
  3. You should see the nudakit-backend repository with a latest tag
The workflow also keeps a previous tag for easy rollbacks.

Step 5: Set Up Valkey (Redis)

Nuda Kit uses Redis-compatible storage for queues. DigitalOcean offers Valkey, a Redis-compatible database.

  1. Go to Marketplace in the left sidebar
  2. Search for Valkey
  3. Click Create Valkey
  4. Configure the droplet:
    • Choose a plan (Basic $6/mo works for most apps)
    • Select a datacenter region (same as your app)
  5. Click Create
  6. Once created, note the connection details:
    • Host (e.g., your-valkey-droplet-ip)
    • Port (default: 6379)
    • Password (if configured)
Make sure your Valkey droplet is in the same VPC as your App Platform app, or configure the firewall to allow connections from your app.

Step 6: Create the App

  1. Go to App Platform in the left sidebar
  2. Click Create App
  3. Select DigitalOcean Container Registry as the source
  4. Choose your registry and the nudakit-backend repository
  5. Select the latest tag
  6. Click Next

Configure the Web Service

  1. Set the Resource Type to Web Service
  2. Set the HTTP Port to 3333
  3. Choose your plan (Basic $5/mo works for starting out)
  4. Click Next

Step 7: Add PostgreSQL Database

  1. On the app configuration page, click Add Resource
  2. Select Database
  3. Choose PostgreSQL
  4. Select Dev Database (free) or a managed plan for production
  5. Click Add Database

DigitalOcean will automatically inject the database connection as environment variables.


Step 8: Configure Environment Variables

Click on your web service component and go to the Environment Variables section.

Add the following variables:

Core Settings

VariableValue
NODE_ENVproduction
HOST0.0.0.0
PORT3333
APP_KEYGenerate with node ace generate:key locally
APP_URLhttps://your-app.ondigitalocean.app (your app URL)
FRONTEND_URLhttps://your-frontend.vercel.app (your Vercel URL)
LOG_LEVELinfo

Database

DigitalOcean injects these automatically when you add a database, but verify they match:

VariableValue
DB_HOST${db.HOSTNAME}
DB_PORT${db.PORT}
DB_USER${db.USERNAME}
DB_PASSWORD${db.PASSWORD}
DB_DATABASE${db.DATABASE}

Redis (Valkey)

VariableValue
QUEUE_REDIS_HOSTYour Valkey droplet IP
QUEUE_REDIS_PORT6379
QUEUE_REDIS_PASSWORDYour Valkey password (if set)

Email (Resend)

VariableValue
MAIL_DRIVERresend
MAIL_FROM_NAMEYour App Name
MAIL_FROM_ADDRESSnoreply@yourdomain.com
RESEND_API_KEYYour Resend API key
Get your Resend API key from resend.com/api-keys. You'll also need to verify your domain in Resend.

Stripe

VariableValue
STRIPE_SECRET_KEYsk_live_xxx (your live key)
STRIPE_WEBHOOK_SECRETWe'll get this in Step 11
STRIPE_API_VERSION2025-10-29.clover
STRIPE_DEVICE_NAMEproduction
STRIPE_EVENTS_WHITELISTcheckout.session.completed,invoice.paid,invoice.payment_failed,customer.subscription.created,customer.subscription.updated,customer.subscription.deleted

Other

VariableValue
LIMITER_STOREdatabase
DRIVE_DISKfs (or s3 if using S3)
SWAGGER_USERNAMEYour swagger docs username
SWAGGER_PASSWORDYour swagger docs password
SERVICE_TYPEweb

Step 9: Add Email Worker

The email worker processes the email queue in the background.

  1. Click Add ResourceCreate from source
  2. Select DigitalOcean Container Registry
  3. Choose the same nudakit-backend repository and latest tag
  4. Set Resource Type to Worker
  5. Name it email-worker
  6. Choose a plan (Basic $5/mo)

Configure Worker Environment Variables

The worker needs the same environment variables as the web service, plus:

VariableValue
SERVICE_TYPEworker
QUEUE_NAMEemails
You can copy environment variables from the web service and then add/modify the worker-specific ones.

Modify the Entrypoint Script

For the workers to use the QUEUE_NAME environment variable, update your docker-entrypoint.sh to:

#!/bin/sh
set -e

SERVICE_TYPE=${SERVICE_TYPE:-web}
QUEUE_NAME=${QUEUE_NAME:-emails}

echo "Running migrations..."
node ace migration:run --force

if [ "$SERVICE_TYPE" = "worker" ]; then
    echo "Starting Queue Worker for queue: $QUEUE_NAME"
    exec node ace queue:listen --queue=$QUEUE_NAME
else
    echo "Starting Web Server..."
    exec node bin/server.js
fi

This allows you to set QUEUE_NAME as an environment variable in DigitalOcean to specify which queue each worker should process.


Step 10: Add User Deletions Worker

Repeat the same process for the user deletions queue:

  1. Click Add ResourceCreate from source
  2. Select the same nudakit-backend repository
  3. Set Resource Type to Worker
  4. Name it user-deletions-worker

Environment Variables

Same as the email worker, but with:

VariableValue
SERVICE_TYPEworker
QUEUE_NAMEuser_deletions

Step 11: Configure Stripe Webhooks (Production)

In production, you need to set up webhooks in the Stripe Dashboard instead of using the CLI.

  1. Go to Stripe Dashboard → Webhooks
  2. Make sure you're in Live mode (toggle in top-right)
  3. Click Add endpoint
  4. Enter your webhook URL:
https://your-app.ondigitalocean.app/webhooks/stripe
  1. Select events to listen for:
    • checkout.session.completed
    • invoice.paid
    • invoice.payment_failed
    • customer.subscription.created
    • customer.subscription.updated
    • customer.subscription.deleted
  2. Click Add endpoint
  3. Click on the created endpoint and copy the Signing secret
  4. Go back to DigitalOcean and update the STRIPE_WEBHOOK_SECRET environment variable with this value
Important: Use your live Stripe keys (sk_live_) and create products/prices in live mode for production.

Step 12: Deploy

  1. Review all your settings
  2. Click Create Resources or Deploy
  3. Wait for the deployment to complete (usually 5-10 minutes)

Once deployed, your app URL will be displayed (e.g., https://your-app.ondigitalocean.app).


Step 13: Verify Deployment

Test that everything is working:

  1. Health Check: Visit https://your-app.ondigitalocean.app/health
  2. API Docs: Visit https://your-app.ondigitalocean.app/docs (if swagger is enabled)
  3. Test from Frontend: Update your Vercel frontend's API_BASE_URL to your new backend URL

Custom Domain (Optional)

To use your own domain:

  1. Go to your app in App Platform
  2. Click SettingsDomains
  3. Click Add Domain
  4. Enter your domain (e.g., api.yourdomain.com)
  5. Follow the DNS configuration instructions

DigitalOcean will automatically provision an SSL certificate.


Automatic Deployments

Every push to main will:

  1. Build a new Docker image via GitHub Actions
  2. Push to Container Registry with latest tag
  3. Keep the previous image as previous for rollbacks

To enable auto-deploy in App Platform:

  1. Go to your app Settings
  2. Under the web service, enable Auto-Deploy

Environment Variables Reference

VariableRequiredDescription
NODE_ENVYesproduction
APP_KEYYesEncryption key (generate with node ace generate:key)
APP_URLYesYour backend URL
FRONTEND_URLYesYour frontend URL (for CORS)
DB_*YesDatabase connection (auto-injected)
QUEUE_REDIS_*YesValkey/Redis connection
MAIL_DRIVERYesresend for production
RESEND_API_KEYYesResend API key
STRIPE_SECRET_KEYYesLive Stripe secret key
STRIPE_WEBHOOK_SECRETYesFrom Stripe Dashboard
SERVICE_TYPEYesweb or worker
QUEUE_NAMEWorkersemails or user_deletions

Troubleshooting

IssueSolution
App won't startCheck logs in App Platform for errors
Database connection failedVerify DB environment variables are using ${db.*} syntax
Queues not processingCheck worker logs and Valkey connection
Stripe webhooks failingVerify webhook URL and signing secret
Emails not sendingCheck Resend API key and domain verification
CORS errors from frontendEnsure FRONTEND_URL matches your Vercel URL

Viewing Logs

  1. Go to your app in App Platform
  2. Click on the component (web service or worker)
  3. Click Runtime Logs to see live logs

Rollback

If something goes wrong, rollback to the previous image:

  1. Go to Container Registry
  2. Find the previous tag
  3. In App Platform, update your component to use the previous tag instead of latest
  4. Deploy

Cost Estimate

ResourceMonthly Cost
App Platform (Web Service)~$5-12
App Platform (2 Workers)~$10-24
PostgreSQL (Dev)Free
PostgreSQL (Managed)~$15+
Valkey Droplet~$6
Total (Starter)~$21-47/mo
Start with the Dev database and Basic plans. Scale up as your app grows.

Next Steps

Frontend Deployment

Deploy your Nuxt frontend to Vercel.

Stripe Configuration

Complete Stripe setup for production.

Visionary Plan Support

Visionary Plan Members — You have access to our private Discord server where you can ask questions, get help with deployment, and receive priority support. If you haven't joined yet, check your purchase confirmation email for the invite link.

Need help with your deployment? Drop a message in the #deployment-help channel and we'll assist you directly.