Appearance
Self-Hosting FormDSL
Deploy your own FormDSL server for submissions, auto-save, dashboards, and file uploads.
Requirements
- PostgreSQL 15+
- S3-compatible storage (AWS S3, Cloudflare R2, Railway Buckets, MinIO, etc.)
- Docker (recommended) or Bun 1.x runtime
Docker Compose (recommended)
Create a docker-compose.yml:
yaml
services:
postgres:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_DB: formdsl
POSTGRES_USER: formdsl
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U formdsl"]
interval: 5s
timeout: 3s
retries: 5
formdsl:
image: formdsl/server
restart: unless-stopped
ports:
- "3000:3000"
depends_on:
postgres:
condition: service_healthy
environment:
DATABASE_URL: postgresql://formdsl:${POSTGRES_PASSWORD}@postgres:5432/formdsl
PORT: 3000
NODE_ENV: production
ENCRYPTION_KEY: ${ENCRYPTION_KEY}
ADMIN_EMAIL: ${ADMIN_EMAIL}
ADMIN_PASSWORD: ${ADMIN_PASSWORD}
APP_URL: ${APP_URL}
DASH_URL: ${APP_URL}
CORS_ORIGIN: ${CORS_ORIGIN}
S3_ENDPOINT: ${S3_ENDPOINT}
S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID}
S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY}
S3_BUCKET: ${S3_BUCKET:-formdsl-attachments}
volumes:
pgdata:Create a .env file:
bash
POSTGRES_PASSWORD=a-strong-password
ENCRYPTION_KEY= # Generate with: openssl rand -hex 32
ADMIN_EMAIL=you@example.com
ADMIN_PASSWORD=changeme
APP_URL=https://forms.yoursite.com
CORS_ORIGIN=https://yoursite.com
# S3 storage
S3_ENDPOINT=https://your-s3-endpoint.com
S3_ACCESS_KEY_ID=your-key
S3_SECRET_ACCESS_KEY=your-secret
S3_BUCKET=formdsl-attachmentsStart everything:
bash
docker compose up -dThe server auto-runs database migrations and creates the admin account on first boot.
Railway
Railway is the fastest path to a running instance.
1. Create a new project
In Railway, create a new project and add:
- PostgreSQL service (from the Railway marketplace)
- Bucket service (for file uploads, from the Railway marketplace)
- Docker service (from your repo or the Docker image)
2. Configure the Docker service
Point the Docker service at the Dockerfile in the repo root (or use the published image). Set these environment variables:
| Variable | Value |
|---|---|
DATABASE_URL | Railway provides this automatically from the Postgres add-on — use the DATABASE_URL reference variable |
PORT | 3000 (or let Railway auto-detect) |
NODE_ENV | production |
ENCRYPTION_KEY | Generate: openssl rand -hex 32 |
ADMIN_EMAIL | Your email |
ADMIN_PASSWORD | A strong password |
APP_URL | Your Railway public URL (e.g., https://formdsl-production.up.railway.app) |
DASH_URL | Same as APP_URL |
CORS_ORIGIN | Your frontend origin(s), comma-separated |
S3_ENDPOINT | Railway Bucket endpoint |
S3_ACCESS_KEY_ID | Railway Bucket access key |
S3_SECRET_ACCESS_KEY | Railway Bucket secret key |
S3_BUCKET | Your bucket name |
3. Deploy
Push to your repo or trigger a manual deploy. Railway builds the Dockerfile automatically. The server starts, runs migrations, and is ready.
4. Verify
Visit your Railway URL — you should see the dashboard login page. Log in with your admin credentials.
Environment variables reference
Required
| Variable | Description |
|---|---|
DATABASE_URL | PostgreSQL connection string |
ENCRYPTION_KEY | 64 hex chars (32 bytes). Generate: openssl rand -hex 32 |
Recommended
| Variable | Default | Description |
|---|---|---|
PORT | 3000 | Server port |
NODE_ENV | development | Set production for strict cookies, CORS, SPA serving |
ADMIN_EMAIL | — | Auto-creates admin account on first boot |
ADMIN_PASSWORD | — | Password for the auto-created admin |
APP_URL | http://localhost:3000 | Public URL of the server (used in emails, webhooks) |
DASH_URL | http://localhost:5174 | Dashboard URL (same as APP_URL in production) |
CORS_ORIGIN | — | Comma-separated allowed origins, or * for all |
S3 storage (for file uploads)
| Variable | Default | Description |
|---|---|---|
S3_ENDPOINT | — | S3 endpoint URL |
S3_ACCESS_KEY_ID | — | S3 access key |
S3_SECRET_ACCESS_KEY | — | S3 secret key |
S3_BUCKET | formdsl-attachments | Bucket name |
File uploads require S3 configuration. Without it, the server runs normally but file fields will fail.
OAuth (optional)
| Variable | Description |
|---|---|
GITHUB_CLIENT_ID | GitHub OAuth app client ID |
GITHUB_CLIENT_SECRET | GitHub OAuth app secret |
GITHUB_REDIRECT_URI | Callback URL (e.g., https://forms.yoursite.com/api/internal/auth/oauth/github/callback) |
GOOGLE_CLIENT_ID | Google OAuth client ID |
GOOGLE_CLIENT_SECRET | Google OAuth secret |
GOOGLE_REDIRECT_URI | Google callback URL |
Email (optional)
| Variable | Description |
|---|---|
RESEND_API_KEY | Resend API key for transactional emails |
EMAIL_FROM | From address for emails |
Advanced
| Variable | Default | Description |
|---|---|---|
AUTO_MIGRATE | true | Run pending DB migrations on boot |
MULTI_TENANT | false | Enable multi-tenant workspace isolation |
DISABLE_BILLING | — | Skip Stripe integration entirely |
FORMDSL_LICENSE_KEY | — | License key for Pro features |
What the server serves
In production (NODE_ENV=production), the server serves everything from a single port:
| Path | Content |
|---|---|
/ | Dashboard SPA (built from dash/dist) |
/showcase | Showcase / demo gallery (built from showcase/dist) |
/docs | Documentation (static files from docs/) |
/f/:id | Hosted forms (rendered server-side from form definitions) |
/api/v1/* | Public API (form submissions, drafts, uploads) |
/api/internal/* | Dashboard API (forms, submissions, settings) |
Health check
GET /api/v1/healthReturns 200 OK with { "status": "ok" }.
Updating
Pull the latest image and restart:
bash
docker compose pull formdsl
docker compose up -d formdslMigrations run automatically on startup — your database schema stays up to date.
What's next
- Connected Mode — Connect your frontend forms to the server
- Getting Started — Frontend-only mode
- Laravel Integration — Use FormDSL manifests in PHP
- Python Integration — Use FormDSL manifests in Python