Files
itpp-infrastructure/docs/infrastructure/docuseal/deployment-template.md
T

6.8 KiB

DocuSeal Deployment Template and Runbook

Spin up one self-hosted DocuSeal instance per legal entity. Reference live deployment: Core sign.itpropartner.com (container docuseal, image docuseal/docuseal:latest, bound 127.0.0.1:8091:3000, volume ./data:/data, env_file .env).

Hard isolation rule

One instance per legal entity. Do NOT multi-tenant a single DocuSeal across entities. Distinct legal entities require hard isolation: signatures, templates, and audit data must never mix. DocuSeal has a multitenant mode but it is not approved for cross-entity use here. Deploy a separate container, data volume, subdomain, and S3 bucket for each entity.

First target: Model Ortho. Future entities follow the same steps with a new slug, port, subdomain, and bucket.

Port remap

Core host port 3000 is occupied by browserless. Bind each instance to a unique loopback port, mapping to container port 3000:

  • Core sign.itpropartner.com: 127.0.0.1:8091
  • Model Ortho: next free port, e.g. 127.0.0.1:8092

List loopback listeners and pick an unused port:

ss -tln | grep 127.0.0.1

Prerequisites

  • Docker and Compose on the host (Core today, app4 in future)
  • DNS record for the sign subdomain
  • Wasabi S3 bucket named <entity>-legal (e.g. modelortho-legal) for attachments
  • SMTP relay credentials (netcup smarthost: only port 2525 works; 25/465/587 are blocked)
  • Vaultwarden (bw CLI) for credential storage

Steps

  1. Create the instance directory:
mkdir -p /root/docker/docuseal-<entity> && cd /root/docker/docuseal-<entity>
  1. Copy the templates and rename:
cp /root/projects/itpp-infrastructure/docs/infrastructure/docuseal/docker-compose.yml.template docker-compose.yml
cp /root/projects/itpp-infrastructure/docs/infrastructure/docuseal/.env.example .env
chmod 600 .env
  1. Edit docker-compose.yml. Replace __ENTITY_SLUG__ with the entity slug and __HOST_PORT__ with the unique loopback port.

  2. Fill .env. Set HOST, the SMTP_* vars, SECRET_KEY_BASE, and (once the bucket exists) the S3_/AWS_ vars. Generate the secret:

openssl rand -hex 64

Record it in Vaultwarden (step 10). NEVER rotate it once the instance is live.

  1. Start the container:
docker compose up -d
  1. Verify boot. A clean instance returns HTTP 302 to /setup:
curl -sI http://127.0.0.1:<port> | head -1

A 500 means a stale encrypted DB under a different SECRET_KEY_BASE. Move data/ aside and boot clean:

mv data data.old-$(date +%s) && docker compose up -d
  1. Complete setup at https://<host>/setup (owner email and password). If the submit button does not advance, run in the browser console:
document.querySelector('form').requestSubmit()
  1. Add the Caddy block for the sign subdomain:
<host> {
    reverse_proxy 127.0.0.1:<port>
}

Then validate and reload:

caddy validate && systemctl reload caddy
  1. Point DNS (A record to origin; grey-cloud for *.iamgmb.com). Verify:
curl -sI https://<host> | head -1

Expected: HTTP/2 200.

  1. Store admin credentials and the secret in Vaultwarden:
bw create item '{"type":1,"name":"DocuSeal <entity> admin","login":{"username":"owner@<entity>.com","password":"<admin password>","uris":[{"uri":"https://<host>"}]},"notes":"SECRET_KEY_BASE never rotate"}'
bw create item '{"type":2,"name":"DocuSeal <entity> SECRET_KEY_BASE","notes":"<secret key base>\nNEVER rotate on a live instance. ActiveRecord encryption root."}'
  1. Wire backup. Copy the Core script pattern (see Backup section) to /root/.hermes/scripts/docuseal-<entity>-backup.sh and add a Hermes cron job (daily).

