Skip to content

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.

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:

Terminal window
cp .env.local.example .env.local
# The secrets ship empty; generate the ones this stack needs
SIGNING_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 passphrase
mkdir -p .secrets
openssl 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 -d

Then 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.

From business/documenso/:

  1. Copy the environment file and set APP_TRAEFIK_HOST, TZ and the SMTP_* block:

    Terminal window
    cp .env.example .env
  2. Generate the secrets and store the SMTP password:

    Terminal window
    mkdir -p .secrets volumes/postgres
    openssl rand -hex 32 > .secrets/db_pwd.txt
    openssl rand -base64 32 | tr -d '\n' > .secrets/nextauth_secret.txt
    openssl rand -base64 32 | tr -d '\n' > .secrets/encryption_key.txt
    openssl rand -base64 32 | tr -d '\n' > .secrets/encryption_secondary_key.txt
    printf '%s' 'YOUR-SMTP-PASSWORD' > .secrets/smtp_password.txt

    Documenso has no _FILE form, so the stack’s entrypoint reads these files and exports the values before starting the application.

  3. 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.txt
    openssl pkcs12 -export -out .secrets/cert.p12 -inkey /tmp/k.pem -in /tmp/c.pem \
    -passout pass:"$PASS"
    rm /tmp/k.pem /tmp/c.pem

    Copy cert.p12 and its passphrase off the host now.

  4. Start it. First boot runs database migrations:

    Terminal window
    docker compose up -d
    docker compose logs documenso-app --follow
  5. 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, set APP_TRAEFIK_ACCESS=access-public in .env and run docker compose up -d so signers can reach it.

Terminal window
docker compose ps # two services, healthy
docker inspect documenso-app | grep -c SECRET # 0 — nothing injected is visible here

Then 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.

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.

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 psql

cert.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.

  1. Read the release notes.

  2. Back up the database, the certificate and the passphrase together.

  3. Raise APP_TAG in .env, then:

    Terminal window
    docker compose pull && docker compose up -d
    docker compose logs documenso-app --follow

    Migrations run on boot.

  4. Sign one document. A migration that touched the signature tables shows up there and not in the log.

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.