Skip to content
Support

Quick Start Guide

Get Formbricks Hub running locally in minutes.

The easiest way to run Formbricks Hub with all dependencies.

Create a new directory:

Terminal window
mkdir formbricks-hub && cd formbricks-hub

Create compose.yml:

services:
postgres:
image: pgvector/pgvector:pg18
container_name: formbricks_hub_postgres
restart: unless-stopped
environment:
POSTGRES_USER: formbricks
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-formbricks_dev}
POSTGRES_DB: hub
ports:
- "${POSTGRES_PORT:-5432}:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U formbricks -d hub"]
interval: 10s
timeout: 5s
retries: 5
networks:
- formbricks_hub
command: >
postgres
-c shared_preload_libraries=vector
hub-migrate:
image: ghcr.io/formbricks/hub:${HUB_IMAGE_TAG:-latest}
container_name: formbricks_hub_migrate
restart: "no"
depends_on:
postgres:
condition: service_healthy
environment:
DATABASE_URL: postgresql://formbricks:${POSTGRES_PASSWORD:-formbricks_dev}@postgres:5432/hub?sslmode=disable
entrypoint: ["/bin/sh", "-c"]
command: >
/usr/local/bin/goose -dir /app/migrations postgres "$$DATABASE_URL" up &&
/usr/local/bin/river migrate-up --database-url "$$DATABASE_URL"
networks:
- formbricks_hub
hub:
image: ghcr.io/formbricks/hub:${HUB_IMAGE_TAG:-latest}
container_name: formbricks_hub_api
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
hub-migrate:
condition: service_completed_successfully
environment:
DATABASE_URL: postgresql://formbricks:${POSTGRES_PASSWORD:-formbricks_dev}@postgres:5432/hub?sslmode=disable
API_KEY: ${API_KEY:-}
PORT: 8080
PUBLIC_BASE_URL: ${PUBLIC_BASE_URL:-}
LOG_LEVEL: ${LOG_LEVEL:-info}
ports:
- "${HUB_PORT:-8080}:8080"
healthcheck:
test:
[
"CMD",
"wget",
"--no-verbose",
"--tries=1",
"--spider",
"http://localhost:8080/health",
]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
networks:
- formbricks_hub
hub-worker:
image: ghcr.io/formbricks/hub:${HUB_IMAGE_TAG:-latest}
container_name: formbricks_hub_worker
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
hub-migrate:
condition: service_completed_successfully
environment:
DATABASE_URL: postgresql://formbricks:${POSTGRES_PASSWORD:-formbricks_dev}@postgres:5432/hub?sslmode=disable
LOG_LEVEL: ${LOG_LEVEL:-info}
entrypoint: ["/app/hub-worker"]
healthcheck:
disable: true
networks:
- formbricks_hub
volumes:
postgres_data:
driver: local
networks:
formbricks_hub:
driver: bridge

Create a .env file in the same directory:

Terminal window
# Required: Secure your PostgreSQL database
POSTGRES_PASSWORD=your_secure_postgres_password_here
# Required: API authentication key
API_KEY=your_secure_api_key_here
# Optional: Host ports and Hub image tag
HUB_PORT=8080
POSTGRES_PORT=5432
HUB_IMAGE_TAG=latest
LOG_LEVEL=info
# Optional: Public URL advertised in /openapi.yaml and /openapi.json
# Set this for production behind ingress, TLS termination, or a path prefix.
PUBLIC_BASE_URL=
Terminal window
# For POSTGRES_PASSWORD
openssl rand -base64 32
# For API_KEY
openssl rand -base64 32
Terminal window
docker compose up -d

This will:

  • Start PostgreSQL with persistent storage
  • Run database migrations and create River tables with the one-shot hub-migrate service
  • Pull the latest Formbricks Hub image from GitHub Container Registry
  • Start the Hub API on port 8080
  • Start the Hub worker for asynchronous jobs

Migrations are managed with goose and River. The published Hub image includes goose, river, and /app/migrations, so Docker Compose can migrate the database without local Go, goose, river, or migration files.

Check that the services are running:

Terminal window
docker compose ps --all

Expected output:

NAME STATUS PORTS
formbricks_hub_api Up (healthy) 0.0.0.0:8080->8080/tcp
formbricks_hub_migrate Exited (0)
formbricks_hub_postgres Up (healthy) 0.0.0.0:5432->5432/tcp
formbricks_hub_worker Up

Test the health endpoint:

Terminal window
curl http://localhost:8080/health

Expected response:

OK

Check that the runtime OpenAPI spec is available without authentication:

Terminal window
curl http://localhost:8080/openapi.json

Expected response: an OpenAPI JSON document with Hub API metadata and paths.

If you have an existing PostgreSQL instance, run migrations first:

Terminal window
docker run --rm \
-e DATABASE_URL="postgresql://user:password@host:5432/hub?sslmode=disable" \
--entrypoint /bin/sh \
ghcr.io/formbricks/hub:latest \
-c 'goose -dir /app/migrations postgres "$DATABASE_URL" up && river migrate-up --database-url "$DATABASE_URL"'