SMTP env vars (exact names, verified against running container)

  • SMTP_ADDRESS: mail.itpropartner.com (or entity relay). NOT SMTP_HOST.
  • SMTP_PORT: 2525 (netcup smarthost; 25/465/587 blocked).
  • SMTP_USERNAME: noreply@. NOT SMTP_USER_NAME.
  • SMTP_PASSWORD: relay password.
  • SMTP_AUTHENTICATION: login (honored only when SMTP_PASSWORD present).
  • SMTP_ENABLE_STARTTLS: true.
  • SMTP_DOMAIN: .
  • SMTP_SSL_VERIFY: true (false = VERIFY_NONE).

SMTP_FROM is INERT. DocuSeal does not read it. The mailer default from is hardcoded as DocuSeal <info@docuseal.com> in app/mailers/application_mailer.rb. Branding the From address requires a fork, which conflicts with the AGPL rule below.

S3/Wasabi attachment vars

S3 storage activates when S3_ATTACHMENTS_BUCKET is present. Exact vars read from config/storage.yml:

  • S3_ATTACHMENTS_BUCKET: -legal (presence triggers S3 storage).
  • S3_ENDPOINT: s3.us-east-1.wasabisys.com (sets force_path_style true).
  • AWS_ACCESS_KEY_ID: Wasabi access key.
  • AWS_SECRET_ACCESS_KEY: Wasabi secret key.
  • AWS_REGION: us-east-1 (default).
  • ACTIVE_STORAGE_PUBLIC: false (optional).

Leave these unset to use local disk storage under ./data/attachments.

SECRET_KEY_BASE rule

SECRET_KEY_BASE is the encryption root for ActiveRecord encrypted columns in the SQLite DB. NEVER rotate it on a live instance. Rotating it breaks decryption (AEAD authentication tag verification failed, HTTP 500). Back it up with the instance and record it in Vaultwarden with a never-rotate note.

Backup script pattern

Runs locally on the host (no SSH). Archive the data dir, compose file, and .env so a restore is fully self-contained. Upload to the entity legal/ops bucket:

#!/bin/bash
set -euo pipefail
if [ -f /opt/awscli-venv/bin/activate ]; then source /opt/awscli-venv/bin/activate; fi
S3_BUCKET="s3://<entity>-legal/docuseal"
S3_ENDPOINT="--endpoint-url https://s3.us-east-1.wasabisys.com"
DOCUSEAL_DIR="/root/docker/docuseal-<entity>"
TSTAMP=$(date +%Y%m%d-%H%M%S)
WORKDIR=$(mktemp -d)
trap 'rm -rf "$WORKDIR"' EXIT
tar czf "$WORKDIR/docuseal-backup-$TSTAMP.tar.gz" -C "$DOCUSEAL_DIR" data docker-compose.yml .env
aws s3 cp $S3_ENDPOINT "$WORKDIR/docuseal-backup-$TSTAMP.tar.gz" "$S3_BUCKET/docuseal-backup-$TSTAMP.tar.gz"

Reference implementation: /root/.hermes/scripts/docuseal-backup.sh. Drive with a Hermes cron job.

AGPL-3.0 constraint

DocuSeal is AGPL-3.0. Run it UNMODIFIED and integrate via API only. Do not fork or patch the source. Consequences: no branded From address on email (see SMTP_FROM above), and all automation goes through DocuSeal's API rather than code changes.

docuseal:latest image note

Newer docuseal:latest bundles Redis and Sidekiq inside the single container. It may log a harmless memory overcommit warning (vm.overcommit_memory). This is cosmetic; the container runs fine. No separate Redis or Sidekiq services are needed.

Teardown

  1. Remove the Caddy block, then caddy validate and systemctl reload caddy.
  2. docker compose down (NOT stop: restart: always resurrects stopped containers on reboot; down removes the container while the bind-mounted data/ is preserved).
  3. Delete the DNS record.
  4. Never delete an old encrypted DB. Preserve as data.old-<epoch>.