Deployment
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.
Docker
Section titled “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:
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.jsonOr pipe through stdin/stdout without mounting anything — stdout carries only document bytes; all diagnostics go to stderr:
cat in.xlsx | docker run --rm -i ghcr.io/rendlio/rendlio convert - -o - > out.pdfLicences in a container
Section titled “Licences in a container”Three equivalent options, in the engine’s normal discovery order:
# 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 itselfdocker run --rm -e RENDLIO_LICENSE="$(cat Rendlio.license)" -v "$PWD:/work" -w /work \ ghcr.io/rendlio/rendlio convert in.xlsx -o out.pdfOr 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
Section titled “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:
docker run --rm --memory 2g --cpus 2 -v "$PWD:/work" -w /work \ ghcr.io/rendlio/rendlio convert big.xlsx -o big.pdf --timeout 120The exit-code contract
Section titled “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
Section titled “GitHub Actions”name: render-reportson: [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.jsonThe 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.
GitLab CI
Section titled “GitLab CI”GitLab can natively surface exit 2 as a warning-level job instead of a failure via allow_failure: exit_codes:
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.jsonartifacts.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
Section titled “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.
- Licences verify offline. The licence file 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 infotells 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-dirorRENDLIO_FONTS_DIR.--use-system-fontsis off by default.
Untrusted uploads — sandbox mode
Section titled “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
Section titled “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
Section titled “Next steps”- CLI reference — every command, flag, and environment variable
- Compatibility report — schema, statuses, and
jqgating recipes - Licensing guide — tiers, the licence file, and the perpetual fallback