What you will set up
Documenso sends PDFs out for signature, much like DocuSign. You upload an agreement, place fields where each person signs, and Documenso emails every recipient a link. They sign in the browser, without an account. Once everyone has signed, Documenso adds a page with the signing history, seals the PDF with a digital signature from your own certificate, and emails the finished copy to everyone.
Documenso is made by Documenso, Inc., which sells a hosted version and an Enterprise Edition. The code is open source under the AGPL 3.0 licence; a few parts, listed in the code as Enterprise Edition, such as billing and the email-domains feature, need a paid licence in production. Nothing in this guide uses them. Out of the box, Documenso sends its version and an installation ID to its maker every hour, according to its documentation, and in our tests the image asked npm's registry for a newer npm at every start. Step 5 switches both off.
Here Documenso runs as a pod under a user of its own called documenso, with PostgreSQL for its data and the documents themselves, behind Caddy from the Podman guide.
Every step below was run on a fresh Melonslab VC-P Alloy (2 vCPU, 8 GB) with Debian 13:
- Documenso 2.20.0 started with PostgreSQL 17 and a self-signed certificate made in step 3, and reported the certificate as ready.
- The first account signed up and confirmed its address by email; an address at another domain was refused. After that, sign-ups were closed, and the sign-up page sent visitors to the login page. An account created from the admin panel got an email to set its password, and joined the organisation through an invitation.
- A PDF was sent for signature, and the recipient signed it from the link in the email. The finished PDF arrived by email with a signing history page, and
pdfsigreported "Signature is Valid" with the certificate from step 3, and "Certificate issuer isn't Trusted", as expected for a self-signed one. - A document was sent without email, with signing links to copy instead, and was signed from the link.
- The signing history showed the signer's real address over IPv4 and IPv6, and ignored a forged
X-Forwarded-Forheader. - With the settings from step 5, a capture of the server's traffic showed no outside connections while Documenso started and while documents were sent and signed, apart from looking up the SMTP server.
- Documenso was restored from a backup, with the keys and the certificate, after which a pending document was signed and sealed with the restored certificate. An update from 2.19.0 to 2.20.0, by changing the image tag, applied its database change by itself, and everything came back by itself after a reboot.
The pod used about 300 MB of memory at rest, and about 380 MB after a document had been signed and sealed, nearly all of it Documenso itself.
Before you start
You need:
- a server set up as in the Podman guide, with Caddy running;
- an A record and an AAAA record for
sign.example.compointing at your server; - an email account at your email provider for Documenso to send from, with its SMTP server name and password. Documenso cannot be set up without email: your account has to confirm its address before it can log in.
The examples use sign.example.com for Documenso, example.com as your company's email domain, sign@example.com as the sender, smtp.example.com as your provider's SMTP server, and Example AB as your company's name. Replace them throughout.
1. Create the user
As root:
useradd -m -s /bin/bash documenso
loginctl enable-linger documenso
machinectl shell documenso@
Everything up to step 6 runs as documenso.
2. Create the secrets
pw=$(openssl rand -hex 24)
printf %s "$pw" | podman secret create documenso-db-password -
printf 'postgresql://documenso:%s@127.0.0.1:5432/documenso' "$pw" | podman secret create documenso-database-url -
unset pw
openssl rand -hex 32 | tr -d '\n' | podman secret create documenso-auth-secret -
openssl rand -hex 32 | tr -d '\n' | podman secret create documenso-encryption-key -
openssl rand -hex 32 | tr -d '\n' | podman secret create documenso-encryption-key-2 -
The first two hold the database password, once for PostgreSQL and once in the address Documenso connects to. The third signs logins, and the last two encrypt secrets that Documenso keeps in the database, such as two-factor codes. Step 10 saves copies of the keys.
3. Make the signing certificate
When the last person has signed, Documenso signs the PDF itself with a certificate, so that anyone can check that it has not been changed since. Make your own, valid for ten years, with your company's name in it:
umask 077
mkdir ~/cert && cd ~/cert
openssl rand -hex 24 | tr -d '\n' > passphrase
openssl req -x509 -newkey rsa:3072 -noenc -keyout key.pem -out cert.pem -days 3650 -subj "/C=SE/O=Example AB/CN=Example AB document signing"
openssl pkcs12 -export -inkey key.pem -in cert.pem -name "Example AB" -passout file:passphrase -out cert.p12
base64 -w0 cert.p12 | podman secret create documenso-signing-cert -
podman secret create documenso-signing-passphrase passphrase
cd && rm -r ~/cert
The certificate and its private key go into a .p12 file protected by a random passphrase, which Documenso requires. Both are stored as Podman secrets, and the files are deleted. Ten years, instead of the one year in Documenso's documentation, means you will not have to replace it soon: a document signed after the certificate has expired gets a warning in PDF readers.
The certificate is self-signed, so a PDF reader can show that the document is unchanged and who sealed it, but not that a trusted authority vouches for your name, and Adobe Acrobat shows no green tick. For a green tick in Acrobat, buy a document signing certificate from a provider on Adobe's trust list, and store it in the two secrets instead.
4. Store the email password
Documenso emails each signing request, the finished documents, invitations and password resets. As documenso, store the password of the email account it sends from:
printf %s 'the-smtp-password' | podman secret create documenso-smtp-password -
Step 5 points Documenso at your provider's SMTP server on port 587, which uses STARTTLS. Step 9 lists what depends on email.
5. Define the pod
mkdir -p ~/.config/containers/systemd
cd ~/.config/containers/systemd
Create documenso.pod:
[Pod]
PodName=documenso
# Only Caddy, on this server, can reach Documenso: the port is not open to the internet.
PublishPort=127.0.0.1:8127:3000
[Install]
WantedBy=default.target
Create documenso-db.container:
[Container]
ContainerName=documenso-db
Image=docker.io/library/postgres:17-alpine
Pod=documenso.pod
Volume=documenso-db:/var/lib/postgresql/data
Environment=POSTGRES_USER=documenso POSTGRES_DB=documenso
Secret=documenso-db-password,type=env,target=POSTGRES_PASSWORD
HealthCmd=pg_isready -U documenso -d documenso
HealthInterval=10s
Notify=healthy
AutoUpdate=registry
[Service]
Restart=always
And documenso-app.container:
[Unit]
Requires=documenso-db.service
After=documenso-db.service
[Container]
ContainerName=documenso-app
Image=docker.io/documenso/documenso:v2.20.0
Pod=documenso.pod
Environment=NEXT_PUBLIC_WEBAPP_URL=https://sign.example.com
Environment=NEXT_PRIVATE_INTERNAL_WEBAPP_URL=http://127.0.0.1:3000
Environment=NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS=example.com
Environment=DOCUMENSO_DISABLE_TELEMETRY=true CHECKPOINT_DISABLE=1 NPM_CONFIG_UPDATE_NOTIFIER=false
Secret=documenso-database-url,type=env,target=NEXT_PRIVATE_DATABASE_URL
Secret=documenso-auth-secret,type=env,target=NEXTAUTH_SECRET
Secret=documenso-encryption-key,type=env,target=NEXT_PRIVATE_ENCRYPTION_KEY
Secret=documenso-encryption-key-2,type=env,target=NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY
Secret=documenso-signing-cert,type=env,target=NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS
Secret=documenso-signing-passphrase,type=env,target=NEXT_PRIVATE_SIGNING_PASSPHRASE
Environment=NEXT_PRIVATE_SMTP_HOST=smtp.example.com NEXT_PRIVATE_SMTP_PORT=587
Environment=NEXT_PRIVATE_SMTP_USERNAME=sign@example.com
Environment=NEXT_PRIVATE_SMTP_FROM_ADDRESS=sign@example.com "NEXT_PRIVATE_SMTP_FROM_NAME=Example AB"
Secret=documenso-smtp-password,type=env,target=NEXT_PRIVATE_SMTP_PASSWORD
[Service]
Restart=always
What the settings do:
NEXT_PUBLIC_WEBAPP_URLis the address Documenso puts in every link and email. It must be the publichttps://address.NEXT_PRIVATE_INTERNAL_WEBAPP_URLis where Documenso sends its own background jobs, such as sealing a finished document. Inside the pod, the public address would reach the pod itself instead of Caddy, as the Podman guide explains, so they go straight to the app.NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINSlets only addresses at your own domain sign up, and only after confirming the address by email. Step 7 closes sign-ups completely once your account exists.DOCUMENSO_DISABLE_TELEMETRYstops the hourly report to Documenso, Inc. of its version and an installation ID.CHECKPOINT_DISABLEstops a report from Prisma, the database tool it uses, andNPM_CONFIG_UPDATE_NOTIFIER=falsestops npm's update check at each start.- The signing certificate is given to Documenso as the contents of a secret, so no file has to be mounted into the container.
- Documents are stored in PostgreSQL, Documenso's default, so the database backup in step 10 includes every PDF.
- The SMTP lines send email through your provider with the account from step 4. Port 587 starts unencrypted and switches to TLS with STARTTLS; for port 465, which uses TLS from the start, add
NEXT_PRIVATE_SMTP_SECURE=trueand change the port. - The image is pinned to the version we tested, as Documenso recommends; step 11 updates it. PostgreSQL stays on version 17 and gets its minor updates from
podman auto-update.
Start it:
systemctl --user daemon-reload
systemctl --user start documenso-pod
systemctl --user enable --now podman-auto-update.timer
The first start downloads about 1.8 GB of images and sets up the database, which takes a few minutes. It is ready when this shows "status":"ok", with ok for both the database and the certificate:
curl -s http://127.0.0.1:8127/api/health
6. Put Caddy in front
Go back to root with exit, switch to machinectl shell caddy@, and add this block at the end of ~/Caddyfile:
sign.example.com {
reverse_proxy 127.0.0.1:8127
}
Restart Caddy with systemctl --user restart caddy.
Caddy sets the X-Forwarded-For header to the visitor's real address and replaces any value the visitor sent. Documenso takes the address from there for the signing history, so it records each signer's real address.
7. Create your account and close sign-ups
Open https://sign.example.com/signup, and sign up with your name, your address at example.com, a long password and a signature, then Create account. Documenso emails you a link to confirm your address; open it. An address at another domain gets "Signup is currently disabled or not available for your email domain."
Documenso has no setup screen for the first administrator. Make your account one in the database: go back to root with exit, switch to machinectl shell documenso@, and run, with your address:
podman exec documenso-db psql -U documenso -c "UPDATE \"User\" SET roles = '{ADMIN,USER}' WHERE email = 'you@example.com';"
It prints UPDATE 1. Log in at https://sign.example.com, and Admin Panel appears in the menu under your name.
Then close sign-ups. In ~/.config/containers/systemd/documenso-app.container, replace the line with NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS with:
Environment=NEXT_PUBLIC_DISABLE_SIGNUP=true
systemctl --user daemon-reload
systemctl --user restart documenso-app
Now https://sign.example.com/signup goes to the login page, and new sign-ups are refused. You add people yourself:
- Admin Panel > Users > Create User, with their email address and name. Documenso emails them a link to set a password.
- Invite them to your organisation, so that they can send documents from it. At
https://sign.example.com/settings, choose Members under Organization Settings, then Invite member. They accept from the email, or from View invites when they log in. Until then, they can only sign documents sent to them.
Your account starts with an organisation called "Personal Organisation" and a team called "Personal Team", which you can rename under General in their settings. People who only sign documents need no account.
8. Send a document for signature
- On Documents, choose Upload Document and pick a PDF.
- Under Recipients, enter each person's Email and Name, and Add Signer for more. The menu next to each sets their role, such as Needs to sign, Needs to approve or Receives copy.
- Add Fields: choose Signature, and click where the recipient should sign. Name, Date, Text and the other fields work the same way.
- Send Document, and Send.
Each recipient gets an email with a link, opens the document, fills in their fields, and chooses Complete and Sign. When everyone has signed, Documenso seals the PDF and emails it, as an attachment, to you and every recipient. The last page is the signing certificate, with each signer's email address, the times the document was sent, viewed and signed, in UTC, and the signer's IP address and browser.
To check the seal, open the PDF in a reader that shows signatures. On Linux, pdfsig agreement.pdf from poppler-utils prints the name from your certificate and "Signature is Valid". Any change to the PDF afterwards makes the signature invalid.
Documenso has no Swedish interface. It is translated into German, Spanish, French, Italian, Dutch, Polish, Portuguese, Albanian, Japanese, Korean and Chinese, besides English.
9. Email
Documenso needs email for:
- confirming the address of a new account, which cannot log in before that;
- the link to set a password for an account created in the admin panel, and Forgot your password?;
- invitations to an organisation;
- signing requests, and the finished document sent to everyone.
We tested sending through STARTTLS on port 587 with a password, to a test mail server on the server itself, not through a real provider.
Without email, you can still have documents signed: in Send Document, choose None instead of Email, and Generate Links. Copy Signing Links on the document then gives you each recipient's link to send them yourself, and nobody gets an email.
10. Back up
As documenso:
umask 077
mkdir -p ~/backup
podman exec documenso-db pg_dump -U documenso documenso > ~/backup/documenso.sql
for s in documenso-auth-secret documenso-encryption-key documenso-encryption-key-2 documenso-signing-cert documenso-signing-passphrase; do
podman secret inspect --showsecret --format '{{.SecretData}}' "$s" > ~/backup/"$s"
done
The database holds the accounts, the documents, both the uploaded and the signed PDFs, and the signing history. The other files are the keys from step 2 and the signing certificate with its passphrase. Copy ~/backup to another machine, for example with restic, and store it safely: with the certificate, anyone can seal PDFs in your company's name.
To restore, as documenso, with the backup in ~/backup:
systemctl --user stop documenso-app
podman exec documenso-db dropdb -U documenso --force documenso
podman exec documenso-db createdb -U documenso documenso
podman exec -i documenso-db psql -q -U documenso documenso < ~/backup/documenso.sql
systemctl --user start documenso-app
On a new server, create the five secrets from the backup instead of new ones in steps 2 and 3, before you start the pod. The database password can be new:
for s in documenso-auth-secret documenso-encryption-key documenso-encryption-key-2 documenso-signing-cert documenso-signing-passphrase; do
tr -d '\n' < ~/backup/"$s" | podman secret create "$s" -
done
11. Update
Documenso recommends a fixed version in production, so it is not part of podman auto-update. Read the release notes on GitHub, at github.com/documenso/documenso/releases, back up as in step 10, and change the tag in documenso-app.container to the new version, such as v2.21.0. Then:
systemctl --user daemon-reload
systemctl --user restart documenso-app
Documenso updates its database when it starts. We updated from 2.19.0 to 2.20.0 this way, and had to log in again afterwards.
Troubleshooting
The health check shows the certificate as not ok, or signing fails with "Failed to get private key bags" or "mac verify failure". The certificate or its passphrase secret is wrong. Make both again as in step 3, after podman secret rm documenso-signing-cert documenso-signing-passphrase, and restart documenso-app. The log line "Certificate not found or not readable" at start is about a certificate file, which this setup does not use; the health check is what counts.
Signing requests are not emailed, and signed documents stay in "Processing document". NEXT_PRIVATE_INTERNAL_WEBAPP_URL is missing, so Documenso sends its background jobs to the public address, which from inside the pod reaches nothing. Add it as in step 5, and restart. Documents signed in the meantime show up under Admin Panel > Unsealed Documents, where Reseal finishes them.
No email arrives. journalctl --user -u documenso-app shows a line such as [JOBS]: Job send.document.completed.emails failed Error: connect ECONNREFUSED, with the reason. Check the server name, port and user name in documenso-app.container, and the password in documenso-smtp-password; to change the password, podman secret rm documenso-smtp-password, create it again as in step 4, and restart documenso-app.
"Unable to sign in. This account has not been verified." The account has not confirmed its address. Send confirmation email on that page sends the link again.