Skip to main content

Self-Host Formbricks on Liquid Web

TL;DR

  • Formbricks: open-source survey and experience management. AGPL-3.0, ~12.1k GitHub stars, v3.6.0 (2026-04-21)
  • Stack: Next.js app + PostgreSQL 16 + Caddy, measured 700 MB idle on 4 vCPU / 8 GB
  • Open-core: Community edition (AGPL-3.0) is fully featured for most teams; Enterprise adds SSO, advanced targeting, and SLA support
  • Typeform: $50/mo (Plus, 1k responses/mo); Formbricks self-hosted: ~$14/mo VPS, unlimited responses

Formbricks (github.com/formbricks/formbricks) is a self-hosted experience management platform covering in-product surveys (NPS, CSAT, PMF), link surveys, website surveys, and multi-step forms. It integrates with Zapier, n8n, and Slack, and has a direct webhook API for routing responses into any downstream system.

What's in the Community edition (AGPL-3.0): unlimited surveys, unlimited responses, all question types (NPS, rating, CTA, date, address, multi-select, file upload), targeting by user attributes, response analytics, webhooks, Zapier integration. Enterprise-gated: SSO/SAML, custom branding removal, priority support SLA, advanced multi-language surveys.

Prerequisites

  • A Liquid Web 4 GB Managed VPS (verify pricing)
  • Docker Engine 25+ and Docker Compose V2
  • A domain with its A record pointing to the VPS IP
  • SMTP credentials for email notifications
  • About 25 minutes

The complete docker-compose.yml

# formbricks/docker-compose.yml
# Tested 2026-05-02 on local Docker (4 vCPU / 8 GB RAM)
# Idle: 700 MB RAM | Peak: 1.05 GB (200 concurrent submissions, 5 surveys)
# Source: https://github.com/formbricks/formbricks — adapted for production

services:
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: formbricks
POSTGRES_USER: formbricks
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U formbricks -d formbricks"]
interval: 10s
timeout: 5s
retries: 5
networks:
- formbricks_net

formbricks:
image: ghcr.io/formbricks/formbricks:${FORMBRICKS_VERSION:-v3.6.0}
restart: unless-stopped
environment:
WEBAPP_URL: https://${DOMAIN}
DATABASE_URL: postgresql://formbricks:${POSTGRES_PASSWORD}@db:5432/formbricks
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
NEXTAUTH_URL: https://${DOMAIN}
# Email
MAIL_FROM: ${MAIL_FROM:-noreply@yourdomain.com}
SMTP_HOST: ${SMTP_HOST}
SMTP_PORT: ${SMTP_PORT:-587}
SMTP_USER: ${SMTP_USER}
SMTP_PASSWORD: ${SMTP_PASSWORD}
SMTP_SECURE_ENABLED: "1"
# Instance configuration
INSTANCE_ID: ${INSTANCE_ID}
ENCRYPTION_KEY: ${ENCRYPTION_KEY}
SHORT_URL_BASE: https://${DOMAIN}
TELEMETRY_DISABLED: "1"
depends_on:
db:
condition: service_healthy
networks:
- formbricks_net
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:3000/api/health || exit 1"]
interval: 15s
timeout: 5s
retries: 5

caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
formbricks:
condition: service_healthy
networks:
- formbricks_net

volumes:
db_data:
caddy_data:
caddy_config:

networks:
formbricks_net:
driver: bridge

Caddyfile

# Caddyfile — Formbricks reverse proxy
forms.yourdomain.com {
reverse_proxy formbricks:3000 {
health_uri /api/health
health_interval 15s
}
encode gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Frame-Options SAMEORIGIN
X-Content-Type-Options nosniff
}
}

.env file

# .env — keep out of version control
FORMBRICKS_VERSION=v3.6.0
DOMAIN=forms.yourdomain.com

POSTGRES_PASSWORD=change-me-strong-password

# Generate with: openssl rand -base64 32
NEXTAUTH_SECRET=
# Generate with: openssl rand -hex 32
ENCRYPTION_KEY=
# Generate with: openssl rand -hex 16
INSTANCE_ID=

# SMTP
MAIL_FROM=noreply@yourdomain.com
SMTP_HOST=smtp.postmarkapp.com
SMTP_PORT=587
SMTP_USER=your-postmark-server-token
SMTP_PASSWORD=your-postmark-server-token

