OpnForm
OpnForm builds forms with a drag-and-drop editor, conditional logic, file uploads and webhooks. Seven containers — a Laravel API with its worker and scheduler, a Nuxt frontend, nginx in front of both, PostgreSQL and Redis.
Last run on a host 8 Nov 2026 · data restored from a backup
Pinned at 2.8.4 (released 5 Nov 2026) · the newest upstream release
In the upstream images: 2 CRITICAL · 34 HIGH with a published fix — per image
To try it: Docker, nothing else. See Try it locally.
To deploy it behind the proxy: Traefik running, a domain, and SMTP if forms should notify anyone.
Try it locally
Section titled “Try it locally”On your own machine, with no proxy, domain or certificate. From apps/opnform/:
cp .env.local.example .env.local
# The secrets ship empty; generate the ones this stack needs{ echo "DB_PASSWORD=$(openssl rand -hex 32)" echo "APP_KEY=base64:$(openssl rand -base64 32)" echo "JWT_SECRET=$(openssl rand -base64 32 | tr -d '\n')" echo "FRONT_API_SECRET=$(openssl rand -base64 32 | tr -d '\n')"} >> .env.local
mkdir -p volumes/local/redissudo chown 999:1000 volumes/local/redis
docker compose -f docker-compose.local.yml --env-file .env.local up -dThen open http://localhost:8080. Stop it again with the same command and down.
Traefik routes to nginx alone. Everything else is reached through it — /api/*
goes to the Laravel backend, the rest to the frontend.
Installation
Section titled “Installation”From apps/opnform/:
-
Copy the environment file and set
APP_TRAEFIK_HOST,TZandMAIL_FROM_ADDRESS:Terminal window cp .env.example .env -
Generate the three application secrets and paste each into
.env:Terminal window echo "base64:$(openssl rand -base64 32)" # APP_KEYopenssl rand -base64 32 # JWT_SECRETopenssl rand -base64 32 # FRONT_API_SECRETLaravel has no
_FILEform for these, so they live in.envrather than in.secrets/. -
Generate the database password and mirror it into
.env:Terminal window mkdir -p .secretsopenssl rand -base64 32 | tr -d '\n' > .secrets/db_pwd.txtsed -i "s|^DB_PWD_INLINE=.*|DB_PWD_INLINE=$(cat .secrets/db_pwd.txt)|" .env -
Create the volumes. The API entrypoint creates the subdirectories and sets their ownership on first start:
Terminal window mkdir -p volumes/storage volumes/postgres volumes/redissudo chown 999:1000 volumes/redis # Redis cannot fix this ownership itself -
Start it and watch all seven come up:
Terminal window docker compose up -ddocker compose logs -fWait for
ready to handle connectionson the API, thenListening onfrom the frontend. -
Over the VPN, open
https://<APP_TRAEFIK_HOST>and create the first account — it becomes the workspace admin, with no setup secret, which is why the route ships closed (access-vpn). Later signups are refused (SELF_HOSTED=true). Then setAPP_TRAEFIK_ACCESS=access-publicin.envand rundocker compose up -d, so the forms reach their respondents.
Verify
Section titled “Verify”docker compose ps # seven servicesThen build one form, submit it with a file attached, and confirm both the answer and the file are there. That covers the frontend, the API, the queue worker and the storage mount — the parts that fail independently of each other.
Going further
Section titled “Going further”Upload size
Section titled “Upload size”NGINX_MAX_BODY_SIZE in .env caps what a respondent can attach, at 64 MB by
default. nginx rejects anything larger before the API sees it, so raising it in
the application alone has no effect.
The worker and the scheduler
Section titled “The worker and the scheduler”Notifications and webhooks run through the queue worker; scheduled tasks through the scheduler. Both are separate containers using the same image as the API. A form that saves answers but sends no notifications is usually the worker rather than the mail configuration.
Maintenance commands
Section titled “Maintenance commands”Laravel’s own commands run in the api service; the queue is worked by
api-worker, not by hand:
docker compose exec api php artisan cache:clear # application cachedocker compose exec api php artisan config:clear # configuration cacheBackup
Section titled “Backup”| Database | PostgreSQL, container opnform-db, database opnform, user opnform |
| Password | .secrets/db_pwd.txt |
| State | volumes/postgres · volumes/storage (files submitted through forms) |
| Rebuildable | volumes/redis |
| Quiescing | Not needed — the dump is consistent on its own. |
For borgmatic:
postgresql_databases: - name: opnform username: opnform # The server's own client, inside its container pg_dump_command: docker exec opnform-db pg_dump pg_restore_command: docker exec -i opnform-db pg_restore psql_command: docker exec -i opnform-db psqlAnswers are database rows; anything a respondent uploaded is in
volumes/storage. A database-only restore gives submissions whose attachments
are gone — and unlike most losses, that data cannot be regenerated without asking
the people who sent it.
Restore order: database, then the application. The worker and scheduler come up with it; a queue restored mid-flight retries.
Updates
Section titled “Updates”-
Read the release notes. The step from 1.x to 2.x is a major change.
-
Back up the database and
volumes/storagetogether. -
Raise
APP_TAGin.env, then:Terminal window docker compose pull && docker compose up -ddocker compose logs -f -
Open one existing form in the editor and submit it. A migration that changed the form schema surfaces there.
Troubleshooting
Section titled “Troubleshooting”404s for /api/_nuxt_icon/… in the browser console. The icon proxy path is
routed to the Laravel API instead of the frontend’s icon server. Upstream issue,
cosmetic — the application works.
Uploads fail with a permissions error. The entrypoint sets ownership on
volumes/storage at first start. If it could not, correct the ownership on the
host and restart the API container.
A form saves answers but sends nothing. Check the queue worker container before the mail settings.
Repository files
Section titled “Repository files”apps/opnform/README.md— setup, the service split, known issuesapps/opnform/UPSTREAM.md— source and upgrade checklist
Official sources
Section titled “Official sources”© 2026 SecDockBlueCode Apache-2.0Site content CC BY-NC 4.0