Documenso

Open-source document signing, an alternative to DocuSign

Documenso on Cubeship

Documenso is an open-source document signing platform: upload a PDF, place fields, send it out for signature, and get back a digitally signed document with an audit trail.

This template installs it on a Cubeship instance with the managed Postgres it keeps accounts, documents and audit logs in.

What it creates

  • documenso — the web app, from documenso/documenso:v2.18.0, answering on the domain you choose. It runs its database migrations each time it starts.
  • documenso-db — a managed Postgres 17 database, attached to the app. Uploaded PDFs are stored in it too, so backing it up backs up everything.

Before installing: a signing certificate

Documenso seals every completed document with a digital signature, and it signs with a .p12 certificate you give it. It does not make one itself: without a certificate the app starts, and documents cannot be completed.

A self-signed certificate is enough unless your industry requires a CA-issued one. PDF readers show its signature as valid but not trusted. Make one on your laptop:

openssl genrsa -out private.key 2048
openssl req -new -x509 -key private.key -out certificate.crt -days 3650 \
  -subj "/O=Your Company/CN=Your Company Signing"
read -s -p "Certificate password: " CERT_PASS; echo; export CERT_PASS
openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt \
  -password env:CERT_PASS
openssl base64 -A -in certificate.p12
rm private.key certificate.crt

Paste the output of the last openssl base64 line as the certificate, and the password you typed as its password. Keep certificate.p12 and the password: the certificate expires after the -days you gave it, and replacing it means pasting a new one into NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS and redeploying.

The password is not optional. Documenso cannot read the key from a .p12 exported without one.

What you are asked

InputWhat to give
Where Documenso answersA domain you control, pointed at your instance.
The signing certificate, as base64The one line openssl base64 -A printed above.
The certificate's passwordThe password the .p12 was exported with.
Your SMTP serverThe host your mail provider gives you, like smtp.mailgun.org.
Its port587 unless your provider says otherwise.
The SMTP usernameFrom your mail provider.
The SMTP passwordFrom your mail provider.
The address Documenso sends fromAn address your provider lets you send as.
The key session cookies are signed withNothing — the instance generates it.
The primary encryption keyNothing — the instance generates it and shows it once. Keep a copy.
The secondary encryption keyThe same. Keep a copy.

Mail is not optional. A signing request is an email with a link, and so are the completed document, reminders and the link that verifies a new account. Without working SMTP nobody can sign anything, and the first account cannot be verified.

Port 587 connects in plain text and upgrades with STARTTLS. A provider that only offers TLS from the start on 465: set the port to 465, then change NEXT_PRIVATE_SMTP_SECURE to true on the documenso app and redeploy.

After installing

  1. Open https://<your domain>/signup and create your account, then click the link in the verification email.
  2. Sign up is open to anyone who finds the domain. Recipients do not need an account to sign, so once your team has theirs, close it: set NEXT_PUBLIC_DISABLE_SIGNUP to true on the documenso app and redeploy.
  3. Check the certificate at https://<your domain>/api/certificate-status. "isAvailable": true means documents can be sealed. false means the pasted value or its password is wrong, or the certificate has expired.

Every account created through signup is a regular user. Documenso's admin panel, for managing users and instance settings, needs the ADMIN role, set in the database. Cubeship has no console into an app or a database, so do it over SSH on the machine the database runs on, with the database's user from its page in the dashboard:

docker exec cubeship-db-documenso-db psql -U <database user> -d documenso \
  -c "UPDATE \"User\" SET roles = '{USER,ADMIN}' WHERE email = 'you@example.com';"

The URLs assume the instance serves HTTPS. On an instance with TLS off, change NEXT_PUBLIC_WEBAPP_URL to start with http://.

Keys

NEXT_PRIVATE_ENCRYPTION_KEY and NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY encrypt what Documenso stores as secrets. Changing either after installing leaves those unreadable. Changing NEXTAUTH_SECRET only signs everybody out.

Resources

The app is limited to 1 CPU and 1 GiB of memory, the minimum Documenso asks for. Raise limits in template.yaml if sealing large documents fails.

