Most teams collect feedback through a patchwork of SaaS toolsโTypeform, SurveyMonkey, or a duct-taped Google Formโonly to find their response data siloed behind a vendor paywall. Formbricks is a self-hosted, open-source alternative that gives you beautiful multi-step surveys, in-app widgets, website popups, and a link survey builder, all under a single deployment you fully control. This guide walks you through a production-ready deployment of Formbricks on Ubuntu using Docker Compose, Caddy as a TLS-terminating reverse proxy, and PostgreSQL as the database backend.
Architecture and flow overview
The Formbricks stack runs as a set of coordinated Docker containers managed by Compose. Caddy sits in front, handling TLS certificate provisioning via ACME/Let's Encrypt and reverse-proxying HTTPS traffic to the Formbricks Node.js web server. PostgreSQL provides persistent relational storage for survey definitions, responses, and user accounts. An optional Redis container can be wired up later for session caching. All services communicate on an internal Docker network; only Caddy exposes ports 80 and 443 to the host.
Formbricks is built on Next.js and uses Prisma as its ORM. The application layer is stateless, which means horizontal scaling later is straightforwardโadd more Formbricks containers sharing the same PostgreSQL backend and point Caddy's upstream to a load-balanced backend group. In a single-server deployment, the three-container topology (Caddy + Formbricks + PostgreSQL) is more than sufficient to handle thousands of survey submissions per day without tuning. Each survey response goes through a REST API endpoint, is validated server-side against the survey schema stored in PostgreSQL, and is persisted atomically. File uploads (used in photo-capture question types) are stored locally or can be redirected to S3-compatible storage by setting the S3_* environment variables.
Prerequisites
- Ubuntu 22.04 or 24.04 server with a public IP and a DNS A record pointing your domain (e.g.,
forms.example.com) at it - Docker Engine 24+ and Docker Compose plugin installed
- Port 80 and 443 open in your firewall (UFW:
ufw allow 80 && ufw allow 443) - At least 2 GB RAM and 20 GB disk space
- A valid email address for ACME TLS registration
Step-by-step deployment
1. Prepare the project directory
mkdir -p /opt/formbricks && cd /opt/formbricks2. Create the environment file
cat > .env <<'EOF'
NEXTAUTH_SECRET=change_me_32_char_random_string_here
ENCRYPTION_KEY=change_me_32_char_random_string_here
NEXTAUTH_URL=https://forms.example.com
DATABASE_URL=postgresql://formbricks:formbricks_pass@db:5432/formbricks
POSTGRES_USER=formbricks
POSTGRES_PASSWORD=formbricks_pass
POSTGRES_DB=formbricks
EOFGenerate random secrets with: openssl rand -hex 32
3. Create the Docker Compose file
cat > docker-compose.yml <<'EOF'
version: "3.9"
services:
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- internal
formbricks:
image: ghcr.io/formbricks/formbricks:latest
restart: unless-stopped
depends_on:
- db
environment:
DATABASE_URL: ${DATABASE_URL}
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
NEXTAUTH_URL: ${NEXTAUTH_URL}
ENCRYPTION_KEY: ${ENCRYPTION_KEY}
expose:
- "3000"
networks:
- internal
- proxy
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
networks:
- proxy
networks:
internal:
proxy:
volumes:
pgdata:
caddy_data:
caddy_config:
EOF4. Create the Caddyfile
cat > Caddyfile <<'EOF'
forms.example.com {
reverse_proxy formbricks:3000
encode gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Frame-Options "SAMEORIGIN"
X-Content-Type-Options "nosniff"
}
}
EOF5. Start the stack
docker compose up -d
docker compose logs -fWait for Caddy to obtain the TLS certificate and for Formbricks to complete its database migrations before proceeding.
Configuration and secrets handling
Never commit the .env file to version control. Store secrets with a tool like Vaultwarden or Infisical and inject them at runtime. Key environment variables to review:
- NEXTAUTH_SECRET โ must be a cryptographically random 32-byte hex string; changing it invalidates all active sessions.
- ENCRYPTION_KEY โ used to encrypt sensitive survey response data at rest; back this up separately from the database.
- NEXTAUTH_URL โ must exactly match your public URL including scheme; mismatches cause OAuth redirect failures.
- EMAIL_AUTH_DISABLED โ set to
1if you want to skip email verification during initial setup; re-enable for production.
For PostgreSQL backups, use pg_dump on a schedule:
docker exec formbricks-db-1 pg_dump -U formbricks formbricks | \
gzip > /opt/backups/formbricks_$(date +%Y%m%d).sql.gzVerification
After the stack is up, run these checks to confirm every component is healthy before directing real users to the survey endpoint. Catching misconfiguration early saves you from debugging half-submitted response data later.
# Check container health
docker compose ps
# Confirm TLS is live
curl -sI https://forms.example.com | grep -E "HTTP|Strict"
# Check PostgreSQL connectivity
docker exec formbricks-db-1 psql -U formbricks -c "SELECT count(*) FROM information_schema.tables WHERE table_schema='public';"Navigate to https://forms.example.com/auth/signup to create your admin account. The first account registered becomes the organization owner. Once logged in, create your first Environment (Formbricks uses environments to separate staging and production survey deployments), then explore the survey builder to create a basic NPS or onboarding survey. Submit a test response and verify it appears in the Responses tab before going live. If you plan to use in-app widgets, install the JS SDK in a test page and confirm the survey appears when the configured trigger fires.
Common issues and fixes
These are the most frequently encountered problems when running Formbricks in a containerized environment, along with their remediation steps:
- Caddy fails to obtain certificate โ confirm DNS A record propagation with
dig forms.example.comand that port 80 is reachable from the internet (required for HTTP-01 ACME challenge). - Formbricks container restarts in a loop โ usually a bad
DATABASE_URLor thedbcontainer not yet ready. Rundocker compose logs formbricksand look for Prisma migration errors; add ahealthcheckto thedbservice if startup ordering is flaky. - 502 Bad Gateway from Caddy โ check that the Formbricks container is actually listening on port 3000:
docker exec formbricks-formbricks-1 ss -tlnp. The Compose service name in the Caddyfile must match exactly. - Email signup not working โ Formbricks requires SMTP configuration for magic-link authentication. Set
MAIL_FROM,SMTP_HOST,SMTP_PORT,SMTP_USER, andSMTP_PASSWORDin your.envand restart the stack. - Survey responses not saving โ a mismatch between
NEXTAUTH_URLand the actual public URL causes CSRF validation failures. Double-check there is no trailing slash difference.
FAQ
Can I connect Formbricks to an external PostgreSQL database?
Yes. Update DATABASE_URL in .env to point to your external host and remove the db service from docker-compose.yml. Formbricks runs Prisma migrations on startup, so ensure the database user has CREATE TABLE privileges on the target schema.
Does Formbricks support SSO or SAML?
Formbricks supports OIDC-based SSO (e.g., via Keycloak, Authentik, or Azure AD) on the enterprise tier. For community self-hosted deployments, authentication is email/password or magic link by default. Set OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, and OIDC_ISSUER to enable OIDC.
How do I embed a Formbricks survey in my web application?
Install the Formbricks JS SDK (npm install @formbricks/js), initialize it with your environment ID, and call formbricks.track() on user events. Surveys are triggered in-app without iframes. For public link surveys, simply share the /s/<survey-id> URL.
How do I upgrade Formbricks to a new version?
Pull the latest image and recreate the container:
docker compose pull formbricks
docker compose up -d formbricksFormbricks runs Prisma migrations automatically on startup. Monitor logs for migration completion before sending traffic.
Can I run multiple Formbricks instances behind a load balancer?
Multi-instance deployments require a shared PostgreSQL backend and a shared Redis instance for session state. Set REDIS_URL in the environment and ensure all instances share the same NEXTAUTH_SECRET and ENCRYPTION_KEY. The Formbricks team recommends a single instance for most self-hosted use cases unless you need horizontal scaling for high survey volumes.
How do I back up and restore my Formbricks data?
All persistent data lives in PostgreSQL. Back up with pg_dump and restore with psql:
# Backup
docker exec formbricks-db-1 pg_dump -U formbricks formbricks > formbricks_backup.sql
# Restore (new instance)
docker exec -i formbricks-db-1 psql -U formbricks formbricks < formbricks_backup.sqlStore backups off-host using a tool like Kopia or rsync to an S3-compatible bucket.
Internal links
- Production Guide: Deploy Memos with Docker Compose + Caddy + PostgreSQL on Ubuntu
- Production Guide: Deploy Listmonk with Docker Compose + Caddy + PostgreSQL on Ubuntu
- Production Guide: Deploy Outline Wiki with Docker Compose + Caddy + PostgreSQL + Redis on Ubuntu
Talk to us
If you want this deployed with hardened access controls, monitoring standards, and production runbooks tailored to your environment, our team can help end-to-end.