Deploy with Dokploy
Deploy your Grit application using Dokploy — a self-hosted PaaS that gives you the convenience of Vercel or Railway on your own VPS. Web dashboard, auto-SSL, GitHub integration, and Docker Compose support — all for free.
On This Page
What is Dokploy?
Dokploy is an open-source, self-hosted Platform-as-a-Service (PaaS). Think of it as your own private Vercel or Heroku running on a VPS you control. It provides:
| Feature | Description |
|---|---|
| Web Dashboard | Visual UI for managing deployments, logs, domains, and environment variables |
| Docker Compose | Deploy multi-service apps with a single Compose file — perfect for Grit monorepos |
| Auto SSL | Automatic Let's Encrypt certificates for all your domains via Traefik |
| GitHub Integration | Auto-deploy on git push with webhook-based continuous deployment |
| Database Backups | Scheduled backups to S3-compatible storage with one-click restore |
| Monitoring | Built-in logs viewer, resource usage graphs, and deployment history |
Why Dokploy for Grit? Grit's monorepo (Go API + 2 Next.js apps + PostgreSQL + Redis) maps naturally to a Docker Compose project. Dokploy deploys the entire stack from a single Compose file, handles SSL for all services, and gives you a dashboard to manage everything — no manual Caddy/Nginx config, no SSH for routine deploys.
Prerequisites
Before you begin, make sure you have:
- ✓A VPS with at least 2 vCPU / 4GB RAM (Hetzner CX22, DigitalOcean 4GB, or similar)
- ✓Ubuntu 22.04 or Debian 12 on the VPS (fresh install recommended)
- ✓A domain name (e.g., yourdomain.com) with access to DNS settings
- ✓Your Grit project pushed to a GitHub repository
- ✓SSH access to your VPS (root or sudo user)
Server sizing: A Grit monorepo (Go API + 2 Next.js apps + PostgreSQL + Redis + MinIO) uses around 2-3 GB of RAM at idle. We recommend 4 GB RAM minimum to leave room for builds and traffic spikes.
Install Dokploy
SSH into your VPS and run the one-line installer. This sets up Docker, Traefik (reverse proxy), and the Dokploy dashboard:
The installation takes 2-5 minutes. Once complete, you can access the Dokploy dashboard at:
http://your-server-ip:3000Create your admin account on first visit. Save these credentials — you'll need them to manage your deployments.
Important: If port 3000 is already in use on your server, Dokploy will fail to start. Make sure no other service is using port 3000 before installing. You can check with sudo lsof -i :3000.
Create a Project
In the Dokploy dashboard:
- 1Click "Projects" in the sidebar, then "Create Project"
- 2Give it a name (e.g., "my-grit-app")
- 3Inside the project, click "+ Create Service" and select "Compose"
- 4Choose "GitHub" as the provider (you'll need to connect your GitHub account on first use)
- 5Select your repository and branch (usually "main")
- 6Set the Compose file path to docker-compose.prod.yml (or wherever your production compose file lives)
Tip: Dokploy also supports deploying from a raw Docker Compose YAML pasted directly into the dashboard. This is useful for quick testing before connecting GitHub.
Docker Compose Configuration
Your docker-compose.prod.yml defines all services. Here's a production-ready Compose file optimized for Dokploy:
services:# Go APIapi:build:context: ./apps/apidockerfile: Dockerfileports:- "8080:8080"env_file:- .envdepends_on:postgres:condition: service_healthyredis:condition: service_healthyrestart: unless-stopped# Next.js Web Appweb:build:context: .dockerfile: apps/web/Dockerfileports:- "3000:3000"environment:- NEXT_PUBLIC_API_URL=${API_URL}restart: unless-stopped# Next.js Admin Paneladmin:build:context: .dockerfile: apps/admin/Dockerfileports:- "3001:3000"environment:- NEXT_PUBLIC_API_URL=${API_URL}restart: unless-stopped# PostgreSQLpostgres:image: postgres:16-alpinevolumes:- postgres_data:/var/lib/postgresql/dataenvironment:POSTGRES_USER: ${DB_USER:-postgres}POSTGRES_PASSWORD: ${DB_PASSWORD:-password}POSTGRES_DB: ${DB_NAME:-gritapp}healthcheck:test: ["CMD-SHELL", "pg_isready -U postgres"]interval: 5stimeout: 5sretries: 5restart: unless-stopped# Redisredis:image: redis:7-alpinevolumes:- redis_data:/var/lib/redis/datacommand: redis-server --appendonly yeshealthcheck:test: ["CMD", "redis-cli", "ping"]interval: 5stimeout: 5sretries: 5restart: unless-stopped# MinIO (S3-compatible file storage)minio:image: minio/minio:latestvolumes:- minio_data:/dataenvironment:MINIO_ROOT_USER: ${STORAGE_ACCESS_KEY:-minioadmin}MINIO_ROOT_PASSWORD: ${STORAGE_SECRET_KEY:-minioadmin}command: server /data --console-address ":9001"ports:- "9000:9000"- "9001:9001"restart: unless-stoppedvolumes:postgres_data:redis_data:minio_data:
Note: Dokploy uses Traefik as a reverse proxy. You do not need Caddy or Nginx — Dokploy handles routing and SSL automatically. The ports are exposed so Dokploy can route traffic to each service.
Build Context & Monorepo
Notice that the API service uses context: ./apps/api because the Go module is self-contained. The Next.js services use context: . (repo root) because they need access to the shared packages/shared, the root pnpm-lock.yaml, and pnpm-workspace.yaml.
Dockerfiles
Grit scaffolds production-ready Dockerfiles with grit new. Here's what each one looks like:
Go API Dockerfile
Multi-stage build — compiles a static Go binary, then copies it into a minimal Alpine image (~15 MB final):
# Build stageFROM golang:1.23-alpine AS builderWORKDIR /app# Copy go mod filesCOPY go.mod go.sum ./RUN go mod download# Copy source codeCOPY . .# Build static binaryRUN CGO_ENABLED=0 GOOS=linux go build -o /app/server ./cmd/server# Run stageFROM alpine:3.19RUN apk --no-cache add ca-certificates tzdataWORKDIR /appCOPY --from=builder /app/server .EXPOSE 8080CMD ["./server"]
Next.js Dockerfile (Web & Admin)
Three-stage build — installs deps, builds with standalone output, then creates a minimal runner (~120 MB final). The same pattern is used for both apps/web and apps/admin:
# Build stageFROM node:20-alpine AS baseRUN corepack enable# Install dependenciesFROM base AS depsWORKDIR /appCOPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./COPY apps/web/package.json ./apps/web/COPY packages/shared/package.json ./packages/shared/RUN pnpm install --frozen-lockfile# BuildFROM base AS builderWORKDIR /appCOPY --from=deps /app/node_modules ./node_modulesCOPY --from=deps /app/apps/web/node_modules ./apps/web/node_modulesCOPY --from=deps /app/packages/shared/node_modules ./packages/shared/node_modulesCOPY . .RUN pnpm --filter web build# RunFROM base AS runnerWORKDIR /appENV NODE_ENV=productionRUN addgroup --system --gid 1001 nodejsRUN adduser --system --uid 1001 nextjsCOPY --from=builder /app/apps/web/.next/standalone ./COPY --from=builder /app/apps/web/.next/static ./apps/web/.next/staticCOPY --from=builder /app/apps/web/public ./apps/web/publicUSER nextjsEXPOSE 3000ENV PORT=3000ENV HOSTNAME="0.0.0.0"CMD ["node", "apps/web/server.js"]
Critical: The Next.js Dockerfiles depend on output: "standalone" being set in your next.config.ts. Grit sets this automatically for both web and admin apps. Without it, .next/standalone won't be generated and the Docker build will fail. If you removed it, add it back:
const nextConfig: NextConfig = {output: "standalone", // Required for Docker buildsreactStrictMode: true,};
How standalone works: With output: "standalone", Next.js creates a self-contained server.js that includes only the required node_modules files — reducing the Docker image from ~1 GB to ~120 MB. The runner stage only needs the standalone, static, and public folders — no full node_modules.
Environment Variables
In the Dokploy dashboard, navigate to your Compose service and click the "Environment" tab. Copy the template below, paste it in, and fill in your values. Every variable has a descriptive placeholder — replace anything that says CHANGE_THIS or YOUR_.
# ═══════════════════════════════════════════════════════════════# GRIT CLOUD — Production Environment (Dokploy)# ═══════════════════════════════════════════════════════════════# AppAPP_NAME=gritcloudAPP_ENV=productionAPP_PORT=8080APP_URL=https://api.yourdomain.com# ─── Database (Docker Postgres service in Dokploy) ───────────# Create a Postgres service in Dokploy, then use the internal hostname.# Format: postgres://USER:PASSWORD@SERVICE_NAME:5432/DB_NAME?sslmode=disableDATABASE_URL=postgres://gritcloud:CHANGE_THIS_STRONG_PASSWORD@gritcloud-postgres:5432/gritcloud?sslmode=disable# ─── Redis (Docker Redis service in Dokploy) ─────────────────# Create a Redis service in Dokploy, then use the internal hostname.REDIS_URL=redis://gritcloud-redis:6379# ─── JWT Authentication ──────────────────────────────────────# Generate with: openssl rand -hex 32JWT_SECRET=CHANGE_THIS_GENERATE_WITH_OPENSSL_RAND_HEX_32JWT_ACCESS_EXPIRY=15mJWT_REFRESH_EXPIRY=168h# ─── Storage (Cloudflare R2) ─────────────────────────────────STORAGE_DRIVER=r2R2_ENDPOINT=https://YOUR_CF_ACCOUNT_ID.r2.cloudflarestorage.comR2_ACCESS_KEY=YOUR_R2_ACCESS_KEY_IDR2_SECRET_KEY=YOUR_R2_SECRET_ACCESS_KEYR2_BUCKET=gritcloud-uploadsR2_REGION=autoR2_PUBLIC_URL=https://pub-XXXX.r2.dev# ─── OAuth2 (Google + GitHub) ────────────────────────────────GOOGLE_CLIENT_ID=YOUR_GOOGLE_CLIENT_IDGOOGLE_CLIENT_SECRET=YOUR_GOOGLE_CLIENT_SECRETGITHUB_CLIENT_ID=YOUR_GITHUB_CLIENT_IDGITHUB_CLIENT_SECRET=YOUR_GITHUB_CLIENT_SECRETGITHUB_WEBHOOK_SECRET=YOUR_GITHUB_WEBHOOK_SECRETOAUTH_FRONTEND_URL=https://cloud.yourdomain.com# ─── Email (Resend) ──────────────────────────────────────────RESEND_API_KEY=re_YOUR_RESEND_API_KEYMAIL_FROM=noreply@yourdomain.com# ─── CORS (all frontend origins) ─────────────────────────────CORS_ORIGINS=https://yourdomain.com,https://cloud.yourdomain.com,https://ai.yourdomain.com# ─── AI Provider ─────────────────────────────────────────────AI_PROVIDER=claudeAI_API_KEY=sk-ant-YOUR_CLAUDE_API_KEYAI_MODEL=claude-sonnet-4-5-20250929# ─── Stripe Billing ──────────────────────────────────────────STRIPE_SECRET_KEY=sk_live_YOUR_STRIPE_SECRET_KEYSTRIPE_PUBLISHABLE_KEY=pk_live_YOUR_STRIPE_PUBLISHABLE_KEYSTRIPE_WEBHOOK_SECRET=whsec_YOUR_STRIPE_WEBHOOK_SECRETSTRIPE_PRICE_STARTER=price_YOUR_STARTER_PRICE_IDSTRIPE_PRICE_PRO=price_YOUR_PRO_PRICE_IDSTRIPE_PRICE_BUSINESS=price_YOUR_BUSINESS_PRICE_ID# ─── GORM Studio (DB browser — disable or secure in prod) ───GORM_STUDIO_ENABLED=falseGORM_STUDIO_USERNAME=adminGORM_STUDIO_PASSWORD=CHANGE_THIS_STRONG_PASSWORD# ─── Pulse (Performance monitoring) ──────────────────────────PULSE_ENABLED=truePULSE_USERNAME=adminPULSE_PASSWORD=CHANGE_THIS_STRONG_PASSWORD# ─── Sentinel (WAF / Rate limiting) ──────────────────────────SENTINEL_ENABLED=trueSENTINEL_USERNAME=adminSENTINEL_PASSWORD=CHANGE_THIS_STRONG_PASSWORDSENTINEL_SECRET_KEY=CHANGE_THIS_GENERATE_WITH_OPENSSL_RAND_HEX_32# ─── Next.js Frontend Build-time Vars ────────────────────────# These are needed when building the Next.js apps (cloud, ai, web)NEXT_PUBLIC_API_URL=https://api.yourdomain.comNEXT_PUBLIC_APP_URL=https://yourdomain.comNEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_YOUR_STRIPE_PUBLISHABLE_KEY
Important: Notice that database and Redis hostnames use Docker service names (e.g., postgres, redis, minio) instead of localhost. Docker Compose networking resolves service names automatically.
Configure Domains
Dokploy uses Traefik to route domains to services and automatically provisions SSL certificates via Let's Encrypt. For each service, go to the "Domains" tab and add a domain:
| Service | Domain | Container Port |
|---|---|---|
| api | api.yourdomain.com | 8080 |
| web | yourdomain.com | 3000 |
| admin | admin.yourdomain.com | 3000 |
| minio (optional) | files.yourdomain.com | 9001 |
For each domain in Dokploy:
- 1Click "Add Domain" on the service
- 2Enter the domain (e.g., api.yourdomain.com)
- 3Set HTTPS to "Let's Encrypt" for automatic SSL
- 4Set the container port (the port your service listens on inside the container)
- 5Save and deploy
SSL note: Let's Encrypt certificates take 1-2 minutes to provision. If you see a "Not Secure" warning immediately after deploying, wait a moment and refresh. The certificate will be issued automatically once DNS is propagated.
DNS Configuration
In your domain registrar or DNS provider (Cloudflare, Namecheap, etc.), create A records pointing to your VPS IP address:
| Type | Name | Value | Purpose |
|---|---|---|---|
| A | @ | YOUR_VPS_IP | Web app (yourdomain.com) |
| A | api | YOUR_VPS_IP | Go API (api.yourdomain.com) |
| A | admin | YOUR_VPS_IP | Admin panel (admin.yourdomain.com) |
| A | files | YOUR_VPS_IP | MinIO console (optional) |
Cloudflare users: If you're using Cloudflare, set the proxy status to "DNS only" (grey cloud) for Let's Encrypt to work. If you want Cloudflare's proxy (orange cloud), set the SSL mode to "Full (Strict)" in Cloudflare and configure Dokploy to use Cloudflare-issued certificates instead of Let's Encrypt.
Deploy
Once your environment variables and domains are configured, hit the "Deploy" button in the Dokploy dashboard. Dokploy will:
- 1Pull your code from GitHub
- 2Build all Docker images (Go API + Next.js apps)
- 3Start all services defined in your Compose file
- 4Configure Traefik routes for your domains
- 5Provision SSL certificates via Let's Encrypt
The first build takes longer (5-10 minutes) because Docker needs to download base images and build from scratch. Subsequent deploys are faster thanks to Docker layer caching.
Auto-Deploy on Git Push
To enable automatic deployments when you push to GitHub:
- 1Go to your Compose service in Dokploy
- 2Click the "General" tab
- 3Enable "Auto Deploy" and select your branch (e.g., main)
- 4Dokploy will set up a GitHub webhook automatically
Now every git push origin main will trigger a new deployment automatically.
Database Backups
Dokploy has built-in backup support for database services. Alternatively, you can run manual backups using pg_dump:
Manual Backup
Automated Backups with Cron
Set up a cron job on your server to back up daily:
# Daily backup at 3 AM — keeps 7 days of backups0 3 * * * docker exec -t $(docker ps --filter name=postgres -q) pg_dumpall -c -U postgres | gzip > /backups/grit_$(date +\%Y\%m\%d).sql.gz && find /backups -name "grit_*.sql.gz" -mtime +7 -delete
Monitoring
Dokploy provides built-in monitoring through its dashboard. On top of that, Grit includes additional observability tools:
| Tool | Access | What It Shows |
|---|---|---|
| Dokploy Dashboard | http://your-ip:3000 | Container logs, CPU/memory usage, deployment history |
| Pulse | api.yourdomain.com/pulse | Request tracing, DB metrics, error tracking, alerting |
| Sentinel | api.yourdomain.com/sentinel/ui | Security threats, blocked IPs, rate limiting |
| GORM Studio | api.yourdomain.com/studio | Database browser, SQL editor, data export |
| API Docs | api.yourdomain.com/docs | Auto-generated OpenAPI spec + interactive UI |
Viewing Logs
Click any service in the Dokploy dashboard to see real-time logs. You can also view logs from the command line:
Tips & Troubleshooting
Build fails with "out of memory"
Next.js builds are memory-intensive. Make sure your VPS has at least 4GB RAM. You can also add swap space: sudo fallocate -l 2G /swapfile && sudo chmod 600 /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile
SSL certificate not provisioning
Make sure your DNS A records are pointing to the correct VPS IP and have propagated (use dig yourdomain.com to verify). If using Cloudflare, set the proxy to "DNS only" (grey cloud). Let's Encrypt needs to reach your server directly.
Port 3000 already in use
Dokploy's dashboard uses port 3000 by default. If your web app also uses port 3000, they won't conflict because the web app runs inside a Docker container with its own network. Dokploy routes traffic based on domain, not port.
Database connection refused
Make sure DATABASE_URL uses the Docker service name (postgres) instead of localhost. Inside Docker Compose, services communicate using service names as hostnames.
How do I SSH into a running container?
Use the Dokploy dashboard's "Terminal" tab, or from the command line: docker exec -it CONTAINER_NAME /bin/sh
How do I redeploy without code changes?
Click the "Redeploy" button in the Dokploy dashboard, or use the Dokploy API webhook URL to trigger a deployment programmatically.
MinIO bucket not found
MinIO doesn't auto-create buckets. After deployment, access the MinIO console (port 9001), sign in with your credentials, and create the bucket manually. Or add a startup script that uses the mc client.
Production checklist: Before going live, make sure you've changed all default passwords (JWT secrets, database password, MinIO keys, Sentinel/Pulse passwords), enabled HTTPS for all domains, and set up automated database backups. See the full production checklist.