Documenso
Documenso sends documents for signature and records the result. Two containers — the application and PostgreSQL.
Last run on a host 8 Nov 2026
Pinned at v2.21.1 (released 22 Oct 2026) · the newest upstream release
In the upstream images: 1 CRITICAL · 45 HIGH with a published fix — per image
To try it: Docker and OpenSSL — the block generates the signing certificate the local stack mounts. See Try it locally.
To deploy it behind the proxy: Traefik running, a domain, SMTP, and OpenSSL for the certificate.
Try it locally
Section titled “Try it locally”On your own machine, with no proxy, domain or TLS certificate. From
business/documenso/; the local stack mounts a signing certificate, so the
block generates one:
cp .env.local.example .env.local
# The secrets ship empty; generate the ones this stack needsSIGNING_PASSPHRASE=$(openssl rand -hex 16){ echo "DB_PASSWORD=$(openssl rand -hex 32)" echo "NEXTAUTH_SECRET=$(openssl rand -base64 32 | tr -d '\n')" echo "ENCRYPTION_KEY=$(openssl rand -base64 32 | tr -d '\n')" echo "ENCRYPTION_SECONDARY_KEY=$(openssl rand -base64 32 | tr -d '\n')" echo "SIGNING_PASSPHRASE=$SIGNING_PASSPHRASE"} >> .env.local
# The signing certificate the local stack mounts, sealed with that passphrasemkdir -p .secretsopenssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 3650 -nodes -subj "/CN=Documenso"openssl pkcs12 -export -out .secrets/cert.p12 -inkey key.pem -in cert.pem -passout pass:"$SIGNING_PASSPHRASE"rm key.pem cert.pem
docker compose -f docker-compose.local.yml --env-file .env.local up -dThen open http://localhost:3000. Stop it again with the same command and down.
Documenso ships no signing certificate. You generate one during installation, and every signature the instance produces is made with it — see Backup.
Installation
Section titled “Installation”From business/documenso/:
-
Copy the environment file and set
APP_TRAEFIK_HOST,TZand theSMTP_*block:Terminal window cp .env.example .env -
Generate the secrets and store the SMTP password:
Terminal window mkdir -p .secrets volumes/postgresopenssl rand -hex 32 > .secrets/db_pwd.txtopenssl rand -base64 32 | tr -d '\n' > .secrets/nextauth_secret.txtopenssl rand -base64 32 | tr -d '\n' > .secrets/encryption_key.txtopenssl rand -base64 32 | tr -d '\n' > .secrets/encryption_secondary_key.txtprintf '%s' 'YOUR-SMTP-PASSWORD' > .secrets/smtp_password.txtDocumenso has no
_FILEform, so the stack’s entrypoint reads these files and exports the values before starting the application. -
Create the signing certificate and its passphrase:
Terminal window openssl req -x509 -newkey rsa:4096 -keyout /tmp/k.pem -out /tmp/c.pem \-days 3650 -nodes -subj "/CN=Documenso"PASS="$(openssl rand -hex 16)"printf '%s' "$PASS" > .secrets/signing_passphrase.txtopenssl pkcs12 -export -out .secrets/cert.p12 -inkey /tmp/k.pem -in /tmp/c.pem \-passout pass:"$PASS"rm /tmp/k.pem /tmp/c.pemCopy
cert.p12and its passphrase off the host now. -
Start it. First boot runs database migrations:
Terminal window docker compose up -ddocker compose logs documenso-app --follow -
Over the VPN, open
https://<APP_TRAEFIK_HOST>and create the first account — it becomes the administrator. The route ships closed (access-vpn) for that reason; once the account exists, setAPP_TRAEFIK_ACCESS=access-publicin.envand rundocker compose up -dso signers can reach it.
Verify
Section titled “Verify”docker compose ps # two services, healthydocker inspect documenso-app | grep -c SECRET # 0 — nothing injected is visible hereThen send one document to yourself and sign it. A signed document is the only check that covers the certificate and the passphrase together; both can be wrong while the application runs normally.
Going further
Section titled “Going further”Embedding is blocked by default
Section titled “Embedding is blocked by default”Documenso ships embedding SDKs that put the signing flow in an iframe on another
site. The stack sends X-Frame-Options: DENY through headers-standard, so none
of them work as shipped.
Signing is the moment a signature acquires legal weight. A page any site may frame is a page any site can overlay — the person clicks what looks like your button and signs whatever was placed underneath. The header is what prevents that.
To use the SDKs, name the parent origins rather than removing the header:
frame-self@file placed before ${APP_TRAEFIK_HEADERS}@file for same-origin
framing, or a per-app middleware replacing X-Frame-Options with a
frame-ancestors allowlist. Neither has been run against Documenso’s embed flow
here.
Backup
Section titled “Backup”| Database | PostgreSQL, container documenso-db, database documenso, user documenso |
| Password | .secrets/db_pwd.txt |
| State | volumes/postgres |
| Also needed | .secrets/cert.p12 and .secrets/signing_passphrase.txt |
| Quiescing | Not needed — the dump is consistent on its own. |
For borgmatic:
postgresql_databases: - name: documenso username: documenso # The server's own client, inside its container pg_dump_command: docker exec documenso-db pg_dump pg_restore_command: docker exec -i documenso-db pg_restore psql_command: docker exec -i documenso-db psqlcert.p12 is not a secret like the others. Every signature this instance has
ever produced was made with it, so a database restored against a freshly
generated certificate leaves signed documents that no longer validate. Back it up
with the same care as the database and keep a copy off the host.
Restore order: database, then the application.
Updates
Section titled “Updates”-
Read the release notes.
-
Back up the database, the certificate and the passphrase together.
-
Raise
APP_TAGin.env, then:Terminal window docker compose pull && docker compose up -ddocker compose logs documenso-app --followMigrations run on boot.
-
Sign one document. A migration that touched the signature tables shows up there and not in the log.
Troubleshooting
Section titled “Troubleshooting”Documents cannot be signed, but the application runs. The certificate or the
passphrase is wrong. cert.p12 is mounted read-only at
/opt/documenso/cert.p12, and the passphrase comes from
.secrets/signing_passphrase.txt — the two have to match the pair created in
step 3.
The embed shows a blank frame. X-Frame-Options: DENY from headers-standard.
See Embedding.
Signature request mail never arrives. The SMTP_* block or
.secrets/smtp_password.txt. Nothing else in the stack depends on mail, so the
application itself gives no sign of it.
Repository files
Section titled “Repository files”business/documenso/README.md— setup, security model, the first-deploy checklistbusiness/documenso/UPSTREAM.md— source, upgrade checklist, deviationsbusiness/opensign/— e-signature
Official sources
Section titled “Official sources”© 2026 SecDockBlueCode Apache-2.0Site content CC BY-NC 4.0