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
| Service | Idle RAM | Peak (200 concurrent submissions) |
|---|---|---|
| formbricks | 530 MB | 840 MB |
| db (postgres 16) | 155 MB | 200 MB |
| caddy | 15 MB | 20 MB |
| Total | 700 MB | 1,060 MB |
Cost vs Typeform
| Typeform Plus | Typeform Business | Formbricks on Liquid Web | |
|---|---|---|---|
| Monthly cost | $50/mo | $83/mo | ~$14/mo VPS |
| Responses/mo | 1,000 | 10,000 | Unlimited |
| Custom branding | ✓ | ✓ | Community (no logo removal) |
| Webhooks | ✓ | ✓ | ✓ |
| SSO | ✗ | ✗ | Enterprise 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.
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.



