Getting Started
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
The repo ships a docker-compose.yml that starts both the Next.js frontend and PocketBase backend together. Clone the repo, copy the env example, and you're one command away from a running instance.
git clone https://github.com/muharremyurtsever/velocms.git
cd velocms
cp .env.example .env.local
# Edit .env.local with your values (see below)
docker compose upRequired 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:
# Core
VELOCMS_MODE=single # or "multi" for multi-tenant SaaS mode
POCKETBASE_URL=http://localhost:8090
POCKETBASE_ADMIN_EMAIL=[email protected]
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=[email protected]
# 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_...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.
| Feature | single | multi |
|---|---|---|
| PocketBase instances | 1 | 1 master + N tenant |
| Tenant isolation | none (single blog) | full DB isolation |
| Custom domains | no | yes (Cloudflare for SaaS) |
| Stripe billing | optional | per-tenant BYOK or platform Stripe |
| Best for | personal blog, agency site | SaaS 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.
# 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.devWildcard 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).
# 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.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.
# 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/loginDeploying to Railway
Railway is the recommended production host — it handles Docker builds, zero-downtime deploys, automatic TLS, and custom domains out of the box. The repo includes a railway.json that wires both services (Next.js and PocketBase) into a single project.
# 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)
# Deploy
git push origin main # Railway auto-deploys on pushThat'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.