Skip to content

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

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-attachments

Start everything:

bash
docker compose up -d

The 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:

VariableValue
DATABASE_URLRailway provides this automatically from the Postgres add-on — use the DATABASE_URL reference variable
PORT3000 (or let Railway auto-detect)
NODE_ENVproduction
ENCRYPTION_KEYGenerate: openssl rand -hex 32
ADMIN_EMAILYour email
ADMIN_PASSWORDA strong password
APP_URLYour Railway public URL (e.g., https://formdsl-production.up.railway.app)
DASH_URLSame as APP_URL
CORS_ORIGINYour frontend origin(s), comma-separated
S3_ENDPOINTRailway Bucket endpoint
S3_ACCESS_KEY_IDRailway Bucket access key
S3_SECRET_ACCESS_KEYRailway Bucket secret key
S3_BUCKETYour 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

VariableDescription
DATABASE_URLPostgreSQL connection string
ENCRYPTION_KEY64 hex chars (32 bytes). Generate: openssl rand -hex 32
VariableDefaultDescription
PORT3000Server port
NODE_ENVdevelopmentSet production for strict cookies, CORS, SPA serving
ADMIN_EMAILAuto-creates admin account on first boot
ADMIN_PASSWORDPassword for the auto-created admin
APP_URLhttp://localhost:3000Public URL of the server (used in emails, webhooks)
DASH_URLhttp://localhost:5174Dashboard URL (same as APP_URL in production)
CORS_ORIGINComma-separated allowed origins, or * for all

S3 storage (for file uploads)

VariableDefaultDescription
S3_ENDPOINTS3 endpoint URL
S3_ACCESS_KEY_IDS3 access key
S3_SECRET_ACCESS_KEYS3 secret key
S3_BUCKETformdsl-attachmentsBucket name

File uploads require S3 configuration. Without it, the server runs normally but file fields will fail.

OAuth (optional)

VariableDescription
GITHUB_CLIENT_IDGitHub OAuth app client ID
GITHUB_CLIENT_SECRETGitHub OAuth app secret
GITHUB_REDIRECT_URICallback URL (e.g., https://forms.yoursite.com/api/internal/auth/oauth/github/callback)
GOOGLE_CLIENT_IDGoogle OAuth client ID
GOOGLE_CLIENT_SECRETGoogle OAuth secret
GOOGLE_REDIRECT_URIGoogle callback URL

Email (optional)

VariableDescription
RESEND_API_KEYResend API key for transactional emails
EMAIL_FROMFrom address for emails

Advanced

VariableDefaultDescription
AUTO_MIGRATEtrueRun pending DB migrations on boot
MULTI_TENANTfalseEnable multi-tenant workspace isolation
DISABLE_BILLINGSkip Stripe integration entirely
FORMDSL_LICENSE_KEYLicense key for Pro features

What the server serves

In production (NODE_ENV=production), the server serves everything from a single port:

PathContent
/Dashboard SPA (built from dash/dist)
/showcaseShowcase / demo gallery (built from showcase/dist)
/docsDocumentation (static files from docs/)
/f/:idHosted 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/health

Returns 200 OK with { "status": "ok" }.

Updating

Pull the latest image and restart:

bash
docker compose pull formdsl
docker compose up -d formdsl

Migrations run automatically on startup — your database schema stays up to date.

What's next