Documenso is an open-source document signing platform. This guide runs the official app with PostgreSQL in Docker Compose, a PKCS#12 signing certificate and Caddy for HTTPS. Documents are stored in PostgreSQL, so plan disk and backup capacity for your expected volume.
Prerequisites
- A RamNode KVM VPS running Ubuntu 24.04 LTS (the same steps apply to 26.04 LTS), 2 GB RAM, 1–2 vCPUs and at least 40 GB disk.
- A domain such as
sign.example.comwith an A record (and AAAA if using IPv6) pointing to the server. - Root SSH access initially, plus credentials for an authenticated SMTP relay. Documenso needs email to deliver signing requests.
Replace all example domains, names and passwords. See Docker Compose and DNS for background.
Prepare the server
Run as root. Verify the new user's SSH login in a second terminal before disabling root login.
apt update && apt -y full-upgrade
apt install -y rsync openssl ufw unattended-upgrades
timedatectl set-timezone UTC
adduser deploy
usermod -aG sudo deploy
rsync --archive --chown=deploy:deploy ~/.ssh /home/deployAfter successfully signing in as deploy, run as root:
cat > /etc/ssh/sshd_config.d/99-hardening.conf <<'CONFIG'
PermitRootLogin no
PasswordAuthentication no
CONFIG
sshd -t && systemctl restart ssh
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile && swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
dpkg-reconfigure -plow unattended-upgradesIf SSH uses a nonstandard port, allow that port before enabling UFW. Docker can bypass UFW for published ports, so only bind the app to 127.0.0.1. See cloud firewall.
Install Docker Engine and Compose
Run as deploy:
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker deploy
sudo tee /etc/docker/daemon.json > /dev/null <<'JSON'
{"log-driver":"json-file","log-opts":{"max-size":"10m","max-file":"3"}}
JSON
sudo systemctl restart dockerLog out and back in to apply group membership; confirm with docker run --rm hello-world and docker compose version.
Create the signing certificate
A self-signed certificate works for sealing PDFs, but PDF readers will identify its issuer as untrusted. For trusted signatures obtain a document-signing certificate from a CA. Run as deploy:
sudo mkdir -p /opt/documenso/cert
sudo chown -R deploy:deploy /opt/documenso
cd /opt/documenso/cert
openssl rand -hex 24 # save this as your signing passphrase
openssl req -x509 -newkey rsa:2048 -sha256 -days 3650 -nodes \
-keyout private.key -out certificate.crt \
-subj "/CN=Example Corp Document Signing/O=Example Corp"
openssl pkcs12 -export -out cert.p12 -inkey private.key -in certificate.crt \
-passout pass:YOUR_SIGNING_PASSPHRASE
shred -u private.key
sudo chown 1001:1001 cert.p12
sudo chmod 400 cert.p12Use a non-empty passphrase. Store the certificate and passphrase securely; they are required after a restore.
Deploy with Docker Compose
Generate a different value for each secret, then create /opt/documenso/.env:
cd /opt/documenso
openssl rand -hex 24 # database password
openssl rand -base64 32 # NEXTAUTH_SECRET
openssl rand -hex 32 # encryption key
openssl rand -hex 32 # secondary encryption keyDOMAIN=sign.example.com
POSTGRES_PASSWORD=change-me-db
NEXTAUTH_SECRET=change-me-nextauth
ENCRYPTION_KEY=change-me-key-1
ENCRYPTION_SECONDARY_KEY=change-me-key-2
SIGNING_PASSPHRASE=change-me-signing
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USERNAME=smtp-user
SMTP_PASSWORD=smtp-password
SMTP_FROM_NAME=Example Corp Signing
SMTP_FROM_ADDRESS=sign@example.comchmod 600 /opt/documenso/.envKeep both encryption keys: changing or losing them can make encrypted database data unreadable. Create /opt/documenso/compose.yml:
services:
documenso-db:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_USER: documenso
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: documenso
volumes:
- documenso-db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U documenso -d documenso"]
interval: 10s
timeout: 5s
retries: 5
documenso:
image: documenso/documenso:latest
restart: unless-stopped
environment:
PORT: 3000
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
NEXT_PRIVATE_ENCRYPTION_KEY: ${ENCRYPTION_KEY}
NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY: ${ENCRYPTION_SECONDARY_KEY}
NEXT_PUBLIC_WEBAPP_URL: https://${DOMAIN}
NEXT_PRIVATE_INTERNAL_WEBAPP_URL: http://localhost:3000
NEXT_PRIVATE_DATABASE_URL: postgresql://documenso:${POSTGRES_PASSWORD}@documenso-db:5432/documenso
NEXT_PRIVATE_DIRECT_DATABASE_URL: postgresql://documenso:${POSTGRES_PASSWORD}@documenso-db:5432/documenso
NEXT_PUBLIC_UPLOAD_TRANSPORT: database
NEXT_PRIVATE_SIGNING_TRANSPORT: local
NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH: /opt/documenso/cert.p12
NEXT_PRIVATE_SIGNING_PASSPHRASE: ${SIGNING_PASSPHRASE}
NEXT_PRIVATE_SMTP_TRANSPORT: smtp-auth
NEXT_PRIVATE_SMTP_HOST: ${SMTP_HOST}
NEXT_PRIVATE_SMTP_PORT: ${SMTP_PORT}
NEXT_PRIVATE_SMTP_USERNAME: ${SMTP_USERNAME}
NEXT_PRIVATE_SMTP_PASSWORD: ${SMTP_PASSWORD}
NEXT_PRIVATE_SMTP_FROM_NAME: ${SMTP_FROM_NAME}
NEXT_PRIVATE_SMTP_FROM_ADDRESS: ${SMTP_FROM_ADDRESS}
volumes:
- ./cert/cert.p12:/opt/documenso/cert.p12:ro
depends_on:
documenso-db:
condition: service_healthy
ports:
- "127.0.0.1:3000:3000"
volumes:
documenso-db:cd /opt/documenso
docker compose config --quiet
docker compose up -d
docker compose logs -f documensoFor SMTP port 465 (implicit TLS), add NEXT_PRIVATE_SMTP_SECURE: "true" under the app environment. Pin the app image to a tested release tag once confirmed.
Configure Caddy and HTTPS
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update && sudo apt install -y caddyPut this in /etc/caddy/Caddyfile:
sign.example.com {
encode zstd gzip
request_body {
max_size 50MB
}
reverse_proxy 127.0.0.1:3000
}sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddyCaddy obtains an HTTPS certificate when DNS resolves and ports 80/443 are reachable. Increase max_size for larger PDFs.
First-run setup
Open https://sign.example.com/signup. Create an account, verify its email (also tests SMTP), and enable two-factor authentication in Settings > Security. Upload a test PDF, send yourself a signature request, sign and inspect the resulting PDF signature. A self-signed issuer warning is expected.
After creating your team's accounts, add NEXT_PUBLIC_DISABLE_SIGNUP: "true" to the app environment and run docker compose up -d; verify /signup no longer accepts registrations. Create a team for shared templates and set email branding as needed.
Backups and restore
The PostgreSQL dump holds documents, signatures and audit logs. Back it up together with .env, compose.yml and cert/cert.p12. Create /opt/documenso/backup.sh:
#!/usr/bin/env bash
set -euo pipefail
cd /opt/documenso
TS=$(date +%F-%H%M)
DEST=/var/backups/documenso
mkdir -p "$DEST"
docker compose exec -T documenso-db pg_dump -U documenso -Fc documenso > "$DEST/db-$TS.dump"
tar czf "$DEST/config-$TS.tar.gz" .env compose.yml cert/cert.p12
chmod 600 "$DEST"/*
find "$DEST" -type f -mtime +14 -deleteRun as root to read the certificate:
sudo chmod 700 /opt/documenso/backup.sh
sudo /opt/documenso/backup.sh
sudo crontab -e
# Add: 45 3 * * * /opt/documenso/backup.shCopy the backups off the VPS; for signed contracts choose retention based on legal requirements. See backups and object storage. To restore, unpack the config archive into /opt/documenso, set chown 1001:1001 cert/cert.p12, then:
cd /opt/documenso
docker compose up -d documenso-db
docker compose exec -T documenso-db pg_restore -U documenso -d documenso --clean --if-exists < /var/backups/documenso/db-YYYY-MM-DD-HHMM.dump
docker compose up -dUpdating
Read release notes before skipping versions. Back up first, update the image tag in compose.yml if pinned, then:
cd /opt/documenso
sudo ./backup.sh
docker compose pull
docker compose up -d
docker compose logs -f documensoTo roll back, restore the pre-update database and previous image tag. Renew the self-signed certificate before expiry; earlier signed PDFs retain their original signature.
Troubleshooting
| Symptom | Check |
|---|---|
| Caddy certificate fails | Check DNS with dig +short sign.example.com, port 80 and Caddy logs with journalctl -u caddy -f. |
| 502 response | Check docker compose ps and docker compose logs documenso; migrations may still be running. |
| Signatures remain pending | Confirm .p12 path, UID 1001 ownership and non-empty passphrase. |
| No verification/signing emails | Verify SMTP credentials; port 465 requires NEXT_PRIVATE_SMTP_SECURE: "true". See outgoing SMTP policy. |
| Email links use localhost | Set NEXT_PUBLIC_WEBAPP_URL to the public HTTPS domain and recreate the app container. |
| Large PDFs fail | Raise Caddy's request_body max_size. |
| PDF reader says issuer untrusted | Expected for a self-signed certificate; use a CA-issued document-signing certificate for trust. |
