mirror of
https://github.com/hanzoai/sign.git
synced 2026-10-21 01:21:51 +00:00
docs(self-hosting): describe the SQLite/Base storage this actually uses
The self-hosting docs still described PostgreSQL — connection strings, database pricing, "plan for database growth". esign runs on embedded SQLite (Hanzo Base), a single sign.db file on a persistent volume, so every one of those instructions sent a self-hoster to provision infrastructure the app never touches. Rewrites configuration, deployment, getting-started and maintenance to match: documents stored base64 in sign.db rather than in Postgres, capacity planning against the volume holding that file rather than database pricing, and backups of the file rather than pg_dump. Docs only — no behaviour change.
This commit is contained in:
@@ -13,7 +13,8 @@ These variables must be set for Hanzo eSign to function:
|
||||
| `NEXT_PRIVATE_ENCRYPTION_KEY` | Primary encryption key for symmetric encryption (minimum 32 characters) |
|
||||
| `NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY` | Secondary encryption key for symmetric encryption (minimum 32 characters) |
|
||||
| `NEXT_PUBLIC_WEBAPP_URL` | Public URL of your Hanzo eSign instance (e.g., `https://sign.example.com`) |
|
||||
| `NEXT_PRIVATE_DATABASE_URL` | PostgreSQL connection URL with connection pooling |
|
||||
| `DATABASE_URL` | SQLite database file URL (e.g., `file:/opt/hanzo-sign/base/data/_dev/sign.db`) |
|
||||
| `BASE_PATH` | Directory root for embedded Base data on the persistent disk |
|
||||
| `NEXT_PRIVATE_SMTP_FROM_ADDRESS` | Email address used as the sender for all outgoing emails |
|
||||
| `NEXT_PRIVATE_SMTP_FROM_NAME` | Display name for the sender of outgoing emails |
|
||||
|
||||
@@ -30,19 +31,26 @@ These variables must be set for Hanzo eSign to function:
|
||||
|
||||
## Database Configuration
|
||||
|
||||
Hanzo eSign requires PostgreSQL 14 or higher.
|
||||
Hanzo eSign stores all of its data in an **embedded SQLite database (Hanzo Base)**
|
||||
on the application's persistent disk. There is no external database server to
|
||||
provision, connect, or tune. esign uses a single shared SQLite file with tenancy
|
||||
enforced at the query layer (see HIP-0305), not a database-per-tenant split.
|
||||
|
||||
| Variable | Required | Description |
|
||||
| ---------------------------------- | ------------ | ------------------------------------------------------------------------------------------ |
|
||||
| `NEXT_PRIVATE_DATABASE_URL` | Yes | PostgreSQL connection URL. Supports connection pooling (e.g., PgBouncer) |
|
||||
| `NEXT_PRIVATE_DIRECT_DATABASE_URL` | When pooling | Direct PostgreSQL URL for migrations. Defaults to `NEXT_PRIVATE_DATABASE_URL` when not set |
|
||||
| Variable | Required | Description |
|
||||
| -------------- | -------- | -------------------------------------------------------------------------- |
|
||||
| `BASE_PATH` | Yes | Directory root for Base data on the persistent disk |
|
||||
| `DATABASE_URL` | Yes | SQLite file URL. Defaults to a file under `BASE_PATH` |
|
||||
|
||||
**Connection string format:**
|
||||
|
||||
```
|
||||
postgres://user:password@host:port/database
|
||||
file:/opt/hanzo-sign/base/data/_dev/sign.db
|
||||
```
|
||||
|
||||
The `file:` URL is the only form SQLite accepts. The application appends
|
||||
`connection_limit=1` automatically — SQLite serialises writes, so a single
|
||||
writer is correct and a larger pool only produces `SQLITE_BUSY`.
|
||||
|
||||
For detailed database setup, see [Database Configuration](/docs/self-hosting/configuration/database).
|
||||
|
||||
---
|
||||
@@ -148,7 +156,7 @@ For detailed email setup, see [Email Configuration](/docs/self-hosting/configura
|
||||
|
||||
## Storage Configuration
|
||||
|
||||
Hanzo eSign can store documents in the database or S3-compatible storage.
|
||||
Hanzo eSign can store documents in the embedded SQLite database (Hanzo Base) or in S3-compatible storage.
|
||||
|
||||
| Variable | Description | Default |
|
||||
| ---------------------------------------- | ------------------------------------------- | ---------- |
|
||||
@@ -248,11 +256,11 @@ AI features must also be enabled in organisation/team settings after configurati
|
||||
|
||||
## Background Jobs
|
||||
|
||||
Hanzo eSign uses a PostgreSQL-based job queue by default. Jobs (email delivery, document processing, webhook dispatch) are stored in the `BackgroundJob` table and processed via internal HTTP requests. No external queue service like Redis is required.
|
||||
Hanzo eSign uses a SQLite-backed job queue by default. Jobs (email delivery, document processing, webhook dispatch) are stored in the `BackgroundJob` table inside the embedded Base database and processed via internal HTTP requests. No external queue service like Redis is required.
|
||||
|
||||
| Variable | Description | Default |
|
||||
| ---------------------------- | ------------------------------------------------------------------------------ | ------- |
|
||||
| `NEXT_PRIVATE_JOBS_PROVIDER` | Jobs provider: `local` (PostgreSQL-based queue) or `inngest` (managed service) | `local` |
|
||||
| Variable | Description | Default |
|
||||
| ---------------------------- | -------------------------------------------------------------------------- | ------- |
|
||||
| `NEXT_PRIVATE_JOBS_PROVIDER` | Jobs provider: `local` (SQLite-backed queue) or `inngest` (managed service) | `local` |
|
||||
|
||||
### Inngest Configuration
|
||||
|
||||
@@ -311,9 +319,9 @@ NEXT_PRIVATE_ENCRYPTION_KEY="your-encryption-key-at-least-32-chars"
|
||||
NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY="your-secondary-key-at-least-32-chars"
|
||||
NEXT_PUBLIC_WEBAPP_URL="https://sign.example.com"
|
||||
|
||||
# Database
|
||||
NEXT_PRIVATE_DATABASE_URL="postgres://user:password@localhost:5432/hanzo-sign"
|
||||
NEXT_PRIVATE_DIRECT_DATABASE_URL="postgres://user:password@localhost:5432/hanzo-sign"
|
||||
# Database (embedded Base SQLite on a persistent disk)
|
||||
BASE_PATH="/opt/hanzo-sign/base/data"
|
||||
DATABASE_URL="file:/opt/hanzo-sign/base/data/_dev/sign.db"
|
||||
|
||||
# Email
|
||||
NEXT_PRIVATE_SMTP_TRANSPORT="smtp-auth"
|
||||
@@ -332,7 +340,7 @@ NEXT_PRIVATE_SIGNING_PASSPHRASE="your-certificate-password"
|
||||
|
||||
## See Also
|
||||
|
||||
- [Database Configuration](/docs/self-hosting/configuration/database) - Set up PostgreSQL
|
||||
- [Database Configuration](/docs/self-hosting/configuration/database) - Set up the embedded SQLite database
|
||||
- [Email Configuration](/docs/self-hosting/configuration/email) - Configure email delivery
|
||||
- [Storage Configuration](/docs/self-hosting/configuration/storage) - Set up S3 storage
|
||||
- [Signing Certificate](/docs/self-hosting/configuration/signing-certificate) - Configure document signing
|
||||
|
||||
@@ -11,7 +11,7 @@ description: Configure your self-hosted Hanzo eSign instance with environment va
|
||||
/>
|
||||
<Card
|
||||
title="Database"
|
||||
description="PostgreSQL setup and configuration."
|
||||
description="Embedded SQLite (Hanzo Base) setup."
|
||||
href="/docs/self-hosting/configuration/database"
|
||||
/>
|
||||
<Card
|
||||
@@ -35,7 +35,7 @@ description: Configure your self-hosted Hanzo eSign instance with environment va
|
||||
|
||||
At minimum, you need to configure:
|
||||
|
||||
1. **Database** - PostgreSQL connection
|
||||
1. **Database** - embedded SQLite file path (`DATABASE_URL` / `BASE_PATH`)
|
||||
2. **Email** - SMTP for sending notifications
|
||||
3. **Base URL** - Your instance's public URL
|
||||
|
||||
|
||||
@@ -29,21 +29,21 @@ NEXT_PUBLIC_UPLOAD_TRANSPORT=s3
|
||||
|
||||
## Database Storage
|
||||
|
||||
Database storage is the default option and requires no additional configuration. Documents are stored as base64-encoded data directly in PostgreSQL.
|
||||
Database storage is the default option and requires no additional configuration. Documents are stored as base64-encoded data directly in the embedded SQLite database (Hanzo Base), in the same `sign.db` file as the rest of your data.
|
||||
|
||||
<Tabs items={['Advantages', 'Limitations']}>
|
||||
<Tab value="Advantages">
|
||||
|
||||
- No external dependencies
|
||||
- Simple deployment
|
||||
- Automatic backups with database
|
||||
- Backed up together with the rest of the database (single file snapshot)
|
||||
|
||||
</Tab>
|
||||
<Tab value="Limitations">
|
||||
|
||||
- Increases database size significantly
|
||||
- Grows the `sign.db` file significantly
|
||||
- Slower for large files
|
||||
- Database backup/restore takes longer
|
||||
- File snapshots take longer as the database grows
|
||||
- Not recommended for files larger than 10MB
|
||||
|
||||
</Tab>
|
||||
@@ -431,7 +431,8 @@ New uploads use the configured `NEXT_PUBLIC_UPLOAD_TRANSPORT` backend.
|
||||
|
||||
### Database Storage Estimates
|
||||
|
||||
When using database storage, plan for significant database growth:
|
||||
When using database storage, plan for significant growth of the `sign.db` file
|
||||
on your persistent volume:
|
||||
|
||||
| Documents/Month | Avg Size | Monthly Growth | Annual Growth |
|
||||
| --------------- | -------- | -------------- | ------------- |
|
||||
@@ -455,12 +456,17 @@ S3 stores files without encoding overhead:
|
||||
|
||||
For high-volume deployments, S3 is more cost-effective:
|
||||
|
||||
| Aspect | Database Storage | S3 Storage |
|
||||
| ------------- | ---------------------------- | ----------------------- |
|
||||
| Storage cost | Database pricing (~$0.10/GB) | S3 pricing (~$0.023/GB) |
|
||||
| Transfer cost | Database I/O | S3 requests + egress |
|
||||
| Backup cost | Larger database backups | Separate S3 backups |
|
||||
| Performance | Degrades with size | Consistent |
|
||||
| Aspect | Database Storage | S3 Storage |
|
||||
| ------------- | -------------------------------------- | ----------------------- |
|
||||
| Storage cost | Persistent volume holding `sign.db` | S3 pricing (~$0.023/GB) |
|
||||
| Transfer cost | Local disk I/O | S3 requests + egress |
|
||||
| Backup cost | Larger `sign.db` snapshots | Separate S3 backups |
|
||||
| Performance | Degrades as the file grows | Consistent |
|
||||
|
||||
With database storage there is no managed-database bill: the documents live in
|
||||
the same `sign.db` file on the persistent volume you already pay for. The
|
||||
tradeoff is that the volume must grow with your document corpus, and snapshots
|
||||
take longer.
|
||||
|
||||
---
|
||||
|
||||
@@ -563,7 +569,7 @@ Default limit is 50MB per request.
|
||||
|
||||
## See Also
|
||||
|
||||
- [Database Configuration](/docs/self-hosting/configuration/database) - Configure PostgreSQL
|
||||
- [Database Configuration](/docs/self-hosting/configuration/database) - Configure the SQLite database
|
||||
- [Environment Variables](/docs/self-hosting/configuration/environment) - Complete configuration reference
|
||||
- [Backups](/docs/self-hosting/maintenance/backups) - Backup strategies for both storage backends
|
||||
- [Docker Compose](/docs/self-hosting/deployment/docker-compose) - Deploy with MinIO for local storage
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Docker Compose
|
||||
description: Deploy Hanzo eSign with Docker Compose, including PostgreSQL. This production-ready setup is suitable for most self-hosted deployments.
|
||||
description: Deploy Hanzo eSign with Docker Compose. A single application container with an embedded SQLite database on a named volume — production-ready for most self-hosted deployments.
|
||||
---
|
||||
|
||||
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
|
||||
@@ -68,48 +68,42 @@ Add the required configuration (see [Environment Configuration](#environment-con
|
||||
|
||||
## Docker Compose File Overview
|
||||
|
||||
The production `compose.yml` includes two services:
|
||||
esign uses an embedded SQLite database (Hanzo Base) — a single shared `sign.db`
|
||||
file with tenancy enforced at the query layer (see HIP-0305), not a separate
|
||||
database server. The production `compose.yml` is therefore a single service whose
|
||||
data lives on a named volume mounted at `BASE_PATH`:
|
||||
|
||||
```yaml
|
||||
name: hanzo-sign-production
|
||||
|
||||
services:
|
||||
database:
|
||||
image: postgres:15
|
||||
environment:
|
||||
- POSTGRES_USER=${POSTGRES_USER:?err}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?err}
|
||||
- POSTGRES_DB=${POSTGRES_DB:?err}
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER}']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
volumes:
|
||||
- database:/var/lib/postgresql/data
|
||||
|
||||
hanzo-sign:
|
||||
image: hanzo-sign/hanzo-sign:latest
|
||||
depends_on:
|
||||
database:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- BASE_PATH=/opt/hanzo-sign/base/data
|
||||
- DATABASE_URL=file:/opt/hanzo-sign/base/data/_dev/sign.db
|
||||
# See environment configuration below
|
||||
ports:
|
||||
- ${PORT:-3000}:${PORT:-3000}
|
||||
volumes:
|
||||
- base-data:/opt/hanzo-sign/base/data
|
||||
- /opt/hanzo-sign/cert.p12:/opt/hanzo-sign/cert.p12:ro
|
||||
|
||||
volumes:
|
||||
database:
|
||||
base-data:
|
||||
```
|
||||
|
||||
| Service | Purpose |
|
||||
| ----------- | ------------------------------------------------------------ |
|
||||
| `database` | PostgreSQL 15 database with persistent storage |
|
||||
| `hanzo-sign` | Main application container, waits for database to be healthy |
|
||||
| Service | Purpose |
|
||||
| ------------ | ----------------------------------------------------------------------- |
|
||||
| `hanzo-sign` | Main application container; its SQLite database lives on the `base-data` volume |
|
||||
|
||||
The Hanzo eSign container waits for the database health check to pass before starting.
|
||||
There is no separate database container to wait for — the database is a file on
|
||||
the `base-data` volume, created and migrated automatically on first start.
|
||||
|
||||
<Callout type="warn">
|
||||
The `base-data` volume must be persistent. If you point `BASE_PATH` at
|
||||
ephemeral container storage, all data is lost when the container is recreated.
|
||||
</Callout>
|
||||
|
||||
## Environment Configuration
|
||||
|
||||
@@ -118,10 +112,9 @@ Create a `.env` file with the following variables:
|
||||
### Required Variables
|
||||
|
||||
```bash
|
||||
# Database (used by both database and hanzo-sign services)
|
||||
POSTGRES_USER=hanzo-sign
|
||||
POSTGRES_PASSWORD=your-secure-database-password
|
||||
POSTGRES_DB=hanzo-sign
|
||||
# Database (embedded Base SQLite on the base-data volume)
|
||||
BASE_PATH=/opt/hanzo-sign/base/data
|
||||
DATABASE_URL=file:/opt/hanzo-sign/base/data/_dev/sign.db
|
||||
|
||||
# Application secrets (generate with: openssl rand -base64 32)
|
||||
NEXT_PRIVATE_AUTH_SECRET=your-nextauth-secret
|
||||
@@ -132,9 +125,6 @@ NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY=your-secondary-key-min-32-characters
|
||||
NEXT_PUBLIC_WEBAPP_URL=https://sign.example.com
|
||||
NEXT_PRIVATE_INTERNAL_WEBAPP_URL=http://localhost:3000
|
||||
|
||||
# Database connection (uses Docker service name)
|
||||
NEXT_PRIVATE_DATABASE_URL=postgresql://hanzo-sign:your-secure-database-password@database:5432/hanzo-sign
|
||||
|
||||
# Email configuration
|
||||
NEXT_PRIVATE_SMTP_TRANSPORT=smtp-auth
|
||||
NEXT_PRIVATE_SMTP_HOST=smtp.example.com
|
||||
@@ -173,11 +163,11 @@ echo "NEXT_PRIVATE_AUTH_SECRET=$(openssl rand -base64 32)"
|
||||
# Generate encryption keys
|
||||
echo "NEXT_PRIVATE_ENCRYPTION_KEY=$(openssl rand -base64 32)"
|
||||
echo "NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY=$(openssl rand -base64 32)"
|
||||
|
||||
# Generate database password
|
||||
echo "POSTGRES_PASSWORD=$(openssl rand -base64 24)"
|
||||
```
|
||||
|
||||
The embedded SQLite database has no separate user or password to generate — there
|
||||
is no database server to authenticate against.
|
||||
|
||||
## Signing Certificate
|
||||
|
||||
A signing certificate is required for document signing. Generate a `.p12` certificate on your host machine and mount it into the container. See [Local Certificate](/docs/self-hosting/configuration/signing-certificate/local) for how to generate one.
|
||||
@@ -224,12 +214,11 @@ docker compose ps
|
||||
Expected output:
|
||||
|
||||
```
|
||||
NAME STATUS PORTS
|
||||
hanzo-sign-production-database-1 running (healthy) 5432/tcp
|
||||
hanzo-sign-production-hanzo-sign-1 running 0.0.0.0:3000->3000/tcp
|
||||
NAME STATUS PORTS
|
||||
hanzo-sign-production-hanzo-sign-1 running 0.0.0.0:3000->3000/tcp
|
||||
```
|
||||
|
||||
Wait for the database to be healthy and for migrations to complete. Check the logs:
|
||||
Wait for the database file to be created and migrations to complete on first start. Check the logs:
|
||||
|
||||
```bash
|
||||
docker compose logs -f hanzo-sign
|
||||
@@ -264,11 +253,10 @@ View all service logs:
|
||||
docker compose logs -f
|
||||
```
|
||||
|
||||
View logs for a specific service:
|
||||
View logs for the application service:
|
||||
|
||||
```bash
|
||||
docker compose logs -f hanzo-sign
|
||||
docker compose logs -f database
|
||||
```
|
||||
|
||||
### Restart Services
|
||||
@@ -306,7 +294,8 @@ docker compose down -v
|
||||
```
|
||||
|
||||
<Callout type="error">
|
||||
Using `down -v` deletes the database volume. Back up your data first.
|
||||
Using `down -v` deletes the `base-data` volume — and with it the entire
|
||||
`sign.db` database. Back up your data first.
|
||||
</Callout>
|
||||
|
||||
### Update Hanzo eSign
|
||||
@@ -377,14 +366,20 @@ sign.example.com {
|
||||
|
||||
### Database Backups
|
||||
|
||||
Set up automated database backups:
|
||||
The database is a single SQLite file on the `base-data` volume, so backups are
|
||||
file snapshots — no `pg_dump`, no replication topology. Use `sqlite3 .backup`
|
||||
for a consistent copy even while the app is running:
|
||||
|
||||
```bash
|
||||
# Manual backup
|
||||
docker compose exec database pg_dump -U hanzo-sign hanzo-sign > backup.sql
|
||||
# Manual backup (consistent online copy, includes the WAL)
|
||||
docker compose exec hanzo-sign \
|
||||
sqlite3 /opt/hanzo-sign/base/data/_dev/sign.db ".backup '/opt/hanzo-sign/base/data/sign-backup.db'"
|
||||
docker compose cp hanzo-sign:/opt/hanzo-sign/base/data/sign-backup.db ./sign-backup.db
|
||||
|
||||
# Restore from backup
|
||||
docker compose exec -T database psql -U hanzo-sign hanzo-sign < backup.sql
|
||||
# Restore: stop the app, replace the file, restart
|
||||
docker compose stop hanzo-sign
|
||||
docker compose cp ./sign-backup.db hanzo-sign:/opt/hanzo-sign/base/data/_dev/sign.db
|
||||
docker compose start hanzo-sign
|
||||
```
|
||||
|
||||
See [Backups](/docs/self-hosting/maintenance/backups) for automated backup strategies.
|
||||
@@ -406,25 +401,19 @@ services:
|
||||
memory: 512M
|
||||
```
|
||||
|
||||
### External Database
|
||||
### No External Datastore Needed
|
||||
|
||||
For production, consider using a managed PostgreSQL service:
|
||||
esign's database is embedded — there is no managed PostgreSQL service to attach,
|
||||
no connection string to rotate, and no separate datastore to scale. Production
|
||||
durability comes from putting `BASE_PATH` (the `base-data` volume) on reliable,
|
||||
backed-up block storage and snapshotting it on a schedule. See
|
||||
[Backups](/docs/self-hosting/maintenance/backups) for volume-snapshot strategies.
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<Steps>
|
||||
<Step>
|
||||
Remove the `database` service from `compose.yml`
|
||||
</Step>
|
||||
<Step>
|
||||
Update environment variables:
|
||||
|
||||
```bash
|
||||
NEXT_PRIVATE_DATABASE_URL=postgresql://user:password@your-db-host:5432/hanzo-sign
|
||||
NEXT_PRIVATE_DIRECT_DATABASE_URL=postgresql://user:password@your-db-host:5432/hanzo-sign
|
||||
```
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
<Callout type="warn">
|
||||
SQLite is a single-writer database. Run exactly one `hanzo-sign` container that
|
||||
owns the `base-data` volume. Do not scale this service to multiple replicas
|
||||
pointed at the same file.
|
||||
</Callout>
|
||||
|
||||
### S3 Storage
|
||||
|
||||
@@ -445,12 +434,15 @@ See [Storage Configuration](/docs/self-hosting/configuration/storage).
|
||||
<Accordions type="multiple">
|
||||
<Accordion title="Container fails to start">
|
||||
Check logs: `docker compose logs hanzo-sign`. Common causes: missing environment variables
|
||||
(ensure all required variables are in `.env`), database not ready (the container waits for
|
||||
database health check), port conflict (change `PORT` in `.env` if 3000 is in use).
|
||||
(ensure all required variables, including `BASE_PATH` and `DATABASE_URL`, are in `.env`),
|
||||
the `base-data` volume not writable by the app user (UID 1001), port conflict (change `PORT`
|
||||
in `.env` if 3000 is in use).
|
||||
</Accordion>
|
||||
<Accordion title="Database connection errors">
|
||||
Verify the database is healthy: `docker compose ps database`. Test: `docker compose exec
|
||||
database psql -U hanzo-sign -d hanzo-sign -c "SELECT 1"`.
|
||||
<Accordion title="unable to open database file">
|
||||
The directory in `DATABASE_URL` does not exist or is not writable. Confirm the `base-data`
|
||||
volume is mounted at `BASE_PATH` and the path is absolute:
|
||||
`docker compose exec hanzo-sign ls -la /opt/hanzo-sign/base/data`. Then verify the database:
|
||||
`docker compose exec hanzo-sign sqlite3 /opt/hanzo-sign/base/data/_dev/sign.db 'SELECT 1'`.
|
||||
</Accordion>
|
||||
<Accordion title="Certificate errors">
|
||||
Check certificate status: `curl http://localhost:3000/api/certificate-status`. Common issues:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Docker
|
||||
description: Run Hanzo eSign as a standalone Docker container with an external database. Ideal for container platforms like AWS ECS, Google Cloud Run, or Azure Container Instances.
|
||||
description: Run Hanzo eSign as a standalone Docker container with an embedded SQLite database on a persistent volume. Ideal for container platforms like AWS ECS, Google Cloud Run, or Azure Container Instances.
|
||||
---
|
||||
|
||||
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
|
||||
@@ -9,12 +9,12 @@ import { Step, Steps } from 'fumadocs-ui/components/steps';
|
||||
|
||||
## Prerequisites
|
||||
|
||||
For a self-contained setup that includes PostgreSQL, see [Docker Compose Deployment](/docs/self-hosting/deployment/docker-compose).
|
||||
For a self-contained setup with a named volume managed for you, see [Docker Compose Deployment](/docs/self-hosting/deployment/docker-compose).
|
||||
|
||||
Before starting, ensure you have:
|
||||
|
||||
- [Docker](https://docs.docker.com/get-docker/) 20.10 or later installed
|
||||
- A PostgreSQL 14+ database accessible from your Docker host
|
||||
- A persistent volume to mount at `BASE_PATH` for the SQLite database
|
||||
- SMTP credentials for sending emails
|
||||
- At least 1GB of available RAM
|
||||
|
||||
@@ -55,27 +55,31 @@ These variables must be set for Hanzo eSign to start:
|
||||
| `NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY` | Secondary encryption key (minimum 32 characters) | - |
|
||||
| `NEXT_PUBLIC_WEBAPP_URL` | Public URL where Hanzo eSign is accessible (e.g., `https://sign.example.com`) | - |
|
||||
| `NEXT_PRIVATE_INTERNAL_WEBAPP_URL` | Internal URL the app uses to call itself (for background jobs) | Same as `NEXT_PUBLIC_WEBAPP_URL` |
|
||||
| `NEXT_PRIVATE_DATABASE_URL` | PostgreSQL connection string | - |
|
||||
| `BASE_PATH` | Directory root for Base data on the mounted persistent volume | - |
|
||||
| `DATABASE_URL` | SQLite file URL (e.g., `file:/opt/hanzo-sign/base/data/_dev/sign.db`) | - |
|
||||
| `NEXT_PRIVATE_SMTP_TRANSPORT` | Email transport: `smtp-auth`, `smtp-api`, `resend`, or `mailchannels` | `smtp-auth` |
|
||||
| `NEXT_PRIVATE_SMTP_FROM_NAME` | Sender name for outgoing emails | - |
|
||||
| `NEXT_PRIVATE_SMTP_FROM_ADDRESS` | Sender email address | - |
|
||||
|
||||
### Database Connection
|
||||
|
||||
Hanzo eSign requires two database connection strings:
|
||||
Hanzo eSign stores all data in an embedded SQLite database (Hanzo Base) on the
|
||||
mounted volume. Two variables control where it lives:
|
||||
|
||||
| Variable | Required | Default | Description |
|
||||
| ---------------------------------- | ------------ | --------------------------- | --------------------------------- |
|
||||
| `NEXT_PRIVATE_DATABASE_URL` | Yes | - | Pooled connection for app queries |
|
||||
| `NEXT_PRIVATE_DIRECT_DATABASE_URL` | When pooling | `NEXT_PRIVATE_DATABASE_URL` | Direct connection for migrations |
|
||||
| Variable | Required | Default | Description |
|
||||
| -------------- | -------- | ------- | --------------------------------------------------- |
|
||||
| `BASE_PATH` | Yes | - | Directory root for Base data on the persistent volume |
|
||||
| `DATABASE_URL` | Yes | - | SQLite `file:` URL, typically under `BASE_PATH` |
|
||||
|
||||
Connection string format:
|
||||
|
||||
```
|
||||
postgresql://user:password@host:5432/database
|
||||
file:/opt/hanzo-sign/base/data/_dev/sign.db
|
||||
```
|
||||
|
||||
If you're not using a connection pooler like PgBouncer, both variables can use the same connection string.
|
||||
The `file:` URL is the only form SQLite accepts. The application appends
|
||||
`connection_limit=1` automatically — SQLite serialises writes, so a single
|
||||
writer is correct.
|
||||
|
||||
### SMTP Configuration
|
||||
|
||||
@@ -112,13 +116,15 @@ For the complete list, see [Environment Variables](/docs/self-hosting/configurat
|
||||
docker run -d \
|
||||
--name hanzo-sign \
|
||||
-p 3000:3000 \
|
||||
-v hanzo-sign-base:/opt/hanzo-sign/base/data \
|
||||
-v /path/to/cert.p12:/opt/hanzo-sign/cert.p12:ro \
|
||||
-e NEXT_PRIVATE_AUTH_SECRET="$(openssl rand -base64 32)" \
|
||||
-e NEXT_PRIVATE_ENCRYPTION_KEY="your-encryption-key-min-32-chars" \
|
||||
-e NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY="your-secondary-key-min-32-chars" \
|
||||
-e NEXT_PUBLIC_WEBAPP_URL="https://sign.example.com" \
|
||||
-e NEXT_PRIVATE_INTERNAL_WEBAPP_URL="http://localhost:3000" \
|
||||
-e NEXT_PRIVATE_DATABASE_URL="postgresql://user:password@db-host:5432/hanzo-sign" \
|
||||
-e BASE_PATH="/opt/hanzo-sign/base/data" \
|
||||
-e DATABASE_URL="file:/opt/hanzo-sign/base/data/_dev/sign.db" \
|
||||
-e NEXT_PRIVATE_SIGNING_PASSPHRASE="your-certificate-password" \
|
||||
-e NEXT_PRIVATE_SMTP_TRANSPORT="smtp-auth" \
|
||||
-e NEXT_PRIVATE_SMTP_HOST="smtp.example.com" \
|
||||
@@ -130,6 +136,13 @@ docker run -d \
|
||||
hanzo-sign/hanzo-sign:latest
|
||||
```
|
||||
|
||||
<Callout type="warn">
|
||||
The `-v hanzo-sign-base:/opt/hanzo-sign/base/data` mount is required. The SQLite
|
||||
database lives under `BASE_PATH`; without a persistent volume there, all data is
|
||||
lost when the container is recreated. SQLite is single-writer, so run exactly one
|
||||
container that owns this volume.
|
||||
</Callout>
|
||||
|
||||
### Signing Certificate
|
||||
|
||||
A signing certificate is required for document signing. You have two options for providing one:
|
||||
@@ -159,8 +172,8 @@ NEXT_PRIVATE_ENCRYPTION_KEY=your-encryption-key-min-32-chars
|
||||
NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY=your-secondary-key-min-32-chars
|
||||
NEXT_PUBLIC_WEBAPP_URL=https://sign.example.com
|
||||
NEXT_PRIVATE_INTERNAL_WEBAPP_URL=http://localhost:3000
|
||||
NEXT_PRIVATE_DATABASE_URL=postgresql://user:password@db-host:5432/hanzo-sign
|
||||
NEXT_PRIVATE_DIRECT_DATABASE_URL=postgresql://user:password@db-host:5432/hanzo-sign
|
||||
BASE_PATH=/opt/hanzo-sign/base/data
|
||||
DATABASE_URL=file:/opt/hanzo-sign/base/data/_dev/sign.db
|
||||
NEXT_PRIVATE_SIGNING_PASSPHRASE=your-certificate-password
|
||||
NEXT_PRIVATE_SMTP_TRANSPORT=smtp-auth
|
||||
NEXT_PRIVATE_SMTP_HOST=smtp.example.com
|
||||
@@ -178,6 +191,7 @@ docker run -d \
|
||||
--name hanzo-sign \
|
||||
-p 3000:3000 \
|
||||
--env-file .env \
|
||||
-v hanzo-sign-base:/opt/hanzo-sign/base/data \
|
||||
-v /path/to/cert.p12:/opt/hanzo-sign/cert.p12:ro \
|
||||
hanzo-sign/hanzo-sign:latest
|
||||
```
|
||||
@@ -242,11 +256,15 @@ docker rm hanzo-sign
|
||||
|
||||
### Start a new container with the same configuration
|
||||
|
||||
Reattach the same `base-data` volume so the new container uses the existing
|
||||
database:
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name hanzo-sign \
|
||||
-p 3000:3000 \
|
||||
--env-file .env \
|
||||
-v hanzo-sign-base:/opt/hanzo-sign/base/data \
|
||||
-v /path/to/cert.p12:/opt/hanzo-sign/cert.p12:ro \
|
||||
hanzo-sign/hanzo-sign:latest
|
||||
```
|
||||
@@ -255,29 +273,38 @@ docker run -d \
|
||||
</Steps>
|
||||
|
||||
<Callout type="info">
|
||||
Database migrations run automatically when the container starts. Back up your database before
|
||||
upgrading.
|
||||
Database migrations run automatically when the container starts. Snapshot your
|
||||
`sign.db` file before upgrading.
|
||||
</Callout>
|
||||
|
||||
## Persistence
|
||||
|
||||
The Hanzo eSign container is stateless. All persistent data is stored in the PostgreSQL database.
|
||||
The Hanzo eSign container's code is stateless, but its data is not — all
|
||||
persistent data is stored in the SQLite database file on the volume mounted at
|
||||
`BASE_PATH`. That volume is the thing to keep and back up; if it is lost, so is
|
||||
your data.
|
||||
|
||||
By default, documents are also stored in the database. For high-volume deployments, configure S3-compatible storage instead. See [Storage Configuration](/docs/self-hosting/configuration/storage) for S3 setup.
|
||||
By default, documents are also stored in the database (and therefore on the same
|
||||
volume). For high-volume deployments, configure S3-compatible storage instead to
|
||||
keep the `sign.db` file lean. See [Storage Configuration](/docs/self-hosting/configuration/storage) for S3 setup.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
<Accordions type="multiple">
|
||||
<Accordion title="Container fails to start">
|
||||
Check the container logs: `docker logs hanzo-sign`. Common causes: missing required environment
|
||||
variables (ensure all required variables are set), database connection failed (verify the
|
||||
database URL and network connectivity), port already in use (change the host port or stop the
|
||||
conflicting service).
|
||||
variables (ensure `BASE_PATH` and `DATABASE_URL` are set), the volume at `BASE_PATH` not mounted
|
||||
or not writable, port already in use (change the host port or stop the conflicting service).
|
||||
</Accordion>
|
||||
<Accordion title="Database connection errors">
|
||||
Test connectivity: `docker run --rm postgres:15 psql
|
||||
"postgresql://user:password@host:5432/database" -c "SELECT 1"`. If using Docker networks, ensure
|
||||
the container can reach the database.
|
||||
<Accordion title="unable to open database file">
|
||||
The directory in `DATABASE_URL` does not exist or is not writable by the app user (UID 1001).
|
||||
Verify the volume is mounted and writable, then test the database file:
|
||||
`docker exec hanzo-sign sqlite3 /opt/hanzo-sign/base/data/_dev/sign.db 'SELECT 1'`.
|
||||
</Accordion>
|
||||
<Accordion title="Data disappeared after restart">
|
||||
`BASE_PATH` pointed at ephemeral container storage instead of a persistent volume. Recreate the
|
||||
container with `-v hanzo-sign-base:/opt/hanzo-sign/base/data` and restore from your last
|
||||
snapshot.
|
||||
</Accordion>
|
||||
<Accordion title="Certificate errors">
|
||||
Check certificate status: `curl http://localhost:3000/api/certificate-status`. Common issues:
|
||||
@@ -300,7 +327,7 @@ By default, documents are also stored in the database. For high-volume deploymen
|
||||
|
||||
## See Also
|
||||
|
||||
- [Docker Compose Deployment](/docs/self-hosting/deployment/docker-compose) - Multi-container setup with PostgreSQL included
|
||||
- [Docker Compose Deployment](/docs/self-hosting/deployment/docker-compose) - Single-service setup with a managed named volume
|
||||
- [Environment Variables](/docs/self-hosting/configuration/environment) - Full configuration reference
|
||||
- [Signing Certificate](/docs/self-hosting/configuration/signing-certificate) - Set up document signing
|
||||
- [Email Configuration](/docs/self-hosting/configuration/email) - Configure SMTP providers
|
||||
|
||||
@@ -12,7 +12,7 @@ Before starting, ensure you have:
|
||||
|
||||
- A Kubernetes cluster (1.24+) running and accessible
|
||||
- `kubectl` configured to communicate with your cluster
|
||||
- A PostgreSQL 14+ database (in-cluster or external)
|
||||
- A `ReadWriteOnce` PersistentVolumeClaim for the SQLite database (mounted at `BASE_PATH`)
|
||||
- SMTP credentials for sending emails
|
||||
- A domain name with DNS configured
|
||||
- An Ingress controller installed (nginx-ingress, traefik, etc.)
|
||||
@@ -26,19 +26,26 @@ kubectl get nodes
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
A typical Hanzo eSign Kubernetes deployment consists of:
|
||||
esign uses an embedded SQLite database (Hanzo Base) — a single shared `sign.db`
|
||||
file with tenancy enforced at the query layer (see HIP-0305), not an external
|
||||
database server. The database lives on a `ReadWriteOnce` PersistentVolumeClaim
|
||||
owned by a single app Pod. A typical deployment consists of:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["Ingress (TLS termination)"] --> B["Service (ClusterIP)"]
|
||||
B --> C1["Pod (app)"]
|
||||
B --> C2["Pod (app)"]
|
||||
B --> C3["Pod (app)"]
|
||||
C1 --> D["PostgreSQL (external or in-cluster)"]
|
||||
C2 --> D
|
||||
C3 --> D
|
||||
B --> C["Pod (app, replicas: 1)"]
|
||||
C --> D[("PersistentVolumeClaim (RWO)<br/>sign.db on persistent volume")]
|
||||
```
|
||||
|
||||
<Callout type="warn">
|
||||
SQLite is a **single-writer** database. The app Deployment must run a single
|
||||
replica that owns the `ReadWriteOnce` volume. Do not scale the web tier to
|
||||
multiple replicas or attach an HPA to it — concurrent writers to the same
|
||||
`sign.db` produce `SQLITE_BUSY` and risk corruption. Scale vertically (more CPU
|
||||
and memory) instead, and offload documents to S3 to keep the database small.
|
||||
</Callout>
|
||||
|
||||
## Namespace Setup
|
||||
|
||||
Create a dedicated namespace for Hanzo eSign:
|
||||
@@ -92,13 +99,14 @@ stringData:
|
||||
NEXT_PRIVATE_AUTH_SECRET: 'your-generated-secret-here'
|
||||
NEXT_PRIVATE_ENCRYPTION_KEY: 'your-encryption-key-min-32-chars'
|
||||
NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY: 'your-secondary-key-min-32-chars'
|
||||
NEXT_PRIVATE_DATABASE_URL: 'postgresql://user:password@postgres-host:5432/hanzo-sign'
|
||||
NEXT_PRIVATE_DIRECT_DATABASE_URL: 'postgresql://user:password@postgres-host:5432/hanzo-sign'
|
||||
NEXT_PRIVATE_SMTP_USERNAME: 'your-smtp-username'
|
||||
NEXT_PRIVATE_SMTP_PASSWORD: 'your-smtp-password'
|
||||
NEXT_PRIVATE_SIGNING_PASSPHRASE: 'your-certificate-passphrase'
|
||||
```
|
||||
|
||||
The SQLite `DATABASE_URL` and `BASE_PATH` are not secrets — they are file paths,
|
||||
not credentials — so they live in the ConfigMap below.
|
||||
|
||||
<Callout type="warn">
|
||||
Never commit Secret manifests with real values to version control. Use a secrets management tool
|
||||
like Sealed Secrets, External Secrets Operator, or your cloud provider's secret manager.
|
||||
@@ -133,6 +141,8 @@ data:
|
||||
NEXT_PRIVATE_INTERNAL_WEBAPP_URL: 'http://localhost:3000'
|
||||
NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH: '/opt/hanzo-sign/cert.p12'
|
||||
NEXT_PUBLIC_UPLOAD_TRANSPORT: 'database'
|
||||
BASE_PATH: '/opt/hanzo-sign/base/data'
|
||||
DATABASE_URL: 'file:/opt/hanzo-sign/base/data/_dev/sign.db'
|
||||
```
|
||||
|
||||
Apply the ConfigMap:
|
||||
@@ -141,6 +151,42 @@ Apply the ConfigMap:
|
||||
kubectl apply -f configmap.yaml
|
||||
```
|
||||
|
||||
## Persistent Volume
|
||||
|
||||
The SQLite database lives on a `ReadWriteOnce` PersistentVolumeClaim mounted at
|
||||
`BASE_PATH`. Create it before the Deployment so the claim exists when the Pod
|
||||
starts:
|
||||
|
||||
```yaml
|
||||
# pvc.yaml
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: hanzo-sign-data
|
||||
namespace: hanzo-sign
|
||||
labels:
|
||||
app.kubernetes.io/name: hanzo-sign
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 20Gi
|
||||
```
|
||||
|
||||
Apply the PVC:
|
||||
|
||||
```bash
|
||||
kubectl apply -f pvc.yaml
|
||||
```
|
||||
|
||||
<Callout type="warn">
|
||||
Size the volume for your document corpus if you use database storage — the
|
||||
PDFs live in the same `sign.db` file. Offload to S3 to keep the volume small.
|
||||
Because the claim is `ReadWriteOnce`, it can only be mounted by the single app
|
||||
Pod; this is what enforces the one-writer rule.
|
||||
</Callout>
|
||||
|
||||
## Deployment
|
||||
|
||||
Create the Hanzo eSign Deployment:
|
||||
@@ -156,16 +202,16 @@ metadata:
|
||||
app.kubernetes.io/name: hanzo-sign
|
||||
app.kubernetes.io/component: web
|
||||
spec:
|
||||
replicas: 2
|
||||
# SQLite is single-writer: exactly one Pod owns the RWO volume.
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: hanzo-sign
|
||||
app.kubernetes.io/component: web
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxSurge: 1
|
||||
maxUnavailable: 0
|
||||
# Recreate (not RollingUpdate): the old Pod must release the RWO volume
|
||||
# before the new Pod can mount it. A rolling surge would deadlock.
|
||||
type: Recreate
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
@@ -221,11 +267,16 @@ spec:
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 30
|
||||
volumeMounts:
|
||||
- name: base-data
|
||||
mountPath: /opt/hanzo-sign/base/data
|
||||
- name: signing-cert
|
||||
mountPath: /opt/hanzo-sign/cert.p12
|
||||
subPath: cert.p12
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: base-data
|
||||
persistentVolumeClaim:
|
||||
claimName: hanzo-sign-data
|
||||
- name: signing-cert
|
||||
secret:
|
||||
secretName: hanzo-sign-signing-cert
|
||||
@@ -366,113 +417,25 @@ kubectl apply -f ingress-nginx.yaml
|
||||
kubectl apply -f ingress-traefik.yaml
|
||||
```
|
||||
|
||||
## Database Options
|
||||
## Database
|
||||
|
||||
### External Database (Recommended)
|
||||
There is no database tier to deploy. esign's database is the embedded SQLite
|
||||
`sign.db` file on the [Persistent Volume](#persistent-volume) created above — no
|
||||
managed PostgreSQL service (RDS, Cloud SQL, Azure Database) to attach and no
|
||||
in-cluster database StatefulSet to operate.
|
||||
|
||||
For production, use a managed PostgreSQL service:
|
||||
Durability is a property of the volume: use a StorageClass backed by reliable,
|
||||
snapshot-capable block storage, and schedule volume snapshots. See
|
||||
[Backups](/docs/self-hosting/maintenance/backups).
|
||||
|
||||
- AWS RDS for PostgreSQL
|
||||
- Google Cloud SQL
|
||||
- Azure Database for PostgreSQL
|
||||
- DigitalOcean Managed Databases
|
||||
|
||||
Update `NEXT_PRIVATE_DATABASE_URL` in your Secret with the connection string from your provider.
|
||||
|
||||
### In-Cluster PostgreSQL
|
||||
|
||||
For testing or development, deploy PostgreSQL in the cluster:
|
||||
|
||||
```yaml
|
||||
# postgres.yaml
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: postgres-pvc
|
||||
namespace: hanzo-sign
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: postgres-secrets
|
||||
namespace: hanzo-sign
|
||||
type: Opaque
|
||||
stringData:
|
||||
POSTGRES_USER: hanzo-sign
|
||||
POSTGRES_PASSWORD: your-secure-password
|
||||
POSTGRES_DB: hanzo-sign
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: postgres
|
||||
namespace: hanzo-sign
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: postgres
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: postgres
|
||||
spec:
|
||||
containers:
|
||||
- name: postgres
|
||||
image: postgres:15-alpine
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: postgres-secrets
|
||||
volumeMounts:
|
||||
- name: postgres-data
|
||||
mountPath: /var/lib/postgresql/data
|
||||
resources:
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 1Gi
|
||||
volumes:
|
||||
- name: postgres-data
|
||||
persistentVolumeClaim:
|
||||
claimName: postgres-pvc
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: postgres
|
||||
namespace: hanzo-sign
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: 5432
|
||||
targetPort: 5432
|
||||
selector:
|
||||
app.kubernetes.io/name: postgres
|
||||
```
|
||||
|
||||
<Callout type="warn">
|
||||
In-cluster PostgreSQL is not recommended for production. It lacks high availability, automated
|
||||
backups, and point-in-time recovery that managed services provide.
|
||||
<Callout type="info">
|
||||
For strict ordering guarantees you may model the app as a single-replica
|
||||
StatefulSet with a `volumeClaimTemplate` instead of a Deployment + standalone
|
||||
PVC. Either way the invariant is the same: one Pod, one `ReadWriteOnce` volume,
|
||||
one writer.
|
||||
</Callout>
|
||||
|
||||
Update your Hanzo eSign secret to use the in-cluster database:
|
||||
|
||||
```yaml
|
||||
NEXT_PRIVATE_DATABASE_URL: 'postgresql://hanzo-sign:[email protected]:5432/hanzo-sign'
|
||||
NEXT_PRIVATE_DIRECT_DATABASE_URL: 'postgresql://hanzo-sign:[email protected]:5432/hanzo-sign'
|
||||
```
|
||||
|
||||
## Persistent Storage
|
||||
## Persistent Storage (Documents)
|
||||
|
||||
Hanzo eSign stores documents in the database by default. For high-volume deployments, configure S3-compatible storage.
|
||||
|
||||
@@ -498,90 +461,48 @@ See [Storage Configuration](/docs/self-hosting/configuration/storage) for detail
|
||||
|
||||
## Scaling
|
||||
|
||||
### Horizontal Pod Autoscaler (HPA)
|
||||
<Callout type="warn">
|
||||
Do **not** add a HorizontalPodAutoscaler or raise `replicas` above 1 for the web
|
||||
tier. esign's SQLite database is single-writer and the `ReadWriteOnce` volume
|
||||
can only be mounted by one Pod. Scale vertically instead.
|
||||
</Callout>
|
||||
|
||||
Scale Hanzo eSign based on CPU and memory utilization:
|
||||
### Vertical Scaling
|
||||
|
||||
Give the single Pod more CPU and memory by raising its resource requests and
|
||||
limits in the Deployment:
|
||||
|
||||
```yaml
|
||||
# hpa.yaml
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: hanzo-sign
|
||||
namespace: hanzo-sign
|
||||
labels:
|
||||
app.kubernetes.io/name: hanzo-sign
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: hanzo-sign
|
||||
minReplicas: 2
|
||||
maxReplicas: 10
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 70
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 80
|
||||
behavior:
|
||||
scaleDown:
|
||||
stabilizationWindowSeconds: 300
|
||||
policies:
|
||||
- type: Percent
|
||||
value: 10
|
||||
periodSeconds: 60
|
||||
scaleUp:
|
||||
stabilizationWindowSeconds: 0
|
||||
policies:
|
||||
- type: Percent
|
||||
value: 100
|
||||
periodSeconds: 15
|
||||
- type: Pods
|
||||
value: 4
|
||||
periodSeconds: 15
|
||||
selectPolicy: Max
|
||||
resources:
|
||||
requests:
|
||||
cpu: 500m
|
||||
memory: 1Gi
|
||||
limits:
|
||||
cpu: '2'
|
||||
memory: 2Gi
|
||||
```
|
||||
|
||||
Apply the HPA:
|
||||
Apply the change:
|
||||
|
||||
```bash
|
||||
kubectl apply -f hpa.yaml
|
||||
kubectl apply -f deployment.yaml
|
||||
```
|
||||
|
||||
### Pod Disruption Budget
|
||||
### Reducing Database Pressure
|
||||
|
||||
Ensure availability during cluster maintenance:
|
||||
The most effective way to keep a single replica fast is to keep the `sign.db`
|
||||
file small:
|
||||
|
||||
```yaml
|
||||
# pdb.yaml
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: hanzo-sign
|
||||
namespace: hanzo-sign
|
||||
labels:
|
||||
app.kubernetes.io/name: hanzo-sign
|
||||
spec:
|
||||
minAvailable: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: hanzo-sign
|
||||
app.kubernetes.io/component: web
|
||||
```
|
||||
- Set `NEXT_PUBLIC_UPLOAD_TRANSPORT=s3` so document PDFs live in object storage
|
||||
rather than the database file. See [Storage Configuration](/docs/self-hosting/configuration/storage).
|
||||
- Archive old audit logs and completed documents on a schedule.
|
||||
|
||||
Apply the PDB:
|
||||
|
||||
```bash
|
||||
kubectl apply -f pdb.yaml
|
||||
```
|
||||
<Callout type="info">
|
||||
A PodDisruptionBudget is intentionally omitted. With a single replica owning a
|
||||
`ReadWriteOnce` volume, a `minAvailable: 1` PDB would block voluntary node
|
||||
drains entirely. Rely on the `Recreate` strategy: the Pod is rescheduled onto a
|
||||
new node and re-mounts the volume there.
|
||||
</Callout>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -602,12 +523,18 @@ kubectl apply -f pdb.yaml
|
||||
- CrashLoopBackOff (check logs for application errors)
|
||||
- Pending (check resource requests and node capacity)
|
||||
</Accordion>
|
||||
<Accordion title="Database connection errors">
|
||||
Verify the database is accessible from the cluster:
|
||||
|
||||
<Accordion title="unable to open database file / database is locked">
|
||||
The SQLite database is the file at `DATABASE_URL` on the mounted PVC. Confirm
|
||||
the volume is mounted and the file exists, then probe it from inside the Pod:
|
||||
|
||||
```bash
|
||||
kubectl run postgres-test --rm -it --image=postgres:15-alpine -n hanzo-sign -- psql "postgresql://user:password@host:5432/database" -c "SELECT 1"
|
||||
kubectl exec -it deploy/hanzo-sign -n hanzo-sign -- ls -la /opt/hanzo-sign/base/data/_dev
|
||||
kubectl exec -it deploy/hanzo-sign -n hanzo-sign -- sqlite3 /opt/hanzo-sign/base/data/_dev/sign.db 'SELECT 1'
|
||||
```
|
||||
|
||||
Persistent `database is locked` errors almost always mean more than one Pod is
|
||||
writing the same file — verify `replicas: 1` and that no second Deployment or
|
||||
Job mounts the same PVC.
|
||||
</Accordion>
|
||||
<Accordion title="Ingress not working">
|
||||
Check Ingress status:
|
||||
@@ -701,6 +628,8 @@ data:
|
||||
NEXT_PRIVATE_INTERNAL_WEBAPP_URL: 'http://localhost:3000'
|
||||
NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH: '/opt/hanzo-sign/cert.p12'
|
||||
NEXT_PUBLIC_UPLOAD_TRANSPORT: 'database'
|
||||
BASE_PATH: '/opt/hanzo-sign/base/data'
|
||||
DATABASE_URL: 'file:/opt/hanzo-sign/base/data/_dev/sign.db'
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
@@ -712,19 +641,32 @@ stringData:
|
||||
NEXT_PRIVATE_AUTH_SECRET: 'REPLACE_WITH_GENERATED_SECRET'
|
||||
NEXT_PRIVATE_ENCRYPTION_KEY: 'REPLACE_WITH_32_CHAR_KEY'
|
||||
NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY: 'REPLACE_WITH_32_CHAR_KEY'
|
||||
NEXT_PRIVATE_DATABASE_URL: 'postgresql://user:password@host:5432/hanzo-sign'
|
||||
NEXT_PRIVATE_DIRECT_DATABASE_URL: 'postgresql://user:password@host:5432/hanzo-sign'
|
||||
NEXT_PRIVATE_SMTP_USERNAME: 'smtp-user'
|
||||
NEXT_PRIVATE_SMTP_PASSWORD: 'smtp-password'
|
||||
NEXT_PRIVATE_SIGNING_PASSPHRASE: 'cert-passphrase'
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: hanzo-sign-data
|
||||
namespace: hanzo-sign
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 20Gi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: hanzo-sign
|
||||
namespace: hanzo-sign
|
||||
spec:
|
||||
replicas: 2
|
||||
# SQLite is single-writer: one Pod owns the RWO volume.
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: hanzo-sign
|
||||
@@ -775,11 +717,16 @@ spec:
|
||||
periodSeconds: 5
|
||||
failureThreshold: 30
|
||||
volumeMounts:
|
||||
- name: base-data
|
||||
mountPath: /opt/hanzo-sign/base/data
|
||||
- name: signing-cert
|
||||
mountPath: /opt/hanzo-sign/cert.p12
|
||||
subPath: cert.p12
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: base-data
|
||||
persistentVolumeClaim:
|
||||
claimName: hanzo-sign-data
|
||||
- name: signing-cert
|
||||
secret:
|
||||
secretName: hanzo-sign-signing-cert
|
||||
@@ -823,37 +770,6 @@ spec:
|
||||
name: hanzo-sign
|
||||
port:
|
||||
name: http
|
||||
---
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: hanzo-sign
|
||||
namespace: hanzo-sign
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: hanzo-sign
|
||||
minReplicas: 2
|
||||
maxReplicas: 10
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 70
|
||||
---
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: hanzo-sign
|
||||
namespace: hanzo-sign
|
||||
spec:
|
||||
minAvailable: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: hanzo-sign
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -16,7 +16,7 @@ import { Step, Steps } from 'fumadocs-ui/components/steps';
|
||||
|
||||
- Node.js 20 or later
|
||||
- npm
|
||||
- PostgreSQL 14 or later
|
||||
- A persistent disk for the embedded SQLite database (no separate database server — set `BASE_PATH` to a directory on durable storage)
|
||||
- A Linux server (for systemd service setup)
|
||||
|
||||
## Install and Build
|
||||
@@ -50,8 +50,8 @@ NEXT_PRIVATE_ENCRYPTION_KEY="your-encryption-key-min-32-chars"
|
||||
NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY="your-secondary-key-min-32-chars"
|
||||
NEXT_PUBLIC_WEBAPP_URL="https://your-domain.com"
|
||||
NEXT_PRIVATE_INTERNAL_WEBAPP_URL="http://localhost:3000"
|
||||
NEXT_PRIVATE_DATABASE_URL="postgresql://user:password@localhost:5432/hanzo-sign"
|
||||
NEXT_PRIVATE_DIRECT_DATABASE_URL="postgresql://user:password@localhost:5432/hanzo-sign"
|
||||
BASE_PATH="/opt/hanzo-sign/base/data"
|
||||
DATABASE_URL="file:/opt/hanzo-sign/base/data/_dev/sign.db"
|
||||
NEXT_PRIVATE_SMTP_FROM_NAME="Hanzo eSign"
|
||||
NEXT_PRIVATE_SMTP_FROM_ADDRESS="[email protected]"
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Railway
|
||||
description: Deploy Hanzo eSign on Railway with automatic builds from Git, managed PostgreSQL, and built-in networking. Use the one-click template or manual setup.
|
||||
description: Deploy Hanzo eSign on Railway with automatic builds from Git, a persistent volume for the embedded SQLite database, and built-in networking. Use the one-click template or manual setup.
|
||||
---
|
||||
|
||||
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
|
||||
@@ -20,6 +20,13 @@ Before deploying, you need:
|
||||
- SMTP credentials for sending emails (required for document signing requests)
|
||||
- A custom domain (optional but recommended for production)
|
||||
|
||||
<Callout type="error">
|
||||
esign stores all data in an embedded SQLite file under `BASE_PATH`. Railway's
|
||||
container filesystem is **ephemeral** — without a persistent volume mounted at
|
||||
`BASE_PATH`, your database is wiped on every deploy and restart. Attach a
|
||||
Railway volume (shown below) before going to production.
|
||||
</Callout>
|
||||
|
||||
## One-Click Deploy
|
||||
|
||||
The fastest way to deploy Hanzo eSign on Railway is using the official template:
|
||||
@@ -29,7 +36,7 @@ The fastest way to deploy Hanzo eSign on Railway is using the official template:
|
||||
This template automatically provisions:
|
||||
|
||||
- Hanzo eSign application service
|
||||
- PostgreSQL database
|
||||
- A persistent volume mounted at `BASE_PATH` for the SQLite database
|
||||
- Required environment variables with secure defaults
|
||||
|
||||
{/* prettier-ignore */}
|
||||
@@ -112,12 +119,13 @@ No additional build configuration is needed.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Add PostgreSQL database
|
||||
### Add a persistent volume
|
||||
|
||||
- In your project, click "New"
|
||||
- Select "Database"
|
||||
- Choose "PostgreSQL"
|
||||
- Railway provisions the database and automatically sets `DATABASE_URL`
|
||||
- Select your Hanzo eSign service
|
||||
- Go to "Settings" > "Volumes"
|
||||
- Click "Add Volume" and set the mount path to `/opt/hanzo-sign/base/data`
|
||||
|
||||
This volume holds the SQLite database and survives deploys and restarts.
|
||||
|
||||
</Step>
|
||||
|
||||
@@ -133,8 +141,8 @@ NEXT_PRIVATE_AUTH_SECRET=<generate with: openssl rand -base64 32>
|
||||
NEXT_PRIVATE_ENCRYPTION_KEY=<minimum 32 character random string>
|
||||
NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY=<minimum 32 character random string>
|
||||
NEXT_PUBLIC_WEBAPP_URL=https://<your-railway-url>.up.railway.app
|
||||
NEXT_PRIVATE_DATABASE_URL=${{Postgres.DATABASE_URL}}
|
||||
NEXT_PRIVATE_DIRECT_DATABASE_URL=${{Postgres.DATABASE_URL}}
|
||||
BASE_PATH=/opt/hanzo-sign/base/data
|
||||
DATABASE_URL=file:${BASE_PATH}/sign.db
|
||||
NEXT_PRIVATE_SMTP_TRANSPORT=smtp-auth
|
||||
NEXT_PRIVATE_SMTP_HOST=<your-smtp-host>
|
||||
NEXT_PRIVATE_SMTP_PORT=587
|
||||
@@ -144,8 +152,10 @@ NEXT_PRIVATE_SMTP_FROM_NAME=Hanzo eSign
|
||||
[email protected]
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
Use `${{Postgres.DATABASE_URL}}` to reference the Railway-managed PostgreSQL connection string. Railway automatically resolves this reference.
|
||||
<Callout type="warn">
|
||||
Point `DATABASE_URL` at a `file:` path **inside** the volume mount
|
||||
(`BASE_PATH`). If it resolves anywhere else, the database lives on the ephemeral
|
||||
container filesystem and is lost on the next deploy.
|
||||
</Callout>
|
||||
|
||||
**Optional variables:**
|
||||
@@ -171,57 +181,76 @@ Click "Deploy" or push a commit to trigger a new deployment. Railway builds and
|
||||
|
||||
Railway manages environment variables through its dashboard. The full list of configuration options is documented in [Environment Variables](/docs/self-hosting/configuration/environment).
|
||||
|
||||
### Referencing Railway Services
|
||||
### Referencing the Volume Mount
|
||||
|
||||
Railway allows referencing other services in your project using the `${{ServiceName.VARIABLE}}` syntax:
|
||||
Railway exposes a project variable for the volume mount path. You can reference it
|
||||
when composing `DATABASE_URL` so the two stay in sync:
|
||||
|
||||
```bash
|
||||
NEXT_PRIVATE_DATABASE_URL=${{Postgres.DATABASE_URL}}
|
||||
BASE_PATH=${{RAILWAY_VOLUME_MOUNT_PATH}}
|
||||
DATABASE_URL=file:${{RAILWAY_VOLUME_MOUNT_PATH}}/sign.db
|
||||
```
|
||||
|
||||
This ensures connection strings stay in sync when Railway updates service configurations.
|
||||
If you set the mount path explicitly, use the literal path instead:
|
||||
|
||||
```bash
|
||||
BASE_PATH=/opt/hanzo-sign/base/data
|
||||
DATABASE_URL=file:${BASE_PATH}/sign.db
|
||||
```
|
||||
|
||||
## Database Setup
|
||||
|
||||
Railway's managed PostgreSQL handles database provisioning automatically. The database:
|
||||
esign's database is an embedded SQLite file on the Railway volume — there is no
|
||||
managed database addon to provision, connect to, or pay for separately. The
|
||||
database:
|
||||
|
||||
- Runs PostgreSQL 15 by default
|
||||
- Includes automatic backups on paid plans
|
||||
- Provides connection pooling via the internal network
|
||||
- Lives in a single `sign.db` file under `BASE_PATH` on the attached volume
|
||||
- Is created and migrated automatically on first start
|
||||
- Is backed up by snapshotting the Railway volume
|
||||
|
||||
### Database Connection
|
||||
|
||||
For Railway deployments, use the same connection string for both pooled and direct connections:
|
||||
Point `DATABASE_URL` at a `file:` path inside the volume mount:
|
||||
|
||||
```bash
|
||||
NEXT_PRIVATE_DATABASE_URL=${{Postgres.DATABASE_URL}}
|
||||
NEXT_PRIVATE_DIRECT_DATABASE_URL=${{Postgres.DATABASE_URL}}
|
||||
BASE_PATH=/opt/hanzo-sign/base/data
|
||||
DATABASE_URL=file:${BASE_PATH}/sign.db
|
||||
```
|
||||
|
||||
Hanzo eSign runs database migrations automatically on startup.
|
||||
|
||||
### Accessing the Database
|
||||
### Inspecting the Database
|
||||
|
||||
To connect directly to your database for debugging or maintenance:
|
||||
To inspect the database for debugging or maintenance, open a shell on the service
|
||||
and use the bundled `sqlite3` CLI:
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<Steps>
|
||||
<Step>
|
||||
### Open the PostgreSQL service
|
||||
### Open a service shell
|
||||
|
||||
In your Railway project dashboard, select the PostgreSQL service.
|
||||
In the Railway dashboard, select your Hanzo eSign service and open a shell (or use
|
||||
the Railway CLI: `railway run bash`).
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
### Open the Connect tab
|
||||
### Query with sqlite3
|
||||
|
||||
The "Connect" tab shows the connection string and credentials.
|
||||
The `sqlite3` CLI takes a plain path, not the `file:` URL:
|
||||
|
||||
```bash
|
||||
sqlite3 "${BASE_PATH}/sign.db" 'SELECT COUNT(*) FROM "User";'
|
||||
```
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
### Connect with your client
|
||||
### Snapshot before changes
|
||||
|
||||
Use the provided connection string with your preferred PostgreSQL client.
|
||||
Make a consistent copy before any manual change:
|
||||
|
||||
```bash
|
||||
sqlite3 "${BASE_PATH}/sign.db" ".backup '${BASE_PATH}/sign-backup.db'"
|
||||
```
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
@@ -307,44 +336,24 @@ Raise the memory and CPU limits; Railway will scale within these limits based on
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### Horizontal Scaling
|
||||
|
||||
Railway supports horizontal scaling through replicas:
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<Steps>
|
||||
<Step>
|
||||
### Open service Settings
|
||||
|
||||
In the Railway dashboard, select your Hanzo eSign service and open "Settings".
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
### Enable Horizontal Scaling
|
||||
|
||||
Turn on "Horizontal Scaling" so Railway can run multiple instances.
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
### Set replicas
|
||||
|
||||
Choose how many replicas to run; Railway will load-balance traffic across them.
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
### Horizontal Scaling Is Not Supported
|
||||
|
||||
<Callout type="warn">
|
||||
Hanzo eSign stores sessions in the database, so horizontal scaling works without additional
|
||||
configuration. However, ensure your database can handle the increased connection load.
|
||||
Do not enable Railway "Horizontal Scaling" (multiple replicas) for esign. The
|
||||
database is a single-writer SQLite file on one volume; multiple instances cannot
|
||||
safely write to it. Run a single replica and scale vertically.
|
||||
</Callout>
|
||||
|
||||
### Database Scaling
|
||||
### Volume Scaling
|
||||
|
||||
For high-traffic deployments:
|
||||
For high-traffic or document-heavy deployments:
|
||||
|
||||
- Upgrade to a larger PostgreSQL instance in Railway
|
||||
- Consider using connection pooling with PgBouncer
|
||||
- Configure read replicas for read-heavy workloads
|
||||
- Increase the persistent volume size in the service's "Volumes" settings as the
|
||||
`sign.db` file grows.
|
||||
- Set `NEXT_PUBLIC_UPLOAD_TRANSPORT=s3` so document PDFs live in object storage
|
||||
instead of the database file, keeping the volume small. See
|
||||
[Storage Configuration](/docs/self-hosting/configuration/storage).
|
||||
- Raise the service's memory and CPU limits (see Vertical Scaling above).
|
||||
|
||||
## Cost Estimate
|
||||
|
||||
@@ -429,16 +438,26 @@ See [Signing Certificate Configuration](/docs/self-hosting/configuration/signing
|
||||
Check the build logs in Railway for specific errors. Common causes: out of memory (increase the build memory limit in service settings), Docker build timeout (Railway has a 20-minute build timeout; ensure your build completes within this limit).
|
||||
</Accordion>
|
||||
<Accordion title="Application crashes on startup">
|
||||
View the deployment logs: click your service, go to Deployments, click the failed deployment. Common issues: missing environment variables (verify all required variables are set), database connection failed (check `NEXT_PRIVATE_DATABASE_URL` references the correct PostgreSQL service), invalid encryption keys (ensure keys are at least 32 characters).
|
||||
View the deployment logs: click your service, go to Deployments, click the failed deployment. Common issues: missing environment variables (verify all required variables are set), database file unreachable (check `DATABASE_URL` is a `file:` path under the volume mount and `BASE_PATH` matches the volume), invalid encryption keys (ensure keys are at least 32 characters).
|
||||
</Accordion>
|
||||
<Accordion title="Emails not sending">
|
||||
Verify SMTP configuration: check `NEXT_PRIVATE_SMTP_TRANSPORT` is set to `smtp-auth`, verify SMTP credentials are correct, ensure your SMTP provider allows connections from Railway's IP addresses.
|
||||
</Accordion>
|
||||
<Accordion title="Database connection errors">
|
||||
Verify PostgreSQL service is running, check the variable reference syntax `${{Postgres.DATABASE_URL}}`, try redeploying the application.
|
||||
<Accordion title="Data disappeared after a deploy">
|
||||
The SQLite file was on the ephemeral container filesystem, not the volume.
|
||||
Confirm a volume is mounted at `BASE_PATH` (Settings > Volumes) and that
|
||||
`DATABASE_URL` points at a `file:` path inside it, then restore from your last
|
||||
volume snapshot.
|
||||
</Accordion>
|
||||
<Accordion title="unable to open database file">
|
||||
The directory in `DATABASE_URL` does not exist or is not on the volume. Open a
|
||||
service shell and verify the mount: `ls -la "$BASE_PATH"`, then probe the file:
|
||||
`sqlite3 "${BASE_PATH}/sign.db" 'SELECT 1'`.
|
||||
</Accordion>
|
||||
<Accordion title="Slow performance">
|
||||
Increase memory allocation for the service, check database query performance in Railway's metrics, consider upgrading to a larger PostgreSQL instance.
|
||||
Increase memory and CPU allocation for the service, move documents to S3 with
|
||||
`NEXT_PUBLIC_UPLOAD_TRANSPORT=s3` to keep `sign.db` small, and grow the volume
|
||||
if it is near full.
|
||||
</Accordion>
|
||||
</Accordions>
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ import { Step, Steps } from 'fumadocs-ui/components/steps';
|
||||
This quick start setup is for local development and testing only:
|
||||
|
||||
- No HTTPS (not suitable for production)
|
||||
- No email delivery (emails are logged to console)
|
||||
- Uses a local PostgreSQL database
|
||||
- No email delivery (emails are captured by a local mail server)
|
||||
- Uses an embedded SQLite database (Hanzo Base) on a local volume
|
||||
- No signing certificate configured (documents will be signed with a generated certificate)
|
||||
- Single-node setup (no high availability)
|
||||
|
||||
@@ -113,23 +113,24 @@ Log in with your credentials.
|
||||
|
||||
### Granting Admin Access
|
||||
|
||||
All accounts created through signup are regular user accounts. To grant admin access, update the user's role directly in the database:
|
||||
All accounts created through signup are regular user accounts. To grant admin access, update the user's role directly in the SQLite database. Run `sqlite3` against the `sign.db` file under `BASE_PATH`:
|
||||
|
||||
```bash
|
||||
docker compose -f docker/development/compose.yml exec database \
|
||||
psql -U hanzo-sign -d hanzo-sign -c "UPDATE \"User\" SET roles = '{ADMIN}' WHERE email = '[email protected]';"
|
||||
sqlite3 "$BASE_PATH/_dev/sign.db" \
|
||||
"UPDATE \"User\" SET roles = '[\"ADMIN\"]' WHERE email = '[email protected]';"
|
||||
```
|
||||
|
||||
## What's Included
|
||||
|
||||
The quick start setup runs the following containers:
|
||||
The quick start setup runs the following containers. The database is **not** one
|
||||
of them — it is an embedded SQLite file on a local volume, created automatically
|
||||
under `BASE_PATH`:
|
||||
|
||||
| Container | Purpose | Port |
|
||||
| ----------- | -------------------------------- | ----- |
|
||||
| `hanzo-sign` | Main application | 3000 |
|
||||
| `database` | PostgreSQL database | 54320 |
|
||||
| `maildev` | Local email testing server | 2500 |
|
||||
| `minio` | S3-compatible storage (optional) | 9000 |
|
||||
| Container | Purpose | Port |
|
||||
| ------------ | ---------------------------------- | ---- |
|
||||
| `hanzo-sign` | Main application | 3000 |
|
||||
| `inbucket` | Local email capture (web UI) | 9000 |
|
||||
| `minio` | S3-compatible storage (optional) | 9001 |
|
||||
|
||||
## Useful Commands
|
||||
|
||||
@@ -166,8 +167,8 @@ docker compose -f docker/development/compose.yml up -d --build
|
||||
</Accordion>
|
||||
<Accordion title="Container fails to start">
|
||||
Check the logs: `docker compose -f docker/development/compose.yml logs hanzo-sign`. Common
|
||||
issues: insufficient memory (ensure Docker has at least 2GB RAM allocated), database connection
|
||||
(wait for the database container to be healthy before the app starts).
|
||||
issues: insufficient memory (ensure Docker has at least 2GB RAM allocated), or the SQLite
|
||||
database directory not writable (the `sign.db` file is created under `BASE_PATH` on first start).
|
||||
</Accordion>
|
||||
<Accordion title="Cannot access localhost:3000">
|
||||
Verify containers are running: `docker compose -f docker/development/compose.yml ps`. Check if
|
||||
@@ -180,7 +181,7 @@ docker compose -f docker/development/compose.yml up -d --build
|
||||
|
||||
## See Also
|
||||
|
||||
- [Docker Compose Deployment](/docs/self-hosting/deployment/docker-compose) - Production setup with PostgreSQL and email
|
||||
- [Docker Compose Deployment](/docs/self-hosting/deployment/docker-compose) - Production setup with a persistent volume and email
|
||||
- [Environment Variables](/docs/self-hosting/configuration/environment) - Configure all available options
|
||||
- [Signing Certificate](/docs/self-hosting/configuration/signing-certificate) - Set up document signing
|
||||
- [Email Configuration](/docs/self-hosting/configuration/email) - Configure SMTP for production
|
||||
|
||||
@@ -147,13 +147,19 @@ Hanzo eSign runs on:
|
||||
The following configurations are not supported and may not work correctly.
|
||||
</Callout>
|
||||
|
||||
| Configuration | Reason |
|
||||
| -------------- | ------------------------------------- |
|
||||
| Windows native | Use Docker or WSL2 instead |
|
||||
| MySQL/MariaDB | PostgreSQL-specific features required |
|
||||
| SQLite | Not suitable for production workloads |
|
||||
| MongoDB | Relational database required |
|
||||
| Node.js < 18 | Modern JavaScript features required |
|
||||
| Configuration | Reason |
|
||||
| --------------------- | -------------------------------------------------------------- |
|
||||
| Windows native | Use Docker or WSL2 instead |
|
||||
| External MySQL/MariaDB | The database is the embedded Base SQLite store; not pluggable |
|
||||
| External PostgreSQL | No longer used at runtime; see the migration note below |
|
||||
| MongoDB | Relational schema required |
|
||||
| Node.js < 18 | Modern JavaScript features required |
|
||||
|
||||
<Callout type="info">
|
||||
PostgreSQL is not a runtime dependency. If you are migrating an older PostgreSQL
|
||||
deployment, see [Database Configuration](/docs/self-hosting/configuration/database)
|
||||
for the one-time backfill into the embedded SQLite store.
|
||||
</Callout>
|
||||
|
||||
---
|
||||
|
||||
@@ -161,7 +167,7 @@ Hanzo eSign runs on:
|
||||
|
||||
Before proceeding to deployment, confirm you have:
|
||||
|
||||
- [ ] PostgreSQL 14+ database accessible
|
||||
- [ ] A persistent disk/volume for the SQLite database (`BASE_PATH`)
|
||||
- [ ] SMTP credentials or email provider API key
|
||||
- [ ] Signing certificate (`.p12`) for document signing — see [Tips](/docs/self-hosting/getting-started/tips)
|
||||
- [ ] Domain name for your Hanzo eSign instance
|
||||
|
||||
@@ -106,8 +106,8 @@ Check the [GitHub releases](https://github.com/hanzoai/esign/releases) page for
|
||||
|
||||
The Docker container runs `prisma migrate deploy` every time it starts. This means:
|
||||
|
||||
- **Upgrades are simple**: pull the new image, restart the container, and migrations apply automatically.
|
||||
- **Always back up your database before upgrading**: if a migration fails, you need a way to restore.
|
||||
- **Upgrades are simple**: pull the new image, restart the container, and migrations apply automatically against the SQLite file.
|
||||
- **Always snapshot the `sign.db` file before upgrading**: if a migration fails, the file copy is your rollback. See [Backups](/docs/self-hosting/maintenance/backups).
|
||||
- **First startup takes longer**: the initial migration creates all tables.
|
||||
|
||||
---
|
||||
@@ -126,9 +126,9 @@ This tells the job system to use the internal address instead of `NEXT_PUBLIC_WE
|
||||
|
||||
## Database Storage vs S3
|
||||
|
||||
Documents are stored in PostgreSQL by default (`NEXT_PUBLIC_UPLOAD_TRANSPORT=database`). This works well for small-to-medium deployments and keeps your infrastructure simple.
|
||||
Documents are stored in the embedded SQLite database (Hanzo Base) by default (`NEXT_PUBLIC_UPLOAD_TRANSPORT=database`), as base64 in the same `sign.db` file. This works well for small-to-medium deployments and keeps your infrastructure simple.
|
||||
|
||||
For high-volume deployments, switch to S3-compatible storage to keep your database lean:
|
||||
For high-volume deployments, switch to S3-compatible storage to keep the `sign.db` file lean:
|
||||
|
||||
```bash
|
||||
NEXT_PUBLIC_UPLOAD_TRANSPORT=s3
|
||||
@@ -146,7 +146,7 @@ See [Storage Configuration](/docs/self-hosting/configuration/storage) for full s
|
||||
|
||||
## Background Jobs Don't Need Redis
|
||||
|
||||
Hanzo eSign uses a PostgreSQL-based job queue by default. No Redis, no external message broker. The job system uses your existing database to store and process background tasks like email delivery and document processing.
|
||||
Hanzo eSign uses a SQLite-backed job queue by default. No Redis, no external message broker. The job system uses the embedded Base database to store and process background tasks like email delivery and document processing.
|
||||
|
||||
For high-throughput deployments, Hanzo eSign optionally supports [Inngest](https://www.inngest.com/) as an alternative job provider:
|
||||
|
||||
|
||||
@@ -11,20 +11,20 @@ import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||
|
||||
Hanzo eSign stores data in three locations:
|
||||
|
||||
| Component | Location | Contains |
|
||||
| --------------------- | ----------------------------- | -------------------------------------------------- |
|
||||
| PostgreSQL database | Database server | Users, documents, signatures, audit logs, settings |
|
||||
| Signing certificate | File system or secret manager | Private key for signing documents |
|
||||
| Document storage (S3) | S3 bucket or database | Uploaded PDFs and signed documents |
|
||||
| Component | Location | Contains |
|
||||
| --------------------- | ----------------------------- | --------------------------------------------------- |
|
||||
| SQLite database | `sign.db` file under `BASE_PATH` | Users, documents, signatures, audit logs, settings |
|
||||
| Signing certificate | File system or secret manager | Private key for signing documents |
|
||||
| Document storage (S3) | S3 bucket or the SQLite database | Uploaded PDFs and signed documents |
|
||||
|
||||
<Callout type="warn">
|
||||
The signing certificate is critical. If lost, you cannot sign new documents until a new
|
||||
certificate is configured. Back it up securely.
|
||||
</Callout>
|
||||
|
||||
### Data Stored in PostgreSQL
|
||||
### Data Stored in the SQLite Database
|
||||
|
||||
The database contains:
|
||||
The embedded Base database (`sign.db`) contains:
|
||||
|
||||
- User accounts and authentication data
|
||||
- Teams and organisation settings
|
||||
@@ -33,65 +33,67 @@ The database contains:
|
||||
- Audit logs and activity records
|
||||
- Templates and template settings
|
||||
- API tokens and webhook configurations
|
||||
- The `BackgroundJob` queue
|
||||
|
||||
If using database storage (`NEXT_PUBLIC_UPLOAD_TRANSPORT=database`), the database also contains the actual PDF files.
|
||||
If using database storage (`NEXT_PUBLIC_UPLOAD_TRANSPORT=database`), the same file also contains the actual PDF files.
|
||||
|
||||
---
|
||||
|
||||
## Database Backups
|
||||
|
||||
### Using pg_dump
|
||||
The database is a single SQLite file under `BASE_PATH`, so backups are file
|
||||
snapshots — no `pg_dump`, no replication topology. Use the `sqlite3 .backup`
|
||||
command, which produces a consistent copy even while the app is running.
|
||||
|
||||
Create a logical backup with `pg_dump`:
|
||||
|
||||
| Flag | Description |
|
||||
| ------ | ----------------------------------------------------- |
|
||||
| `-h` | Database host |
|
||||
| `-U` | Database user |
|
||||
| `-d` | Database name |
|
||||
| `-F c` | Custom format (compressed, supports parallel restore) |
|
||||
| `-f` | Output file |
|
||||
### Online Backup with sqlite3 .backup
|
||||
|
||||
<Tabs items={['Direct', 'Docker Compose']}>
|
||||
<Tab value="Direct">
|
||||
|
||||
```bash
|
||||
pg_dump -h localhost -U hanzo-sign -d hanzo-sign -F c -f hanzo-sign_backup.dump
|
||||
sqlite3 /opt/hanzo-sign/base/data/_dev/sign.db \
|
||||
".backup '/backup/hanzo-sign/sign-$(date +%Y%m%d_%H%M%S).db'"
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab value="Docker Compose">
|
||||
|
||||
```bash
|
||||
docker compose exec database pg_dump -U hanzo-sign -F c hanzo-sign > hanzo-sign_backup.dump
|
||||
docker compose exec hanzo-sign \
|
||||
sqlite3 /opt/hanzo-sign/base/data/_dev/sign.db \
|
||||
".backup '/opt/hanzo-sign/base/data/sign-backup.db'"
|
||||
docker compose cp \
|
||||
hanzo-sign:/opt/hanzo-sign/base/data/sign-backup.db \
|
||||
"./sign-$(date +%Y%m%d_%H%M%S).db"
|
||||
```
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Backup with Timestamp
|
||||
The `.backup` command copies the live database (including the WAL) into a new,
|
||||
self-contained file. It is safe to run while Hanzo eSign is serving traffic.
|
||||
|
||||
Include timestamps in backup filenames for easier management:
|
||||
### Plain File Copy (App Stopped)
|
||||
|
||||
If the app is stopped, an ordinary `cp` is sufficient and fastest:
|
||||
|
||||
```bash
|
||||
pg_dump -h localhost -U hanzo-sign -d hanzo-sign -F c \
|
||||
-f "hanzo-sign_$(date +%Y%m%d_%H%M%S).dump"
|
||||
cp /opt/hanzo-sign/base/data/_dev/sign.db \
|
||||
"/backup/hanzo-sign/sign-$(date +%Y%m%d_%H%M%S).db"
|
||||
```
|
||||
|
||||
### Plain SQL Backup
|
||||
|
||||
For human-readable backups or cross-version compatibility:
|
||||
|
||||
```bash
|
||||
pg_dump -h localhost -U hanzo-sign -d hanzo-sign -F p -f hanzo-sign_backup.sql
|
||||
```
|
||||
<Callout type="warn">
|
||||
A plain `cp` of a live database can capture a torn write. Use `sqlite3 .backup`
|
||||
for online copies; reserve `cp` for when the app is stopped.
|
||||
</Callout>
|
||||
|
||||
### Compressed Backup
|
||||
|
||||
Compress backups to save storage space:
|
||||
Compress snapshots to save storage space:
|
||||
|
||||
```bash
|
||||
pg_dump -h localhost -U hanzo-sign -d hanzo-sign | gzip > hanzo-sign_backup.sql.gz
|
||||
sqlite3 /opt/hanzo-sign/base/data/_dev/sign.db ".backup '/tmp/sign.db'"
|
||||
gzip -c /tmp/sign.db > "hanzo-sign_$(date +%Y%m%d_%H%M%S).db.gz" && rm /tmp/sign.db
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user