What this creates

documenso

documenso/documenso:v2.18.0

documenso-db

Postgres 17

template.yaml
# yaml-language-server: $schema=https://cubeship.dev/schema/template/v1.json
version: 1
minCubeship: "0.6.0"
project: documenso

inputs:
  - key: domain
    type: domain
    label: Where Documenso answers
  - key: certificate
    type: secret
    label: The signing certificate, as base64
    help: A .p12 file, base64 on one line. Make a self-signed one with the commands in the README, then paste the output of `openssl base64 -A -in certificate.p12`.
  - key: certificatePassphrase
    type: secret
    label: The certificate's password
    help: The one set when the .p12 was exported. A certificate without a password cannot sign.
  - key: smtpHost
    type: text
    label: Your SMTP server
    help: Signing requests, completed documents and account verification all go out by email.
  - key: smtpPort
    type: number
    label: Its port
    help: 587 upgrades to TLS with STARTTLS. For 465, see the README.
    default: 587
    min: 1
    max: 65535
  - key: smtpUser
    type: text
    label: The SMTP username
  - key: smtpPassword
    type: secret
    label: The SMTP password
  - key: mailFrom
    type: text
    label: The address Documenso sends from
    help: One your SMTP provider lets you send as, like sign@example.com.
    pattern: ^[^@\s]+@[^@\s]+\.[^@\s]+$
  - key: authSecret
    type: secret
    label: The key session cookies are signed with
    generate: 32
  - key: encryptionKey
    type: secret
    label: The primary encryption key
    help: Keep a copy. Without it, secrets Documenso stored cannot be read again.
    generate: 32
  - key: encryptionSecondaryKey
    type: secret
    label: The secondary encryption key
    help: Keep a copy, for the same reason.
    generate: 32

databases:
  - key: db
    name: documenso-db
    engine: postgres
    version: "17"
    database: documenso

apps:
  - key: web
    name: documenso
    image: documenso/documenso
    tag: "v2.18.0"
    port: 3000
    # Answers 200 without signing in, and 500 only when the database is down.
    health: /api/health
    domains:
      - host: ${input.domain}
    attach:
      - database: db
    limits: { cpu: 1, memory: 1Gi }
    env:
      # The image leaves it unset, and Documenso marks cookies Secure only in production.
      NODE_ENV: production
      NEXT_PUBLIC_WEBAPP_URL: https://${input.domain}
      # Background jobs and PDF fonts are fetched from the app itself.
      NEXT_PRIVATE_INTERNAL_WEBAPP_URL: http://localhost:3000
      NEXT_PRIVATE_DATABASE_URL: postgresql://${db.db.user}:${db.db.password}@${db.db.host}:${db.db.port}/${db.db.name}
      NEXT_PRIVATE_DIRECT_DATABASE_URL: postgresql://${db.db.user}:${db.db.password}@${db.db.host}:${db.db.port}/${db.db.name}
      NEXTAUTH_SECRET: ${input.authSecret}
      NEXT_PRIVATE_ENCRYPTION_KEY: ${input.encryptionKey}
      NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY: ${input.encryptionSecondaryKey}
      NEXT_PRIVATE_SIGNING_TRANSPORT: local
      NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS: ${input.certificate}
      NEXT_PRIVATE_SIGNING_PASSPHRASE: ${input.certificatePassphrase}
      NEXT_PRIVATE_SMTP_TRANSPORT: smtp-auth
      NEXT_PRIVATE_SMTP_HOST: ${input.smtpHost}
      NEXT_PRIVATE_SMTP_PORT: ${input.smtpPort}
      # true is TLS from the first byte, for port 465.
      NEXT_PRIVATE_SMTP_SECURE: "false"
      NEXT_PRIVATE_SMTP_USERNAME: ${input.smtpUser}
      NEXT_PRIVATE_SMTP_PASSWORD: ${input.smtpPassword}
      NEXT_PRIVATE_SMTP_FROM_ADDRESS: ${input.mailFrom}
      NEXT_PRIVATE_SMTP_FROM_NAME: Documenso