docs: add legal storage policy, DocuSeal deploy template, and app4 migration plan
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
# 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>
|
||||
```
|
||||
|
||||
2. 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
|
||||
```
|
||||
|
||||
3. Edit `docker-compose.yml`. Replace `__ENTITY_SLUG__` with the entity slug and `__HOST_PORT__` with the unique loopback port.
|
||||
|
||||
4. 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.
|
||||
|
||||
5. Start the container:
|
||||
|
||||
```
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
6. 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
|
||||
```
|
||||
|
||||
7. 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()
|
||||
```
|
||||
|
||||
8. 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
|
||||
```
|
||||
|
||||
9. Point DNS (A record to origin; grey-cloud for `*.iamgmb.com`). Verify:
|
||||
|
||||
```
|
||||
curl -sI https://<host> | head -1
|
||||
```
|
||||
|
||||
Expected: HTTP/2 200.
|
||||
|
||||
10. 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."}'
|
||||
```
|
||||
|
||||
11. 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@<domain>. NOT SMTP_USER_NAME.
|
||||
- `SMTP_PASSWORD`: relay password.
|
||||
- `SMTP_AUTHENTICATION`: login (honored only when SMTP_PASSWORD present).
|
||||
- `SMTP_ENABLE_STARTTLS`: true.
|
||||
- `SMTP_DOMAIN`: <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`: <entity>-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>`.
|
||||
Reference in New Issue
Block a user