# Deployment

> Run Spooldoc with Docker or Docker Compose: the image, ports, network to the IBM i, secrets, backups and upgrades.

Source: https://spooldoc.com/docs/deployment/

The container image holds the web app and a Java runtime for jt400, and
runs on amd64 and arm64. It needs a Postgres database, three environment
variables and a [Carbone](https://spooldoc.com/docs/carbone/) server to render documents.

Every release publishes the image to `ghcr.io/strom-capital/spooldoc`,
tagged with its version (`0.1.0`), its minor line (`0.1`) and `latest`.
Pin a version in production so an upgrade happens when you choose it.

## Docker Compose

`compose.yaml` at the repository root starts the published image and a
Postgres 18 database with a named volume:

```sh
printf 'BETTER_AUTH_SECRET=%s\nSPOOLDOC_SECRET_KEY=%s\n' "$(openssl rand -base64 32)" "$(openssl rand -base64 32)" > .env
docker compose up -d
```

Open http://localhost:3000. The `.env` file next to `compose.yaml` holds
the settings of `apps/web/.env.example`; `DATABASE_URL` is set by the
Compose file. `SPOOLDOC_VERSION` in `.env` pins the image version, `latest`
otherwise. `docker compose up --build -d` builds the image from the
checkout instead. Set `POSTGRES_PASSWORD` (URL-safe characters) for anything
but a local trial. The database port is published on this machine only, on
5433, so a local Postgres on 5432 is not in the way.

## Docker

With your own database:

```sh
docker run -p 3000:3000 \
  -e DATABASE_URL=postgres://... \
  -e BETTER_AUTH_SECRET="$(openssl rand -base64 32)" \
  -e SPOOLDOC_SECRET_KEY="$(openssl rand -base64 32)" \
  ghcr.io/strom-capital/spooldoc:<version>
```

To build the image from source, run `docker build -t spooldoc .` in the
repository and run `spooldoc` in place of the published name.

Passing secrets with `-e` shows them in `docker inspect` and the shell
history; use `--env-file` or your orchestrator's secrets. The image runs as
the `node` user, listens on port 3000 (`PORT` changes it) and applies the
database migrations when it starts.

## Ports

| Port | What it is |
|------|------------|
| 3000 | The web app. Publish it, or put a reverse proxy in front of it. `PORT` changes it in the container; from source, `next dev` takes the next free port when 3000 is busy. |
| 5433 on the host | Postgres, in the Compose setup, bound to 127.0.0.1 only. |

## Behind a reverse proxy

Set `APP_URL` to the address users type, such as
`https://spooldoc.example.com`. Origin checks on every write accept that
address and the request's own host, and the session cookie is marked
secure when the address is https. The proxy should pass the `Host` header
through unchanged.

## Network

The container needs a route to the IBM i on the host server ports jt400
uses: 449 (port mapper) and 8470 to 8476 (central, database on 8471 for
JDBC, network print, remote command and sign-on). When Spooldoc runs in
the cloud and the IBM i is on premises, connect them over a private link
such as a site-to-site VPN; do not open the host server ports to the
internet. Set `SPOOLDOC_ALLOWED_HOSTS` so users can only add the systems
you intend.

Spooldoc connects to:

- the IBM i, on the host server ports above
- its Postgres database
- the Carbone server you configure under Settings, Rendering: the
  `carbone` service of the Compose file, another server of yours, or
  Carbone Cloud over HTTPS
- the model provider you choose, over HTTPS, or a local model at the base
  URL you enter, only when the agent is used

Nothing else leaves the host.

## More than one instance

Every instance shares the database. Set `SPOOLDOC_WATCHER=off` on all but
one, so one process reads the data queues and polls; the watcher also
takes database locks, so a second watcher would not double-run a job, but
it would double the IBM i traffic.

## Backups

Everything Spooldoc keeps is in the database: servers, jobs and their
versions, captured spooled files, runs and their files, assets and
settings. One `pg_dump` is the whole backup. Keep `SPOOLDOC_SECRET_KEY`
with it: a restore under another key cannot read the saved IBM i
passwords and model keys, and they must be entered again. Captured
spooled files and delivered files are in the database in clear text, so
protect the database and its backups as you protect the spooled files
themselves.

## Upgrades

Pull or build the new image and start it; the server applies the
migrations. With Compose, set the new `SPOOLDOC_VERSION` (or leave it at
`latest`) and run `docker compose pull && docker compose up -d`. Take a
`pg_dump` first. What changed in each release is in the
[release notes](https://github.com/Strom-Capital/spooldoc/releases). Until
1.0, a minor release (0.1 to 0.2) can include breaking changes, so read
them before you upgrade, and pin `X.Y.Z` rather than following `latest`.

### From 0.2.0

The migrations turn each job's template and formats into the job's first
template, Template 1, so every job and version opens again. Two things
need you:

- Spooldoc now renders with Carbone. Run it next to Spooldoc and connect
  it in Settings; see [Carbone](https://spooldoc.com/docs/carbone/).
- Templates and file name patterns are now written with Carbone tags,
  not Nunjucks. Rewrite `{{ doc.invoiceNo }}` as `{d.pg.invoiceNo}`. The
  Template tab of each job lists the Nunjucks left in its template, and
  the definition check names a file name pattern that still has it. Until
  then, a run of the job fails with the same message. Save the job as a
  new version once both are rewritten.

## Sizing

A run reads one spooled file and extracts in the Node process, so memory
goes with the size of the reports. Carbone renders: an HTML to PDF render
takes tens of milliseconds, and the Carbone container uses about 500 MB of
memory. Its `full` image is about 560 MB to download. `UV_THREADPOOL_SIZE` is set to 16 in the image because
jt400 calls block a thread pool thread.
