What it actually takes to run your own e-signature server — ESIGN/UETA evidence, PAdES-sealed audit trails, TLS/reverse-proxy/SMTP, and backups — with a copy-pasteable Docker setup. Vendor-neutral, with Lifted Sign as the worked example.
Vendor install docs tell you how to run their tool. This is the vendor-neutral version: the whole landscape of self-hostable options, a working docker setup you can copy, and the operational gotchas — TLS, SMTP, backups, and a court-admissible audit trail — that decide whether your deployment holds up.
Self-hosting changes where the server runs, not what the law requires. Under ESIGN and UETA an e-signature is enforceable when the process captures the right evidence, so your deployment still needs to record all of it:
The full statute-to-code mapping is here: ESIGN & UETA compliance for developers →
All four are open source and Docker-deployable. The practical difference for ops is how much they need underneath.
| Tool | Language | License | GitHub stars | Runs on just a file? |
|---|---|---|---|---|
| DocuSeal | Ruby | AGPL-3.0 | ~18,100 | No — database-backed |
| Documenso | TypeScript | AGPL-3.0 | ~14,100 | No — needs PostgreSQL |
| OpenSign | JavaScript | AGPL-3.0 (core) | ~6,700 | No — database-backed |
| Lifted Sign | Python | AGPL-3.0 | ~2 (new) | Yes — SQLite by default |
The biggest operational fork is the datastore. Documenso needs a PostgreSQL instance; DocuSeal and OpenSign are database-backed too. Lifted Sign is the outlier — it boots on a single SQLite file with zero external services, and you point it at Postgres with one environment variable only when you outgrow a file. That makes it the lightest thing to stand up for a proof of concept or a low-volume internal signer.
Here is the smallest real deployment — the published Lifted Sign image, one required secret, one port. This is the citable artifact; adapt the same shape for any of the tools above.
# 1. One command — pull the published image and run it. docker run --rm \ -e SIGN_SECRET=$(openssl rand -base64 48) \ -p 8080:8080 \ ghcr.io/liftedholdings/lifted-sign # Open http://localhost:8080 — upload a PDF, place fields, send, sign.
Or with Compose, so it restarts and persists the SQLite file:
# docker-compose.yml services: sign: image: ghcr.io/liftedholdings/lifted-sign:latest restart: unless-stopped ports: - "127.0.0.1:8080:8080" environment: SIGN_SECRET: "${SIGN_SECRET}" # required; server refuses to boot without it # SMTP so signer links actually get delivered: SMTP_HOST: "smtp.example.com" SMTP_USER: "postmaster@example.com" SMTP_PASS: "${SMTP_PASS}" # Outgrew SQLite? point at Postgres with one var: # DATABASE_URL: "postgresql://user:pass@db:5432/sign" volumes: - ./data:/app/state # the SQLite file + uploaded/sealed PDFs live here
SIGN_SECRET keys every session, signer-access cookie, and one-time code, so the server fails closed if it is missing, too short, or a placeholder. Generate a real one — python -c "import secrets; print(secrets.token_urlsafe(48))" — and store it in your secret manager, not in the image.The container is the easy 20 minutes. The real friction is the three things around it: terminating HTTPS, proxying to the app, and getting email delivered so signer links don’t land in spam.
Bind the app to localhost and put nginx (or Caddy/Traefik) in front with a real certificate. A minimal nginx server block:
server {
server_name sign.example.com;
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/sign.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/sign.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; # so the app builds https signer links
client_max_body_size 25m; # PDFs are bigger than the nginx default (1m)
}
}
Use certbot (Let’s Encrypt) for the cert. The two settings people forget: X-Forwarded-Proto (or the app mints http:// signer links behind your HTTPS proxy) and client_max_body_size (the default 1 MB rejects most real PDFs with a confusing 413).
Signing is worthless if the invite email never arrives. Use a real transactional sender (Postmark, SES, Mailgun, or your own authenticated relay) and set SPF, DKIM, and DMARC on the sending domain — signer links come from an unfamiliar host and get filtered aggressively without them. Test to a Gmail and an Outlook address before you go live.
A self-hosted signer owns legally binding records, so treat the data like it. Two things to get right:
pg_dump plus the object store. Snapshot both together so the audit records and the documents they describe stay consistent. Test a restore — an untested backup is a hope, not a backup.Set a retention policy that matches the underlying agreement (many commercial contracts imply 7+ years) and make sure your backup lifecycle doesn’t silently delete records you are legally expected to reproduce.
Self-hosting a US-law (ESIGN/UETA) e-signature server is the right call for most SMB and internal use. It is the wrong call when you need:
A reasonable middle path: self-host for control, but use a managed option (like Lifted Sign’s cloud) when you’d rather not run TLS, SMTP, and backups yourself.
Every license, star count, and language below was read from the GitHub REST API on July 23, 2026; pricing and carrier figures come from each vendor’s own pages on the same date. Open-source status means source published under an OSI/FSF-recognized license you can self-host and inspect. Figures change — check the primary source before you rely on a number, and tell us if anything here is out of date.
Lifted Sign self-hosts free under AGPL-3.0 on nothing but SQLite — or skip the TLS-and-SMTP chores and run the managed cloud.