The published Hub image includes goose, river, and /app/migrations, so this applies both Hub database migrations and River queue migrations without local Go, goose, river, or migration files.

Then run the Hub API:

Terminal window
docker run -d \
--name formbricks-hub \
-p 8080:8080 \
-e DATABASE_URL="postgresql://user:password@host:5432/hub?sslmode=disable" \
-e API_KEY="your-secret-key" \
-e LOG_LEVEL="info" \
ghcr.io/formbricks/hub:latest
Terminal window
curl -X POST http://localhost:8080/v1/feedback-records \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your_secure_api_key_here" \
-d '{
"tenant_id": "org-123",
"submission_id": "subm-my-first-survey-001",
"source_type": "survey",
"source_id": "my-first-survey",
"field_id": "q1",
"field_label": "How satisfied are you?",
"field_type": "rating",
"value_number": 5,
"metadata": {
"country": "US",
"device": "desktop"
}
}'

Expected response:

{
"id": "01932c8a-8b9e-7000-8000-000000000001",
"tenant_id": "org-123",
"submission_id": "subm-my-first-survey-001",
"collected_at": "2025-10-20T12:34:56Z",
"created_at": "2025-10-20T12:34:56Z",
"updated_at": "2025-10-20T12:34:56Z",
"source_type": "survey",
"source_id": "my-first-survey",
"field_id": "q1",
"field_type": "rating",
"value_number": 5,
"metadata": {
"country": "US",
"device": "desktop"
}
}
Terminal window
curl -X POST http://localhost:8080/v1/feedback-records \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your_secure_api_key_here" \
-d '{
"tenant_id": "org-123",
"submission_id": "subm-my-first-survey-001",
"source_type": "survey",
"source_id": "my-first-survey",
"field_id": "q2",
"field_label": "What can we improve?",
"field_type": "text",
"value_text": "The checkout process could be simplified!"
}'
Terminal window
curl "http://localhost:8080/v1/feedback-records?tenant_id=org-123&limit=10" \
-H "Authorization: Bearer your_secure_api_key_here"

Response:

{
"data": [
{
"id": "01932c8a-8b9e-7000-8000-000000000001",
"tenant_id": "org-123",
"submission_id": "subm-my-first-survey-001",
"collected_at": "2025-10-20T12:34:56Z",
"created_at": "2025-10-20T12:34:56Z",
"updated_at": "2025-10-20T12:34:56Z",
"source_type": "survey",
"source_id": "my-first-survey",
"field_id": "q1",
"field_type": "rating",
"value_number": 5
}
],
"limit": 10
}
VariableRequiredDefaultDescription
DATABASE_URLNopostgres://postgres:postgres@localhost:5432/test_db?sslmode=disablePostgreSQL connection string
API_KEYYes-API authentication key
PORTNo8080HTTP server port
PUBLIC_BASE_URLNo-Public API root advertised in runtime OpenAPI specs
LOG_LEVELNoinfoLog level (debug, info, warn, error)

In the Docker Compose example above, use HUB_PORT and POSTGRES_PORT to change host ports without changing the ports used inside the containers.

See full environment variable reference ->

postgresql://username:password@host:port/database?sslmode=disable

For Docker Compose (services in same network):

postgresql://formbricks:password@postgres:5432/hub?sslmode=disable

For external PostgreSQL:

postgresql://user:pass@db.example.com:5432/hub?sslmode=require
Terminal window
# All services
docker compose logs -f
# Just the Hub API
docker compose logs -f hub
# Just the Hub worker
docker compose logs -f hub-worker
# Just PostgreSQL
docker compose logs -f postgres
Terminal window
docker compose down
Terminal window
docker compose down -v
Terminal window
docker compose restart
Terminal window
docker compose pull
docker compose stop hub hub-worker
docker compose up --force-recreate hub-migrate
docker compose up -d

When updating Hub images, recreate hub-migrate so goose and River migrations from the new image are applied before the API and worker continue running.

If port 8080 or 5432 is occupied:

Terminal window
# Change ports in .env
HUB_PORT=8081
POSTGRES_PORT=5433

Or modify the port mapping in compose.yml:

ports:
- "${HUB_PORT:-8080}:8080" # Host:Container

Check the logs for errors:

Terminal window
docker compose logs hub

Common issues:

  • Database not ready: Wait for PostgreSQL healthcheck to pass
  • Invalid API key: Ensure API_KEY is set in .env
  • Database connection failed: Check DATABASE_URL format

Verify PostgreSQL is running:

Terminal window
docker compose ps postgres

If unhealthy, check PostgreSQL logs:

Terminal window
docker compose logs postgres

Check if the service is listening:

Terminal window
docker exec formbricks_hub_api wget -O- http://localhost:8080/health

Ensure you have access to GitHub Container Registry:

Terminal window
docker pull ghcr.io/formbricks/hub:latest

If authentication is required:

Terminal window
echo $GITHUB_TOKEN | docker login ghcr.io -u USERNAME --password-stdin

Verify the volume is mounted:

Terminal window
docker volume ls | grep postgres_data
docker volume inspect formbricks-hub_postgres_data