The short answer
Pick Formbricks if you want the Typeform feel: one question at a time, a modern editor, an NPS score and charts out of the box, signed webhooks and a full REST API, all in the free version. It is also by far the heavier of the two, with seven containers and about 840 MB at rest, and roles, two-factor sign-in, single sign-on and reCAPTCHA need a paid licence. Pick LimeSurvey for questionnaires that are long, formal or in many languages, or when several people need different permissions: users, groups, permissions per survey, CAPTCHA, two-factor sign-in and exports to SPSS, R and Stata are all free, and it ran in about 150 MB. Its weak points are an older look, no webhooks, no NPS question, and a survey that would not work embedded on another domain in our test.
Both are actively maintained: Formbricks 6.0.0 came out on 21 September 2026 and LimeSurvey 7.5.1 on 7 October 2026, according to their GitHub releases.
What we tested
We installed both on the same fresh Melonslab VC-P Titanium (4 vCPU, 16 GB) with Debian 13, each under a user of its own, as rootless Podman Quadlet units behind the same Caddy, with passwords and keys as Podman secrets. We have no guide for either yet, so we built each in the pattern of our other guides, on top of the Podman guide. How we ran Formbricks and How we ran LimeSurvey have the files.
Then we built the same customer feedback survey in both, in English and Swedish:
- 12 questions: where you heard of us (single choice), an NPS question from 0 to 10, "What should we do better?" shown only for scores of 6 or lower, the services you use (multiple choice), support rated from 1 to 5, a matrix rating price, quality, delivery time and communication from poor to excellent, whether you contacted support, a free-text question about the case shown only if you did, a file upload, how often you use us, a free-text question and whether we may contact you;
- two hidden fields,
channelandcustomer, filled from the address, such as?channel=newsletter&customer=C-1001; - a public link, and the same survey in an
iframeon a plain HTML page; - an email to Anna, the owner, for every response, caught by a test mail server on the same machine. No real email was sent anywhere.
We answered the survey in a browser in both languages, with an upload, to check the logic. Then 200 simulated respondents, generated by a script with the same answers for both tools, 115 of them in Swedish, went through each tool's public path: Formbricks' public responses API, which its survey page uses, and LimeSurvey's survey pages, page by page as a browser would. No real people took part. We then looked at the reports, exported CSV files, tried webhooks and the API, deleted one respondent's data, switched IP storage on and off, added a second user with fewer rights, switched the interface to Swedish, rebooted the server, measured, and backed up both and restored them for a new user.
Typeform, Google Forms and SurveyMonkey were not tested. We did not sign up for any of them. What this page says about them comes from their own pricing and help pages, linked where we use them and read on 14 October 2026.
The numbers
| Formbricks 6.0.2 | LimeSurvey 7.5.1 | |
|---|---|---|
| Containers | 7 running: Formbricks, Hub, Cube, SpiceDB, PostgreSQL 18 with pgvector, Valkey 9, Garage for uploads; plus 4 jobs that run at start and stop | 2: LimeSurvey (Apache and PHP 8.4), MariaDB 12.3 |
| Memory at rest after a reboot | 835 MB (Formbricks 616, Cube 82, PostgreSQL 74, SpiceDB 38) | 145 MB (MariaDB 132, LimeSurvey 13) |
| Memory at the peak of use | 1.10 GB (Formbricks 848) | 201 MB (MariaDB 141, LimeSurvey 60) |
| Images on disk | 3.27 GB | 1.27 GB |
| Data after the test | 90 MB | 174 MB |
From useradd to the login page | 1 minute 53 seconds | 45 seconds |
| 200 simulated respondents | about 25 seconds, one request each | 7 minutes 40 seconds, 12 to 14 pages each |
| NPS | a question type, with the score in the report | a list from 0 to 10; no score |
| Webhooks | yes, signed | no, only through a third-party plugin |
| Roles and permissions, free | no: every member is an owner | yes: users, groups, per survey |
| Spam protection, free | email check, PIN, response limit; reCAPTCHA is paid | CAPTCHA |
| Two-factor sign-in, free | no | yes |
| Swedish interface | yes | yes |
| Licence | AGPL 3.0, paid features under Formbricks' own licence | GPL 2.0 or later |
Memory is the sum of each app's containers, measured with podman stats five minutes after a reboot, with each app opened once and the 200 responses in place. The peak is the highest reading while the 200 respondents were sent. Times are how long the commands took to run on a fresh user, mostly downloading images, not the time you need to read and type them. The respondent times say as much about how each tool takes answers as about speed: Formbricks takes a whole response in one request, LimeSurvey a page at a time, and answered each page in about 0.2 seconds.
Setup
LimeSurvey is two containers. We used martialblog/limesurvey, an image made by the community: LimeSurvey publishes no image of its own. It creates the admin account from a Podman secret on the first start, so there is no open setup page, and it was ready 45 seconds after useradd.
Formbricks has grown with each major version. Version 5 added Hub, its own API service, and Cube, which powers the reports; version 6 added SpiceDB, Google Zanzibar-style permissions in a database of their own. Its Compose file starts eleven containers, four of them jobs that prepare the databases and stop. File uploads need S3 storage that the respondent's browser can reach, which we ran with Garage under a second address, forms-files.example.com, as in the Garage guide. The first visitor to Formbricks gets the page that creates the admin, so create it as soon as Caddy is in place; after that, the sign-up pages answered "Page not found" and new people could only join by invitation.
Building the survey
Formbricks' editor works like Typeform's: blocks of questions, conditional logic on each block, and a preview beside it. Its question types covered all twelve of ours, including NPS, matrix and file upload. Hidden fields are a list of names on the survey; we added channel and customer, and they were saved from the address. One snag: source was refused as a name, "Hidden field id is not allowed".
LimeSurvey is a classic survey tool with more question types than we needed: arrays, rankings, dual scales, equations and more. Logic is a condition on each question, written in its expression language, such as !is_empty(nps) and nps <= 6. It has no NPS question, so ours was a list from 0 to 10. Hidden fields are ordinary questions set to hidden, filled from the address by their question code. LimeSurvey also took the underscores out of our codes, so support_rating became supportrating. We imported the survey as a tab-separated file, a format LimeSurvey documents for this.
Both showed only the right follow-up questions: a score of 3 brought up "What should we do better?", a 9 skipped it, and "No" to the support question skipped the case.
The respondent's view
Formbricks shows one question per page in a clean card, with a back button. The Swedish version showed "Nästa", "Tillbaka" and "Obligatorisk" around our own Swedish texts, but the survey's own name stayed in English, since that is an internal field.
LimeSurvey showed a welcome page with "Det finns 12 frågor i denna enkät.", then a question per page in its default theme, with a language menu. It looks dated next to Formbricks, but is plain and clear. Its file upload opens a dialog, and allowed at most 2 MB per file, although we set 5 MB on the question: the image's PHP limit.
Embedding the form on a web page
Both gave a link and worked in an iframe on a plain HTML page under another subdomain of the same domain. LimeSurvey needed one setting first: by default it sends X-Frame-Options: SAMEORIGIN, and only allows framing after Configuration > Global settings > Security > IFrame embedding allowed is set to Allow.
On a page on another domain, LimeSurvey failed: the first page loaded, and Nästa answered "400: Bad Request - The CSRF token could not be verified." Its session cookies are not sent from a page on another site. LimeSurvey's own bug tracker has this case, closed with no change; the workaround people use is to change the cookie settings in config.php, which the image we used does not let you set. Formbricks worked from another domain, and also has a script for pop-up surveys on your own website, which we did not test. If your survey has to live inside your website, Formbricks is the safe choice; with LimeSurvey, link to it.
Results and reports
Formbricks' summary page showed each question as a chart or table: starts and completions, the NPS split into promoters, passives and detractors with the score, an average for the rating, percentages per cell in the matrix, and free-text answers in a list. Answers in Swedish and English were counted together under the same choices, and a filter and a date range narrow it down.
LimeSurvey has a Statistics page that builds tables and charts for the questions you pick, with filters, and a response browser with every answer. It gave no NPS score: you compute it yourself, or with an equation question.
CSV export
| Formbricks | LimeSurvey | |
|---|---|---|
| Formats | CSV, Excel, and a ZIP file with the uploads | CSV, Excel, PDF, HTML, Word, JSON, R, SPSS, Stata |
| Answers | in the respondent's language, plus an option ID column for each choice question | in one language you pick, as text or codes |
| Columns | 53, with browser, device, URL, language and hidden fields | one per question and subquestion |
One flaw in Formbricks: in its CSV export, the matrix columns were empty for every response given in Swedish. The answers were saved, and the summary counted them, but the export looks them up by the English row names. If you run a matrix in more than one language, export through the API or check the file.
Email on a new response
Both sent Anna an email for every finished response, including all 200 simulated ones, through the test mail server. In Formbricks, each user turns this on per survey under Notifications; surveys created through the API had it off. LimeSurvey sends it to the address in the survey's Notifications & data settings, in the respondent's language: "Svar inskickat för undersökningen Kundenkät 2026" for a Swedish answer. Formbricks reads its mail server from the unit file, with the password as a Podman secret; LimeSurvey stores it in its database, set under Global settings > Email settings.
Webhooks and API
Formbricks sends webhooks for new, updated and finished responses, signed in the Standard Webhooks format with a Webhook-Signature header, and its body had every answer, the hidden fields and the link to the uploaded file. It has a REST API with keys per organisation and workspace; we built the survey with it. One thing to know: its version 1 API could not create a survey in two languages in our test, while the version 3 API could. A webhook to another app on the same server failed with ECONNREFUSED: Formbricks connects to the server's public address, which inside the pod is the pod itself, and AddHost did not change that. For the test we gave the pod an address of its own, and the webhooks arrived.
LimeSurvey has no webhooks of its own; there are third-party plugins, which we did not try. Its API, RemoteControl, is JSON-RPC and off by default: Global settings > Interfaces > RPC interface enabled. Through it we imported and activated the survey, exported responses and deleted them. To react to a new response, you poll the API.
Languages
Both ran our survey in English and Swedish from the same survey, with the respondent's language in the link (?lang=sv-SE in Formbricks, ?lang=sv in LimeSurvey) or picked from a menu. In Formbricks you first add Swedish under Survey Languages, then translate each text; multi-language surveys are in the free version. LimeSurvey has had this for years and handles many languages per survey, with its own translation view.
Anonymity and GDPR
- IP addresses. Both store none by default. With Capture IP address on, Formbricks saved the first half of our IPv6 address and zeros after it; LimeSurvey, with Save IP address and Anonymize IP address on, kept the first three groups. Formbricks also has Anonymize responses, which stops storing the address, country and device details and strips query parameters from saved page addresses.
- Deleting a respondent. We deleted every response from
customerC-1001 through each API. Both removed the responses. LimeSurvey also deleted the uploaded file; Formbricks did not: the file stayed in storage, reachable only by signed-in users, and has to be removed by hand. - Where the data is. On your own server, the answers and uploads stay in Sweden.
Spam and bots
Our 200 simulated respondents went through both without being stopped, at about eight a second in Formbricks. Formbricks' reCAPTCHA v3 protection is a paid feature, and it calls Google; the free version has Verify email before submission, Protect survey with a PIN and a limit on responses. LimeSurvey has its own image CAPTCHA, free: with Use CAPTCHA for survey access on, the survey opened with a CAPTCHA page. It also has participant lists with access codes for closed surveys.
Teams and permissions
Formbricks has organisations and workspaces, but without a licence everyone you invite is an Owner: the invite dialog showed member and manager with "Unlock Organization Roles with a higher plan". Teams with access per workspace are paid too, as are two-factor sign-in and single sign-on.
LimeSurvey has users, groups and permissions per survey, all free. We gave Erik read access to the responses and statistics of one survey: he could browse the responses and view statistics, and global settings answered "403: Forbidden". Two-factor sign-in and LDAP sign-in come as core plugins, which we did not try.
Swedish
- Formbricks: Swedish (Svenska) under Your Profile. Most of it was Swedish, such as "Enkäter", "Ny enkät" and "Svarsalternativ", with parts in English, such as the "ASK" and "ANALYZE" headings.
- LimeSurvey: Svenska in each user's settings. Erik saw "Enkäter", "Svar" and "Statistik", with a few English words, such as "Running".
The respondent's side was Swedish in both, as described above.
Resources
LimeSurvey is a PHP app. At rest, MariaDB held almost all of its 145 MB, and under 200 respondents it rose to 201 MB.
Formbricks is a Next.js app with services around it. At rest it used 835 MB, three quarters of it the Formbricks container, and while it took the 200 responses it rose to 1.10 GB. Its images take 3.27 GB, against 1.27 GB for LimeSurvey.
Both fit an 8 GB server with plenty of room, as on our test server, where both ran at once.
Updates and backups
LimeSurvey follows the 7-apache tag of its image, so Podman updates it within version 7, and it updates its database at start. We built and answered the survey on 7.5.0; our reinstall and restore a few hours later pulled 7.5.1, with the data intact. Its backup is a database dump and the uploads volume, 240 KB in our test.
Formbricks follows the 6.0 tag, but the Formbricks images have no automatic update: four jobs prepare the databases before it starts, so you update by changing the tags and restarting the whole pod. Expect to read release notes: each of the last two major versions added services. Its backup is a dump of PostgreSQL and the Garage volumes, 2.4 MB in our test. When we also restored SpiceDB's database, SpiceDB failed with "duplicate key value violates unique constraint" and Formbricks did not start; leaving SpiceDB's database empty and letting Formbricks rebuild it from its own data worked.
We restored both for a new user, with the secrets from the backup, and the surveys, the 200 responses, the users, the uploads and the API key came back. Both also came back by themselves after a reboot.
How we ran Formbricks
Formbricks' Docker documentation installs with a script and a Compose file. We ran the same images and settings from the Compose file for 6.0.2 as Quadlet units in one pod, with three changes: SpiceDB's database is created by a script at PostgreSQL's first start instead of a separate job, uploads go to Garage instead of the optional RustFS, and AddHost sends the storage address to Caddy, because Formbricks fetches uploads from it and the server's own address belongs to the pod.
As root, create the user as in step 1 of the Plane guide, with formbricks instead of plane. As formbricks, create the secrets, including the Garage key Formbricks uses:
pw=$(openssl rand -hex 24)
printf %s "$pw" | podman secret create formbricks-db-password -
printf %s "postgresql://postgres:$pw@localhost:5432/formbricks?schema=public" | podman secret create formbricks-db-url -
printf %s "postgresql://postgres:$pw@localhost:5432/formbricks?sslmode=disable" | podman secret create formbricks-hub-db-url -
spw=$(openssl rand -hex 24)
printf %s "$spw" | podman secret create formbricks-spicedb-db-password -
printf %s "postgresql://spicedb:$spw@localhost:5432/spicedb?sslmode=disable" | podman secret create formbricks-spicedb-db-url -
for s in spicedb-token nextauth-secret encryption-key cron-secret hub-key cube-secret s3-rpc; do
openssl rand -hex 32 | tr -d '\n' | podman secret create formbricks-$s -
done
printf %s "GK$(openssl rand -hex 12)" | podman secret create formbricks-s3-key-id -
openssl rand -hex 32 | tr -d '\n' | podman secret create formbricks-s3-secret-key -
printf %s 'your-smtp-password' | podman secret create formbricks-smtp-password -
Download Cube's configuration from the release, and create ~/garage.toml as in step 3 of the Plane guide, with api_bind_addr = "[::]:3900":
mkdir -p ~/cube/schema ~/initdb ~/.config/containers/systemd
curl -fsSL -o ~/cube/cube.js https://raw.githubusercontent.com/formbricks/formbricks/6.0.2/docker/cube/cube.js
curl -fsSL -o ~/cube/schema/FeedbackRecords.js https://raw.githubusercontent.com/formbricks/formbricks/6.0.2/docker/cube/schema/FeedbackRecords.js
~/initdb/spicedb.sh, which PostgreSQL runs once, on an empty volume:
#!/bin/sh
psql -v ON_ERROR_STOP=1 -U postgres -v pw="$SPICEDB_DB_PASSWORD" <<'SQL'
CREATE ROLE spicedb LOGIN PASSWORD :'pw';
CREATE DATABASE spicedb OWNER spicedb;
SQL
In ~/.config/containers/systemd/, formbricks.pod:
[Pod]
PodName=formbricks
PublishPort=127.0.0.1:8134:3000
PublishPort=127.0.0.1:8135:3900
AddHost=forms-files.example.com:host-gateway
[Install]
WantedBy=default.target
formbricks-db.container, formbricks-cache.container and formbricks-s3.container:
[Container]
ContainerName=formbricks-db
Image=docker.io/pgvector/pgvector:pg18
Pod=formbricks.pod
Volume=formbricks-db:/var/lib/postgresql
Volume=%h/initdb:/docker-entrypoint-initdb.d:ro
Environment=POSTGRES_DB=formbricks
Secret=formbricks-db-password,type=env,target=POSTGRES_PASSWORD
Secret=formbricks-spicedb-db-password,type=env,target=SPICEDB_DB_PASSWORD
HealthCmd=pg_isready -U postgres
HealthInterval=10s
Notify=healthy
AutoUpdate=registry
[Service]
Restart=always
TimeoutStartSec=300
[Container]
ContainerName=formbricks-cache
Image=docker.io/valkey/valkey:9-alpine
Pod=formbricks.pod
Volume=formbricks-cache:/data
Exec=valkey-server --appendonly yes --maxmemory-policy noeviction
AutoUpdate=registry
[Service]
Restart=always
[Container]
ContainerName=formbricks-s3
Image=docker.io/dxflrs/garage:v2.4.1
Exec=/garage server --single-node
Pod=formbricks.pod
Volume=%h/garage.toml:/etc/garage.toml:ro
Volume=formbricks-s3-meta:/var/lib/garage/meta
Volume=formbricks-s3-data:/var/lib/garage/data
Secret=formbricks-s3-rpc,type=env,target=GARAGE_RPC_SECRET
StopTimeout=30
[Service]
Restart=always
The image is pgvector's, PostgreSQL 18 with the vector extension that Formbricks' Compose file uses. SpiceDB first sets up its database and stops, then runs. formbricks-spicedb-migrate.container and formbricks-spicedb.container:
[Unit]
Requires=formbricks-db.service
After=formbricks-db.service
[Container]
ContainerName=formbricks-spicedb-migrate
Image=docker.io/authzed/spicedb:v1.52.0
Pod=formbricks.pod
Exec=datastore migrate head
Environment=SPICEDB_DATASTORE_ENGINE=postgres
Secret=formbricks-spicedb-db-url,type=env,target=SPICEDB_DATASTORE_CONN_URI
[Service]
Type=oneshot
RemainAfterExit=yes
[Unit]
Requires=formbricks-spicedb-migrate.service
After=formbricks-spicedb-migrate.service
[Container]
ContainerName=formbricks-spicedb
Image=docker.io/authzed/spicedb:v1.52.0
Pod=formbricks.pod
Exec=serve
Environment=SPICEDB_DATASTORE_ENGINE=postgres SPICEDB_LOG_FORMAT=json SPICEDB_TELEMETRY_ENDPOINT=
Secret=formbricks-spicedb-db-url,type=env,target=SPICEDB_DATASTORE_CONN_URI
Secret=formbricks-spicedb-token,type=env,target=SPICEDB_GRPC_PRESHARED_KEY
[Service]
Restart=always
Formbricks' and Hub's database jobs, formbricks-migrate.container and formbricks-hub-migrate.container:
[Unit]
Requires=formbricks-db.service
After=formbricks-db.service
[Container]
ContainerName=formbricks-migrate
Image=ghcr.io/formbricks/formbricks:6.0
Pod=formbricks.pod
Entrypoint=node
Exec=packages/database/dist/scripts/apply-migrations.js
Secret=formbricks-db-url,type=env,target=DATABASE_URL
[Service]
Type=oneshot
RemainAfterExit=yes
TimeoutStartSec=600
[Unit]
Requires=formbricks-migrate.service
After=formbricks-migrate.service
[Container]
ContainerName=formbricks-hub-migrate
Image=ghcr.io/formbricks/hub:0.14
Pod=formbricks.pod
Entrypoint=sh
Exec=-c 'goose -dir /app/migrations postgres "$$DATABASE_URL" up && river migrate-up --database-url "$$DATABASE_URL"'
Secret=formbricks-hub-db-url,type=env,target=DATABASE_URL
[Service]
Type=oneshot
RemainAfterExit=yes
$$ keeps systemd from replacing the variable before the container sees it. Then Hub and Cube, formbricks-hub.container and formbricks-cube.container:
[Unit]
Requires=formbricks-hub-migrate.service
After=formbricks-hub-migrate.service
[Container]
ContainerName=formbricks-hub
Image=ghcr.io/formbricks/hub:0.14
Pod=formbricks.pod
Secret=formbricks-hub-db-url,type=env,target=DATABASE_URL
Secret=formbricks-hub-key,type=env,target=API_KEY
[Service]
Restart=always
[Unit]
Requires=formbricks-hub-migrate.service
After=formbricks-hub-migrate.service
[Container]
ContainerName=formbricks-cube
Image=ghcr.io/formbricks/cube:1.7.47-1
Pod=formbricks.pod
Volume=%h/cube/cube.js:/cube/conf/cube.js:ro
Volume=%h/cube/schema:/cube/conf/model:ro
Environment=CUBEJS_DB_TYPE=postgres CUBEJS_DB_HOST=localhost CUBEJS_DB_PORT=5432 CUBEJS_DB_NAME=formbricks CUBEJS_DB_USER=postgres
Environment=CUBEJS_JWT_ISSUER=formbricks-web CUBEJS_JWT_AUDIENCE=formbricks-cube CUBEJS_DEFAULT_API_SCOPES=meta,data
Environment=CUBEJS_EXTERNAL_DEFAULT=false CUBEJS_CACHE_AND_QUEUE_DRIVER=memory
Secret=formbricks-db-password,type=env,target=CUBEJS_DB_PASS
Secret=formbricks-cube-secret,type=env,target=CUBEJS_API_SECRET
[Service]
Restart=always
formbricks-authzed-init.container, which writes Formbricks' permissions into SpiceDB, and rebuilds them if SpiceDB's database is empty:
[Unit]
Requires=formbricks-migrate.service formbricks-spicedb.service
After=formbricks-migrate.service formbricks-spicedb.service
[Container]
ContainerName=formbricks-authzed-init
Image=ghcr.io/formbricks/formbricks:6.0
Pod=formbricks.pod
Entrypoint=formbricks-authzed
Exec=upgrade prepare
Secret=formbricks-db-url,type=env,target=DATABASE_URL
Secret=formbricks-spicedb-token,type=env,target=AUTHZED_TOKEN
Environment=ENCRYPTION_KEY=unused CUBEJS_API_SECRET=unused CUBEJS_API_URL=http://localhost HUB_API_KEY=unused HUB_API_URL=http://localhost REDIS_URL=redis://localhost
Environment=AUTHZED_ENABLED=true AUTHZED_ENDPOINT=localhost:50051 AUTHZED_SYSTEM_KEY=formbricks AUTHZED_INSECURE=true AUTHZED_CONSISTENCY=fully_consistent
[Service]
Type=oneshot
RemainAfterExit=yes
TimeoutStartSec=300
The unused values are placeholders that Formbricks' Compose file sets the same way; this job does not use them. And formbricks-app.container, with your own addresses and mail server:
[Unit]
Requires=formbricks-cache.service formbricks-hub.service formbricks-cube.service formbricks-authzed-init.service
After=formbricks-cache.service formbricks-hub.service formbricks-cube.service formbricks-authzed-init.service
[Container]
ContainerName=formbricks-app
Image=ghcr.io/formbricks/formbricks:6.0
Pod=formbricks.pod
Environment=WEBAPP_URL=https://forms.example.com NEXTAUTH_URL=https://forms.example.com
Environment=REDIS_URL=redis://localhost:6379 HUB_API_URL=http://localhost:8080 CUBEJS_API_URL=http://localhost:4000
Environment=CUBEJS_JWT_ISSUER=formbricks-web CUBEJS_JWT_AUDIENCE=formbricks-cube
Environment=SKIP_STARTUP_MIGRATION=true TELEMETRY_DISABLED=1
Environment=AUTHZED_ENABLED=true AUTHZED_ENDPOINT=localhost:50051 AUTHZED_SYSTEM_KEY=formbricks AUTHZED_INSECURE=true AUTHZED_CONSISTENCY=fully_consistent
Environment=S3_REGION=garage S3_BUCKET_NAME=formbricks S3_ENDPOINT_URL=https://forms-files.example.com S3_FORCE_PATH_STYLE=1
Environment=MAIL_FROM=forms@example.com MAIL_FROM_NAME=Formbricks
Environment=SMTP_HOST=smtp.example.com SMTP_PORT=587 SMTP_USER=forms@example.com SMTP_AUTHENTICATED=1
Environment=EMAIL_VERIFICATION_DISABLED=0 PASSWORD_RESET_DISABLED=0
Secret=formbricks-db-url,type=env,target=DATABASE_URL
Secret=formbricks-nextauth-secret,type=env,target=NEXTAUTH_SECRET
Secret=formbricks-encryption-key,type=env,target=ENCRYPTION_KEY
Secret=formbricks-cron-secret,type=env,target=CRON_SECRET
Secret=formbricks-hub-key,type=env,target=HUB_API_KEY
Secret=formbricks-cube-secret,type=env,target=CUBEJS_API_SECRET
Secret=formbricks-spicedb-token,type=env,target=AUTHZED_TOKEN
Secret=formbricks-s3-key-id,type=env,target=S3_ACCESS_KEY
Secret=formbricks-s3-secret-key,type=env,target=S3_SECRET_KEY
Secret=formbricks-smtp-password,type=env,target=SMTP_PASSWORD
[Service]
Restart=always
TimeoutStartSec=300
Pull the seven images first, docker.io/pgvector/pgvector:pg18, docker.io/valkey/valkey:9-alpine, docker.io/dxflrs/garage:v2.4.1, docker.io/authzed/spicedb:v1.52.0, ghcr.io/formbricks/formbricks:6.0, ghcr.io/formbricks/hub:0.14 and ghcr.io/formbricks/cube:1.7.47-1, about 3.3 GB. Then start the pod and give Garage the bucket and key:
systemctl --user daemon-reload
systemctl --user start formbricks-pod
systemctl --user enable --now podman-auto-update.timer
garage() { podman exec -e RUST_LOG=warn formbricks-s3 /garage "$@"; }
garage bucket create formbricks
garage key import -n formbricks --yes "$(podman secret inspect --showsecret --format '{{.SecretData}}' formbricks-s3-key-id)" "$(podman secret inspect --showsecret --format '{{.SecretData}}' formbricks-s3-secret-key)"
garage bucket allow --read --write formbricks --key formbricks
It is ready when curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8134/health prints 200. The blocks in Caddy's ~/Caddyfile, where the second lets browsers upload straight to Garage from the survey page:
forms.example.com {
reverse_proxy 127.0.0.1:8134
}
forms-files.example.com {
header Access-Control-Allow-Origin https://forms.example.com
header Access-Control-Allow-Methods "GET, POST, PUT, HEAD"
header Access-Control-Allow-Headers *
header Access-Control-Expose-Headers ETag
@preflight method OPTIONS
respond @preflight 204
reverse_proxy 127.0.0.1:8135
}
Restart Caddy, open https://forms.example.com straight away and create the admin. Formbricks confirms the address by email before the first sign-in, so the mail settings must work; for port 465, add SMTP_SECURE_ENABLED=1. Without email, sign-up confirmation, invitations, password resets and response notifications fail.
To back up, as formbricks, dump the database and export Garage, stopped for a few seconds while uploads fail, and keep the secrets:
umask 077
mkdir -p ~/backup
podman exec formbricks-db pg_dump -U postgres -Fc formbricks > ~/backup/formbricks.dump
systemctl --user stop formbricks-s3
podman volume export formbricks-s3-meta > ~/backup/formbricks-s3-meta.tar
podman volume export formbricks-s3-data > ~/backup/formbricks-s3-data.tar
systemctl --user start formbricks-s3
for s in $(podman secret ls --format '{{.Name}}'); do
printf '%s %s\n' "$s" "$(podman secret inspect --showsecret --format '{{.SecretData}}' "$s")"
done > ~/backup/secrets.txt
To restore for a new user: put the files back, create the secrets from secrets.txt, import the two Garage volumes, start the pod and wait for /health, then stop formbricks-app, formbricks-hub, formbricks-cube and formbricks-spicedb, drop and create the formbricks database, load the dump with pg_restore and restart the pod. Leave SpiceDB's database as the new install made it: formbricks-authzed-init fills it from Formbricks' data.
How we ran LimeSurvey
LimeSurvey publishes no container image, and its installation manual describes PHP and a web server. We used martialblog/limesurvey, the community image others build on, in its Apache variant, with MariaDB as in the MariaDB guide: version 12.3, the image's health check and MARIADB_AUTO_UPGRADE. Two things to know about the image:
- It writes
config.phpandsecurity.phpinside the container at each start, from the settings below. The encryption keys come from secrets, so they survive a new container. - It creates the admin from
ADMIN_PASSWORDon an empty database only.
As root, create the user as in step 1 of the Plane guide, with limesurvey. As limesurvey, create the secrets:
openssl rand -hex 24 | tr -d '\n' | podman secret create limesurvey-db-password -
printf %s "$(openssl rand -base64 18)" | podman secret create limesurvey-admin-password -
openssl rand -hex 24 | tr -d '\n' | podman secret create limesurvey-encrypt-nonce -
openssl rand -hex 32 | tr -d '\n' | podman secret create limesurvey-encrypt-key -
~/.config/containers/systemd/limesurvey.pod:
[Pod]
PodName=limesurvey
PublishPort=127.0.0.1:8141:8080
[Install]
WantedBy=default.target
limesurvey-db.container:
[Container]
ContainerName=limesurvey-db
Image=docker.io/library/mariadb:12.3
Pod=limesurvey.pod
Volume=limesurvey-db:/var/lib/mysql
Environment=MARIADB_DATABASE=limesurvey MARIADB_USER=limesurvey MARIADB_RANDOM_ROOT_PASSWORD=1 MARIADB_AUTO_UPGRADE=1
Secret=limesurvey-db-password,type=env,target=MARIADB_PASSWORD
HealthCmd=healthcheck.sh --connect --innodb_initialized
HealthInterval=10s
Notify=healthy
AutoUpdate=registry
[Service]
Restart=always
limesurvey-app.container, with your own address:
[Unit]
Requires=limesurvey-db.service
After=limesurvey-db.service
[Container]
ContainerName=limesurvey-app
Image=docker.io/martialblog/limesurvey:7-apache
Pod=limesurvey.pod
Volume=limesurvey-upload:/var/www/html/upload
Environment=DB_TYPE=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_NAME=limesurvey DB_USERNAME=limesurvey DB_MYSQL_ENGINE=InnoDB
Environment=ADMIN_USER=admin ADMIN_NAME=Anna ADMIN_EMAIL=anna@example.com
Environment=PUBLIC_URL=https://survey.example.com BASE_URL=https://survey.example.com URL_FORMAT=path
Secret=limesurvey-db-password,type=env,target=DB_PASSWORD
Secret=limesurvey-admin-password,type=env,target=ADMIN_PASSWORD
Secret=limesurvey-encrypt-nonce,type=env,target=ENCRYPT_NONCE
Secret=limesurvey-encrypt-key,type=env,target=ENCRYPT_SECRET_BOX_KEY
AutoUpdate=registry
[Service]
Restart=always
TimeoutStartSec=300
DB_MYSQL_ENGINE=InnoDB replaces the image's default, MyISAM, for the response tables. Pull docker.io/library/mariadb:12.3 and docker.io/martialblog/limesurvey:7-apache, then:
systemctl --user daemon-reload
systemctl --user start limesurvey-pod
systemctl --user enable --now podman-auto-update.timer
podman secret inspect --showsecret --format '{{.SecretData}}' limesurvey-admin-password; echo
Add survey.example.com to Caddy with reverse_proxy 127.0.0.1:8141, and sign in as admin at https://survey.example.com/index.php/admin. The address without index.php, /admin, sent us to the public start page. Set the mail server under Configuration > Global settings > Email settings: SMTP, your provider's host with :587 under SMTP host, user, password and the SMTP encryption your provider uses. We tested sending through our test mail server only. Without email, invitations to participants and notifications fail; surveys still work. LimeSurvey warns "SSL not enforced" on its dashboard; Caddy serves it over HTTPS only, and we left Force HTTPS off.
To back up, as limesurvey:
umask 077
mkdir -p ~/backup
podman exec limesurvey-db sh -c 'mariadb-dump -ulimesurvey -p"$MARIADB_PASSWORD" --single-transaction limesurvey' > ~/backup/limesurvey-db.sql
podman volume export limesurvey-upload > ~/backup/limesurvey-upload.tar
for s in $(podman secret ls --format '{{.Name}}'); do
printf '%s %s\n' "$s" "$(podman secret inspect --showsecret --format '{{.SecretData}}' "$s")"
done > ~/backup/secrets.txt
To restore for a new user: create the secrets from secrets.txt, podman volume create limesurvey-upload and import the tar file into it, start limesurvey-db, load the dump with podman exec -i limesurvey-db sh -c 'mariadb -ulimesurvey -p"$MARIADB_PASSWORD" limesurvey' < ~/backup/limesurvey-db.sql, and restart limesurvey-app.
Licence and cost
We read the licence texts and pricing pages on 14 October 2026.
- Formbricks is made by Formbricks GmbH in Germany. Its core is open source under the AGPL 3.0; the paid features are in the same image, under Formbricks' own Enterprise licence, and unlock with a key. According to its licence page, the free Community edition has unlimited responses and users, one workspace, webhooks, the API, file uploads and multi-language surveys. A key is needed for, among others, hiding "Powered by Formbricks", teams and access roles, contacts and segments, quotas, audit logs, single sign-on with OpenID Connect or SAML, reCAPTCHA spam protection, two-factor sign-in and more workspaces. Self-hosted, it is priced per response, workspace and feature, with no public price. Formbricks Cloud is free for 250 responses a month, hosted in Frankfurt; Pro is listed at 74 and Scale at 325 US dollars a month, for 2,000 and 5,000 responses.
- LimeSurvey is made by LimeSurvey GmbH in Germany and is open source under the GPL, version 2 or later, according to its licence file. The Community Edition has no paid features. LimeSurvey Cloud is listed, including 25 % VAT, at 380 kr a month for Basic with 1,000 responses a month, 3,887 kr a year for Expert with 10,000 responses a year, and 10,053 kr a year for Business with 50,000; you pick where it is hosted, with Germany and Finland suggested for EU customers.
For a company running surveys for itself, both free versions allow it at no cost, with no limit on responses, and on your own server the answers stay in Sweden.
Typeform, Google Forms and SurveyMonkey today
What follows is from the services' own pages, read on 14 October 2026, not from our tests. None of the three has a version to run yourself.
Typeform's pricing page showed, in US dollars per month billed yearly, Basic at 28 with 100 responses a month, Plus at 56 with 1,000, and Business at 91 with 10,000; billed monthly, 39, 79 and 129. Conditional logic, webhooks and integrations come with Basic, and removing the Typeform branding with Plus. Multi-language forms are listed on the Talent and Growth Flow plans, and "SSO, HIPAA, GDPR, US or EU data center" on Enterprise. The same page also had a comparison table with lower prices than its cards, so check before you buy. It mentions a free plan without showing it among the plans.
Google Forms is free with a Google account, and part of Google Workspace, which the pricing page listed from 6.80 euros per user a month, excluding VAT; data regions are listed under Enterprise. A file upload question needs every respondent to sign in: "To answer this question, responders need to sign in to a Google Account", and the files land in the owner's Google Drive, according to Google's help page. Branching, Go to section based on answer, "is only available for Multiple choice and Dropdown question types", according to another help page.
SurveyMonkey's pricing page showed a free Basic plan with 10 questions per survey, Standard Monthly at 409 kr a month for 1,000 responses a month, Advantage Annual at 4,428 kr a year for 15,000 responses a year, and Premier Annual at 10,788 kr a year for 40,000, each for one user, with extra responses at 1 kr each. Its team plans start at 300 kr per user a month, billed yearly, for at least three users; multilingual surveys are listed from Team Premier, at 675 kr, and an EU data center under Enterprise.
What you give up compared with Typeform, Google Forms and SurveyMonkey
Moving to your own server keeps the answers in Sweden, costs a fixed server price instead of a price per response or user, and nobody caps your responses. But be honest about what you leave behind:
- Polish and templates. Typeform's designs, video questions and AI form building, and SurveyMonkey's question bank and benchmarks, have no equal here. Formbricks comes closest in look; LimeSurvey looks dated.
- Integrations. Google Forms writes straight into Google Sheets, and Typeform and SurveyMonkey connect to hundreds of services. Here you have webhooks and an API in Formbricks, an API you poll in LimeSurvey, and some work of your own.
- Respondent panels. SurveyMonkey sells responses from its own panel. Neither self-hosted tool finds respondents for you.
- Spam protection. Formbricks' reCAPTCHA is paid; LimeSurvey's CAPTCHA is free but simple. Our 200 scripted respondents went through both unhindered.
- Running it. Updates, backups, email and the server are yours, and Formbricks in particular is a stack of seven services that changes with each major version.
Which one to pick
Customer feedback, NPS and surveys on your website: Formbricks. It has the Typeform-like form, NPS with a score, charts that need no setup, signed webhooks, a REST API, uploads, logic and two languages in the free version, and it worked embedded on another domain. Its weak points are the weight, about 840 MB at rest and 3.3 GB of images, seven services to keep running and a restore that needs care, everyone an owner without a licence, reCAPTCHA, two-factor sign-in and single sign-on only with a paid key, uploads left behind when you delete a response, and a CSV export that lost our Swedish matrix answers. There is no guide of ours yet: the files in How we ran Formbricks are what we ran, on top of the Podman guide. It runs comfortably on a VC-P Alloy.
Research, long questionnaires, many languages or many users: LimeSurvey. Permissions per survey, groups, CAPTCHA, two-factor sign-in, statistics and exports to SPSS, R and Stata are all free, under the GPL, in about 150 MB. Its weak points are an older interface, no webhooks, no NPS score, a 2 MB upload limit in the image we used, a community-made image, and surveys that would not work embedded on another domain, so link to them instead. The files are in How we ran LimeSurvey; a VC-P Alloy runs it with room for many other apps.
Simple internal forms that today live in Google Forms: either works. LimeSurvey is the lighter one to run; Formbricks the nicer one to fill in.
Together they used about 1 GB at rest and 1.3 GB at their peaks, so one VC-P Alloy runs both.