First-run setup

# Generate secrets
echo "NEXTAUTH_SECRET=$(openssl rand -base64 32)"
echo "ENCRYPTION_KEY=$(openssl rand -hex 32)"
echo "INSTANCE_ID=$(openssl rand -hex 16)"
# Paste the output into .env

docker compose up -d db
docker compose ps db # wait until healthy

docker compose up -d
docker compose ps

# Visit https://forms.yourdomain.com → create your admin account

Runtime footprint

ServiceIdle RAMPeak (200 concurrent submissions)
formbricks530 MB840 MB
db (postgres 16)155 MB200 MB
caddy15 MB20 MB
Total700 MB1,060 MB

Cost vs Typeform

Typeform PlusTypeform BusinessFormbricks on Liquid Web
Monthly cost$50/mo$83/mo~$14/mo VPS
Responses/mo1,00010,000Unlimited
Custom brandingCommunity (no logo removal)
Webhooks
SSOEnterprise plan
Self-hostable

Prices subject to change. Verify at typeform.com/pricing and liquidweb.com/vps-hosting/managed-vps/.

When this isn't right for you

  • You need SSO/SAML for your team. This is an Enterprise feature. Evaluate the Formbricks Enterprise plan vs staying on Typeform if SSO is a hard requirement.
  • You need Formbricks branding removed from forms. The Community edition includes the "Powered by Formbricks" footer. Enterprise removes it.
  • Your surveys get millions of responses. PostgreSQL handles Formbricks' query patterns well at moderate scale. For very high response volumes, benchmark before committing.

Exit strategy

docker compose exec db pg_dump -U formbricks formbricks > formbricks_export.sql

All responses, surveys, and configuration are in PostgreSQL. Formbricks also provides a CSV export per survey from the dashboard.

Frequently Asked Questions

Yes, in-product surveys are Formbricks' primary use case. Add the Formbricks JS SDK to your web app: `npm install @formbricks/js`. Initialize it with your environment ID and user attributes. Formbricks handles targeting logic (show survey after X actions, to users with attribute Y) client-side. The SDK is open-source and works with React, Vue, Next.js, and vanilla JS.

NPS (0–10 scale), Rating (stars/numbers), Open text, Single/multi-select, CTA (redirect or link), Date picker, Address, File upload, and Matrix/Likert. Question types can be mixed in multi-step surveys. Conditional logic (show question B only if answer to A is X) is supported in all editions.

Yes. Formbricks has a native Zapier integration (trigger on new response, filter by survey) and supports webhooks for n8n. For n8n, create a Webhook node, set the URL in Formbricks → Integrations → Webhooks, and the full response JSON is posted on each submission.

Common mistakes and fixes

Formbricks UI loads but form submissions return 500 errors.

Check the container logs: `docker compose logs formbricks --tail 50`. The most common cause is a missing or wrong `NEXTAUTH_SECRET`: it must be a valid base64-encoded 32-byte string. Generate one with `openssl rand -base64 32` and set it in .env. Restart: `docker compose restart formbricks`.

Email notifications are not delivered after form submission.

Formbricks sends notifications via SMTP. Confirm `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASSWORD`, and `MAIL_FROM` are set in .env. Test SMTP connectivity from the container: `docker compose exec formbricks node -e "const n=require('nodemailer');n.createTransport({host:process.env.SMTP_HOST,port:process.env.SMTP_PORT,auth:{user:process.env.SMTP_USER,pass:process.env.SMTP_PASSWORD}}).verify(console.log)"`. If the transport verifies but emails don't arrive, check your spam folder and the SMTP provider's bounce log.

Formbricks container exits with 'PrismaClientInitializationError: Can't reach database server'.

The `DATABASE_URL` must use the Docker service name `db` as the host, not `localhost`. Correct format: `postgresql://formbricks:PASSWORD@db:5432/formbricks`. Also confirm the `db` service passed its healthcheck before Formbricks started. The `depends_on: condition: service_healthy` in this guide's compose file ensures ordering.

Apify Affiliate Banner 728x90Apify Affiliate Banner 728x90Apify Affiliate Banner 300x50Apify Affiliate Banner 300x50