Getting Started

10 min readUpdated 27 Apr 2026

VeloCMS runs anywhere Docker runs — a $6/mo VPS, a Railway service, or your laptop. This guide walks you through the full self-hosting path: Docker Compose setup, environment variables, Cloudflare R2 for media, wildcard DNS, and creating your first admin account. By the end you'll have a running instance you can actually log into.

Prerequisites

  • Docker 24+ and Docker Compose v2
  • A domain name (or subdomain) you control
  • A Cloudflare account (free tier works) for R2 media storage
  • Optional but recommended: a Railway account for managed hosting

Docker Compose quickstart

VeloCMS ships pre-built Docker images — you don't need the source code to run it. Three commands and you have a live instance: download the Compose template, fill in your secrets, and pull the images.

terminal
# 1. Create a working directory
mkdir velocms && cd velocms

# 2. Download the docker-compose template and env example
curl -O https://velocms.org/install/docker-compose.yml
curl -O https://velocms.org/install/.env.example
cp .env.example .env
# Edit .env with your secrets (POCKETBASE_ADMIN_PASSWORD, ENCRYPTION_KEY, etc.)

# 3. Pull the images and start
docker compose up -d
bash

Required environment variables

VeloCMS reads all configuration from environment variables — no config files, no magic. The .env.example in the root covers every required and optional variable. Here are the ones you must set before first launch:

.env.local
# Core
VELOCMS_MODE=single                # or "multi" for multi-tenant SaaS mode
POCKETBASE_URL=http://localhost:8090
POCKETBASE_ADMIN_EMAIL=admin@example.com
POCKETBASE_ADMIN_PASSWORD=changeme123

# Public site URL (used for sitemap, OG images, canonical links)
NEXT_PUBLIC_SITE_URL=https://yourdomain.com
NEXT_PUBLIC_PLATFORM_DOMAIN=yourdomain.com

# Encryption key — 64 hex characters (32 bytes). Generate with:
# openssl rand -hex 32
ENCRYPTION_KEY=<64-char-hex>

# Cloudflare R2 media storage
CLOUDFLARE_ACCOUNT_ID=<your-account-id>
CLOUDFLARE_API_TOKEN=<your-api-token>
R2_BUCKET_NAME=velocms-media
R2_PUBLIC_URL=https://pub-<hash>.r2.dev

# Email (Resend)
RESEND_API_KEY=re_...
RESEND_FROM_EMAIL=noreply@yourdomain.com

# AI writing assistant (optional — editor AI features require this)
GEMINI_API_KEY=AI...

# Billing — only required for membership/paywall features
STRIPE_SECRET_KEY=sk_live_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
bash

Single-instance vs multi-tenant mode

VeloCMS ships in two modes controlled by a single env var. VELOCMS_MODE=single is what you want for a personal blog or a small agency running one site — everything lives in one PocketBase instance, dead simple. VELOCMS_MODE=multi is the SaaS mode: a master PocketBase handles auth and billing, and each tenant gets their own isolated PocketBase with their own content. Multi-mode is what velocms.org itself runs in production.

Featuresinglemulti
PocketBase instances11 master + N tenant
Tenant isolationnone (single blog)full DB isolation
Custom domainsnoyes (Cloudflare for SaaS)
Stripe billingoptionalper-tenant BYOK or platform Stripe
Best forpersonal blog, agency siteSaaS platform, blog hosting

Setting up Cloudflare R2

R2 is where all uploaded media lives — images, videos, file attachments. It's S3-compatible and has zero egress fees, which matters when you're hosting a photo-heavy blog. You'll create a bucket, generate an API token with object write permissions, and grab the public URL.

Cloudflare R2 setup steps
# 1. Log into https://dash.cloudflare.com → R2 Object Storage
# 2. Create a bucket: "velocms-media" (or any name you prefer)
# 3. Under bucket settings, enable Public Access → note the r2.dev URL
# 4. Create an API Token with:
#    - Object Read & Write permissions on your bucket
#    - Copy the token value — you only see it once
# 5. Set in .env.local:
CLOUDFLARE_ACCOUNT_ID=<from Cloudflare dashboard top-right>
CLOUDFLARE_API_TOKEN=<the token you just copied>
R2_BUCKET_NAME=velocms-media
R2_PUBLIC_URL=https://pub-<hash>.r2.dev
bash

Wildcard DNS setup

If you're running in multi-tenant mode, each tenant gets their own subdomain (e.g. alice.yourdomain.com). You'll need a wildcard DNS record pointing to your server, and Cloudflare configured to not proxy it (grey cloud). The apex domain stays orange cloud (Cloudflare-proxied).

Cloudflare DNS records
# Add these two records in Cloudflare DNS:
Type   Name             Content               Proxy status
A      yourdomain.com   <your-server-IP>      Proxied (orange cloud)
A      *.yourdomain.com <your-server-IP>      DNS only (grey cloud)

# The wildcard MUST be grey cloud — Cloudflare's proxy doesn't support
# wildcard certificates on free plans, and Railway handles TLS for subdomains.
bash

Creating your first admin account

Once Docker Compose is running, PocketBase exposes its admin UI at port 8090. The very first time you visit it, you'll be prompted to create a superuser — this is your admin account. After that, you can log into VeloCMS at /login with those credentials.

terminal
# Navigate to PocketBase admin (while Docker is running):
open http://localhost:8090/_/

