Analytics cookies only (Google Analytics, PostHog). No ad networks, no cross-site tracking. See our privacy policy.
A no-code form builder with form logic, file uploads and other input types, embeds, email notifications, captcha protection, form analytics and Slack, Discord and webhook integrations. The core is AGPL-3.0 (the api/app/Enterprise directory carries its own licence); it runs as a Laravel API with a queue worker and scheduler, a Nuxt UI, PostgreSQL, Redis and an nginx ingress in front.
Runs on a VPS from $12/mo (Kamatera 4GB, 4 GB RAM) · prices checked Sep 2026
Pick OpnForm when you want a form builder that non-technical colleagues can use on their own: a no-code editor with form logic, file uploads, embeds, email notifications, captcha protection, analytics and Slack, Discord and webhook integrations, plus an API and an MCP server for agents. The core is AGPL-3.0 PHP (Laravel) with a Nuxt front end; the `api/app/Enterprise` directory is under its own licence, and the Docker deployment page says a free self-hosted instance is limited to 2 users, with more needing an Enterprise licence. Our snippet is the repo's compose file with pinned images, the secrets the `setup-env.sh` script would have generated written into `.env`, and the relative URL defaults from `client/.env.docker`. Upstream publishes no RAM minimum, so our 2 GB floor is an estimate for six containers with a 1 GB PHP memory limit. In the catalog, Formbricks is survey and experience-management first and HeyForm is a conversational one-question-at-a-time form; OpnForm is the classic form builder of the three.
Paid link — we earn a commission if you shop through it.
Run these commands on your server:
# OpnForm — form builder: Laravel API + worker + scheduler, Nuxt UI, PostgreSQL, Redis, nginx ingress — based on the repo's docker-compose.yml, docker/nginx.conf and the api/.env.docker + client/.env.docker templates, pinned to v2.6.0
curl -fsSLo nginx.conf https://raw.githubusercontent.com/OpnForm/OpnForm/v2.6.0/docker/nginx.conf
echo "APP_KEY=base64:$(openssl rand -base64 32)" > .env
echo "JWT_SECRET=$(openssl rand -hex 20)" >> .env
echo "FRONT_API_SECRET=$(openssl rand -hex 20)" >> .env
echo "DB_PASSWORD=$(openssl rand -hex 16)" >> .env
cat > docker-compose.yml <<'EOF'
services:
api: &api
image: jhumanj/opnform-api:2.6.0
container_name: opnform-api
restart: unless-stopped
volumes:
- opnform_storage:/usr/share/nginx/html/storage
environment:
APP_NAME: OpnForm
APP_ENV: production
APP_DEBUG: "false"
APP_KEY: ${APP_KEY}
APP_URL: http://SERVER_IP
FRONT_URL: http://SERVER_IP
LOG_CHANNEL: errorlog
DB_CONNECTION: pgsql
DB_HOST: db
DB_DATABASE: opnform
DB_USERNAME: opnform
DB_PASSWORD: ${DB_PASSWORD}
REDIS_HOST: redis
CACHE_DRIVER: redis
QUEUE_CONNECTION: redis
SESSION_DRIVER: redis
FILESYSTEM_DRIVER: local
LOCAL_FILESYSTEM_VISIBILITY: public
MAIL_MAILER: log
JWT_SECRET: ${JWT_SECRET}
JWT_TTL: "1440"
JWT_SKIP_IP_UA_VALIDATION: "false"
FRONT_API_SECRET: ${FRONT_API_SECRET}
PHP_MEMORY_LIMIT: 1G
PHP_MAX_EXECUTION_TIME: "600"
PHP_UPLOAD_MAX_FILESIZE: 64M
PHP_POST_MAX_SIZE: 64M
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "php /usr/share/nginx/html/artisan about || exit 1"]
interval: 30s
timeout: 15s
retries: 3
start_period: 60s
api-worker:
<<: *api
container_name: opnform-api-worker
command: ["php", "artisan", "queue:work"]
healthcheck:
test: ["CMD-SHELL", "pgrep -f 'php artisan queue:work' > /dev/null || exit 1"]
interval: 60s
timeout: 10s
retries: 3
start_period: 30s
api-scheduler:
<<: *api
container_name: opnform-api-scheduler
command: ["php", "artisan", "schedule:work"]
healthcheck:
test: ["CMD-SHELL", "php /usr/share/nginx/html/artisan app:scheduler-status --mode=check --max-minutes=3 || exit 1"]
interval: 60s
timeout: 30s
retries: 3
start_period: 70s
ui:
image: jhumanj/opnform-client:2.6.0
container_name: opnform-client
restart: unless-stopped
environment:
NUXT_PUBLIC_APP_URL: /
NUXT_PUBLIC_API_BASE: /api
NUXT_PRIVATE_API_BASE: http://ingress/api
NUXT_API_SECRET: ${FRONT_API_SECRET}
depends_on:
api:
condition: service_healthy
redis:
image: redis:7.4.11
restart: unless-stopped
volumes:
- redis_data:/data
healthcheck:
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
interval: 30s
timeout: 5s
db:
image: postgres:16.15
restart: unless-stopped
environment:
POSTGRES_DB: opnform
POSTGRES_USER: opnform
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U opnform -d opnform"]
interval: 30s
timeout: 5s
ingress:
image: nginx:1.31.7
restart: unless-stopped
volumes:
- ./nginx.conf:/etc/nginx/templates/default.conf.template
ports:
- 80:80
environment:
NGINX_MAX_BODY_SIZE: 64m
depends_on:
api:
condition: service_started
ui:
condition: service_started
volumes:
postgres_data:
opnform_storage:
redis_data:
EOF
docker compose up -d
timeout 300 bash -c 'until curl -fsS http://localhost/ >/dev/null; do sleep 5; done'
# → http://SERVER_IP:80 — the first visit redirects to the setup page where you create the admin account; APP_URL and FRONT_URL must be the public URL (replace SERVER_IP above)OpnForm's Docker deployment page and the repo's env templates flag these before you hand out a form link: