Rendlio is a single self-contained CLI — no Office, no LibreOffice, no network — which makes deployment mostly a question of *where you run the binary* and *what your pipeline does with the exit code*. This page covers the common patterns.

:::note[Pre-launch]
Rendlio is pre-launch. The install channels referenced here (`Rendlio.Tool` on NuGet, the container image, self-contained archives) publish at the public launch; this page documents the normative contract they ship with. Until then, [request a 30-day trial](/trial) to be notified, or follow the [changelog](/changelog).
:::

## Docker

The container image wraps the exact same `rendlio` binary as the .NET global tool — same flags, same exit codes, same reports. Mount your working directory and convert:

```bash
docker pull ghcr.io/rendlio/rendlio

docker run --rm -v "$PWD:/work" -w /work ghcr.io/rendlio/rendlio \
  convert book.xlsm -o book.pdf --format pdfa --report report.json
```

Or pipe through stdin/stdout without mounting anything — stdout carries only document bytes; all diagnostics go to stderr:

```bash
cat in.xlsx | docker run --rm -i ghcr.io/rendlio/rendlio convert - -o - > out.pdf
```

### Licences in a container

Three equivalent options, in the engine's normal [discovery order](/docs/licensing#where-to-put-the-file):

```bash
# 1. Mount the file into the working directory (cwd discovery)
docker run --rm -v "$PWD:/work" -w /work ghcr.io/rendlio/rendlio convert in.xlsx -o out.pdf

# 2. Pass it inline — RENDLIO_LICENSE accepts a path OR the licence text itself
docker run --rm -e RENDLIO_LICENSE="$(cat Rendlio.license)" -v "$PWD:/work" -w /work \
  ghcr.io/rendlio/rendlio convert in.xlsx -o out.pdf
```

Or bake `Rendlio.license` into a derived image at `~/.rendlio/Rendlio.license`. The licence is verified offline — the container needs no network access, ever.

### Container resource limits

The engine's own memory cap (`--max-memory`) is a cooperative soft cap enforced at checkpoints. Always set a hard container limit as the backstop for spikes between checkpoints:

```bash
docker run --rm --memory 2g --cpus 2 -v "$PWD:/work" -w /work \
  ghcr.io/rendlio/rendlio convert big.xlsx -o big.pdf --timeout 120
```

## The exit-code contract

CI gating keys on the exit code, which is the report's verdict as a number (normative, unchanged from first public release):

| Code | Meaning | Contract |
|---|---|---|
| 0 | clean | No warnings, nothing approximated/unsupported |
| 1 | rendered with warnings | Output written; report has warnings/approximated |
| 2 | unsupported content | Output written (one exception: a `--recalculate=strict-v1` refusal produces no document — scripts using strict-v1 must check `result.status`); report lists `unsupported` entries |
| 3 | invalid input / invalid usage | No output. Message says exactly what and the remedy |
| 4 | resource limit | Partial outputs **deleted**; message names the limit and the override env/flag |
| 5 | internal error | Bug. Message asks to file an issue with `--verbose` log; never silently swallowed |

Scripting contract: `exit <= 2` ⇒ the document exists and is complete per the report (strict-v1 refusal excepted, see the exit-2 row); CI users typically treat `<=1` as pass, `2` as review. On exit 4 partial outputs are deleted — you never gate on a truncated document — and the report is still written, naming the limit that fired.

For reproducible pipelines, add `--deterministic` (identical input + options ⇒ identical bytes) and pin `--reference-date` so date-dependent conditional formats do not shift between runs.

## GitHub Actions

```yaml
name: render-reports
on: [push]

jobs:
  convert:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Rendlio
        run: dotnet tool install -g Rendlio.Tool

      - name: Convert with report
        shell: bash
        run: |
          set +e
          rendlio convert reports/monthly.xlsm -o monthly.pdf \
            --format pdfa --report report.json \
            --deterministic --reference-date 2026-08-01
          code=$?
          set -e
          if [ "$code" -gt 1 ]; then
            echo "::error::conversion not clean (exit $code) — see report.json"
            exit "$code"
          fi

      - name: Gate on the report (optional, stricter)
        run: jq -e '.fonts.substitutions | length == 0' report.json

      - name: Upload the report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: compat-report
          path: report.json
```

The `set +e` / `$?` dance matters: Actions fails a step on any non-zero exit, and exit 1 (rendered with warnings) is usually a pass in practice. Capture the code, then apply *your* policy. More report-level gates (fail on any `unsupported`, allowlist specific warning codes, schema validation) are on the [compatibility report page](/docs/compat-report#gating-ci-on-the-report).

## GitLab CI

GitLab can natively surface exit 2 as a warning-level job instead of a failure via `allow_failure: exit_codes`:

```yaml
render-pdf:
  image:
    name: ghcr.io/rendlio/rendlio
    entrypoint: [""]
  script:
    - |
      rendlio convert reports/monthly.xlsm -o monthly.pdf \
        --format pdfa --report report.json --deterministic || code=$?
      code=${code:-0}
      if [ "$code" -le 1 ]; then exit 0; fi   # 0 clean, 1 warnings → pass
      exit "$code"                            # 2 → warning job (below); >=3 → fail
  allow_failure:
    exit_codes: [2]
  artifacts:
    when: always
    paths:
      - monthly.pdf
      - report.json
```

`artifacts.when: always` keeps `report.json` even on failing runs — the report is written on every outcome, including limit hits, and it names exactly what went wrong.

## Air-gapped hosts

Rendlio is designed to run where the network does not exist:

- **Zero network I/O, categorically.** The engine performs no activation, no telemetry, no font downloads, no phone-home — a contractual promise, enforced in the codebase by a static analyzer that bans network APIs outright.
- **Install without a feed.** Self-contained archives per platform need neither the .NET SDK nor a package feed — copy, unpack, run. Checksums publish alongside every archive on [Downloads](/downloads).
- **Licences verify offline.** The [licence file](/docs/licensing) is checked by signature against public keys embedded in the binary. Copy the file in with the binary, or carry it inline in `RENDLIO_LICENSE`. `rendlio license info` tells you on the spot whether the build is covered.
- **Perpetual fallback.** Builds released within your maintenance window keep working forever — an air-gapped host never needs to reach anything to stay licensed.
- **Fonts resolve locally.** Bundled fonts are used by default (deterministic everywhere); corporate fonts come from local directories via `--fonts-dir` or `RENDLIO_FONTS_DIR`. `--use-system-fonts` is off by default.

## Untrusted uploads — sandbox mode

If you embed Rendlio in a path where *anyone* can upload a workbook (the public playground runs exactly this configuration), set `RENDLIO_SANDBOX=1`. It hard-disables user font directories and system fonts (bundled fonts only), refuses `--recalculate=strict-v1`, and tightens the default timeout to 60 s. Any option the sandbox forbids fails loudly with `W8112` and exit 3 — never silently ignored. Pair it with an explicit `RENDLIO_MAX_MEMORY` and a lower `RENDLIO_MAX_FILE_SIZE` (below), plus a hard container limit.

## Resource limits

Every quantitative limit in the engine has a default and an environment override; the timeout and memory cap also have CLI flags. Precedence: **flag > environment variable > default.** The effective values are echoed into `report.options`, so a report always shows what actually applied.

Two rules worth knowing before you tune anything:

- **Overrides are never clamped or rounded.** An unparseable value (signs, separators, fractions, units) is a usage error, exit 3 — a limit silently weaker than you asked for would be worse than a refusal. Unset or empty means "default".
- **A limit hit never leaves a partial document.** On breach the run exits 4, temp outputs are deleted, and the report is still written naming the limit, its configured value, and the measured value.

| Limit | Env override | CLI flag | Default |
|---|---|---|---|
| Input file size | `RENDLIO_MAX_FILE_SIZE` | — | 500 MB |
| ZIP entry count | `RENDLIO_MAX_ZIP_ENTRIES` | — | 10,000 |
| Total uncompressed bytes | `RENDLIO_MAX_UNCOMPRESSED_TOTAL` | — | 2 GB |
| Per-entry uncompressed bytes | `RENDLIO_MAX_ENTRY_UNCOMPRESSED` | — | 512 MiB |
| Per-entry compression ratio | `RENDLIO_MAX_COMPRESSION_RATIO` | — | 1000:1 (past a 1 MiB floor) |
| XML element depth | `RENDLIO_MAX_XML_DEPTH` | — | 128 |
| Shared-string entries | `RENDLIO_MAX_SHARED_STRINGS` | — | 2,000,000 |
| Single string length | `RENDLIO_MAX_STRING_LENGTH` | — | 32,767 chars (the file format's own cell-text cap) |
| Style records | `RENDLIO_MAX_STYLES` | — | 65,490 (the format's own cap) |
| Merged ranges | `RENDLIO_MAX_MERGED_RANGES` | — | 100,000 |
| Images per sheet | `RENDLIO_MAX_IMAGES_PER_SHEET` | — | 1,000 |
| Pixels per image decode | `RENDLIO_MAX_IMAGE_PIXELS` | — | 100,000,000 (100 MP) — an over-cap image is skipped with a warning; the conversion continues |
| Sheet count | `RENDLIO_MAX_SHEETS` | — | 1,000 |
| Used-range cells (workbook total) | `RENDLIO_MAX_CELLS` | — | 50,000,000 |
| Wall-clock timeout | `RENDLIO_TIMEOUT` (seconds) | `--timeout` | 300 s (60 s under `RENDLIO_SANDBOX=1`) |
| Memory soft cap | `RENDLIO_MAX_MEMORY` (bytes) | `--max-memory` (MB) | see note below |

**Memory-cap default:** under review (FS-10 OPEN-3) — the CLI contract prints `1024` MB while the engine's canonical limits registry applies **unset (no cap)** unless one is given. Set it explicitly if you rely on it, and set a hard container limit either way (the soft cap is checkpoint-based; the container limit is the backstop).

Recalculation (`--recalculate=strict-v1`, planned) adds one more: `RENDLIO_MAX_RECALC_CELLS`, default 10,000,000 cells in the evaluation closure.

## Next steps

- [CLI reference](/docs/cli) — every command, flag, and environment variable
- [Compatibility report](/docs/compat-report) — schema, statuses, and `jq` gating recipes
- [Licensing guide](/docs/licensing) — tiers, the licence file, and the perpetual fallback