# Create your superuser email + password when prompted.
# Then visit the VeloCMS admin panel:
open http://localhost:3000/login
bash

Deploying to Railway

Railway is the recommended production host — it handles Docker builds, zero-downtime deploys, automatic TLS, and custom domains out of the box. Once the public repo is available (Q3 2026 at github.com/VeloCMS), Railway auto-deploys from git push. Until then, use the Railway Docker registry directly or contact hello@velocms.org for managed hosting.

terminal
# Install Railway CLI
npm install -g @railway/cli

# Login and link to your project
railway login
railway link

# Set production env vars (do this before the first deploy)
railway variables --set "VELOCMS_MODE=multi" --skip-deploys
railway variables --set "POCKETBASE_URL=https://your-pb.up.railway.app" --skip-deploys
railway variables --set "NEXT_PUBLIC_SITE_URL=https://yourdomain.com" --skip-deploys
# ... (set all other vars from .env.example / public/install/.env.example)

# Deploy — Railway auto-deploys from the VeloCMS Docker image
railway up
bash

That's it. Once the first deploy finishes, visit your Railway service URL to confirm everything is live. Check the build logs if anything looks wrong — Railway's log viewer is genuinely useful for tracing startup errors.

Reference

Environment variables

VariableRequiredDefaultDescription
VELOCMS_MODEYessinglesingle = one blog, multi = SaaS multi-tenant
POCKETBASE_URLYeshttp://localhost:8090PocketBase API base URL (internal in Docker)
POCKETBASE_ADMIN_EMAILYesPocketBase superuser email
POCKETBASE_ADMIN_PASSWORDYesPocketBase superuser password
NEXT_PUBLIC_SITE_URLYeshttp://localhost:3000Public URL — used in sitemap, OG images, canonical links
NEXT_PUBLIC_PLATFORM_DOMAINYes (multi)velocms.orgApex domain for subdomain tenant routing
ENCRYPTION_KEYYes64-char hex (openssl rand -hex 32). Encrypts tenant API keys.
CLOUDFLARE_ACCOUNT_IDYesCloudflare account ID for R2 presigned uploads
CLOUDFLARE_API_TOKENYesR2-scoped API token (Object Read & Write)
R2_BUCKET_NAMEYesvelocms-mediaCloudflare R2 bucket name
R2_PUBLIC_URLYesPublic r2.dev or custom domain URL for media files
RESEND_API_KEYYesResend send-only API key (re_...)
RESEND_FROM_EMAILYesFrom address for transactional email
GEMINI_API_KEYNoGoogle Gemini API key. Required for editor AI features.
STRIPE_SECRET_KEYNoStripe restricted key (sk_live_...). Required for membership billing.
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEYNoStripe publishable key (pk_live_...)
STRIPE_WEBHOOK_SECRETNoStripe webhook signing secret (whsec_...)
STRIPE_PRICE_PRONoStripe price ID for Pro plan
STRIPE_PRICE_BUSINESSNoStripe price ID for Business plan
STRIPE_PRICE_AGENCYNoStripe price ID for Agency plan
CLOUDFLARE_ZONE_IDNo (multi)CF zone for custom domain routing (Pro+ tenants)
CLOUDFLARE_FALLBACK_ORIGINNo (multi)Fallback origin hostname for CF for SaaS
NEXT_PUBLIC_SENTRY_DSNNoSentry DSN for error monitoring
PREVIEW_SECRETNoDraft mode preview token (openssl rand -hex 16)
CRON_SECRETNoProtects /api/cron/* endpoints (openssl rand -hex 32)
BETA_WAITLIST_ENABLEDNoSet true to gate signups behind waitlist
BETA_INVITE_CODESNoCSV of invite codes that bypass waitlist
UNSPLASH_ACCESS_KEYNoUnsplash API key for onboarding featured image

Install path summary

StepCommandNotes
1. Get templatecurl -O https://velocms.org/install/docker-compose.ymlSanitized Docker Compose for self-hosters
2. Get env templatecurl -O https://velocms.org/install/.env.example && cp .env.example .envFill in required vars before starting
3. Start servicesdocker compose up -dStarts VeloCMS on :3000 + PocketBase on :8090
4. Create adminopen http://localhost:8090/_/Create superuser on first visit
5. Log inopen http://localhost:3000/loginSign in with superuser credentials
6. Set domainEdit NEXT_PUBLIC_SITE_URL in .env + restartRequired for sitemap and email links

Single-instance vs multi-tenant

Featuresinglemulti
PocketBase instances11 master + N tenant
Tenant isolationnone (single blog)full DB isolation per tenant
Custom domainsnoyes (Cloudflare for SaaS, Pro+ plan)
Stripe billingoptional (BYOK)per-tenant BYOK or platform Stripe
Wildcard DNS needednoyes (*.yourdomain.com)
Best forpersonal blog, agency siteSaaS platform, multi-blog hosting
Additional env varsMASTER_POCKETBASE_URL, NEXT_PUBLIC_PLATFORM_DOMAIN, CLOUDFLARE_ZONE_ID

Docker image reference

ImageTagContents
velocms/velocmslatestNext.js 16 app + all API routes + edge middleware
velocms/velocmsx.y.zPinned release — recommended for production
velocms/pocketbaselatestPocketBase 0.36 with VeloCMS schema pre-loaded
velocms/pocketbasex.y.zPinned release matching the app image version