Install
One install serves both products: the Viewer's render API and <zpl-viewer>, and the Editor's <zpl-editor> and merge
API. It is a single program with no database and no state: it renders and merges what it is sent and keeps nothing.
Your templates live in your own database. This page covers downloading it, running it from a terminal or in Docker,
configuring it, licensing it, protecting it, and upgrading it.
Download and verify#
Each release is one archive per platform, plus a checksum file:
| Platform | Archive |
|---|---|
| Linux x64 (glibc 2.28 or later: Debian 10+, Ubuntu 20.04+, RHEL/UBI 8+, Amazon Linux 2023) | labelviewer-<version>-linux-x64.tar.gz |
| Linux ARM64 (same requirements, e.g. AWS Graviton) | labelviewer-<version>-linux-arm64.tar.gz |
| macOS, Apple silicon | labelviewer-<version>-darwin-arm64.tar.gz |
| macOS, Intel | labelviewer-<version>-darwin-x64.tar.gz |
| Windows x64 | labelviewer-<version>-win-x64.zip |
| all | SHA256SUMS |
Every archive holds one directory with the labelviewer executable (labelviewer.exe on Windows), EULA.txt,
NOTICE.txt (third-party licenses), QUICKSTART.md, and on Linux a ready-to-use Dockerfile. The executable is
self-contained: the server, the developer portal, the browser SDK and the fonts are inside it. It needs no Node.js,
no other files and no internet access.
Check the download before you run it:
sha256sum -c SHA256SUMS --ignore-missing # Linux
shasum -a 256 -c SHA256SUMS --ignore-missing # macOS
(Get-FileHash labelviewer-1.0.1-win-x64.zip -Algorithm SHA256).Hash # Windows: compare with the line in SHA256SUMS
Run it from a terminal#
Linux and macOS#
tar -xzf labelviewer-1.0.1-linux-x64.tar.gz
cd labelviewer-1.0.1-linux-x64
./labelviewer serve # http://localhost:10090/ (with the included evaluation license)
./labelviewer serve --port 8080 --host 127.0.0.1
LICENSE_KEY="$(cat license.txt)" ./labelviewer serve # with your production license
LICENSE_FILE=/etc/labelviewer/license.txt ./labelviewer serve
Stop it with Ctrl+C or SIGTERM. To make it available to all users, copy the executable to /usr/local/bin/.
macOS: the executable is not notarized yet. macOS quarantines a file downloaded with a browser and refuses to open
it. After checking the checksum, remove the flag once: xattr -d com.apple.quarantine ./labelviewer. Files fetched
with curl are not quarantined.
Windows#
Expand-Archive labelviewer-1.0.1-win-x64.zip -DestinationPath .
cd labelviewer-1.0.1-win-x64
.\labelviewer.exe serve
$env:PORT = "8080"; .\labelviewer.exe serve
The executable is not code-signed yet, so SmartScreen may warn the first time: choose "More info", then "Run anyway". Allow the port in Windows Defender Firewall if other machines need to reach the server.
Commands#
| Command | |
|---|---|
labelviewer serve [--port 10090] [--host 0.0.0.0] | The HTTP API, this portal (/) and the SDK (/sdk/). --port and --host override PORT and HOST. |
labelviewer render <file.zpl> [-o out.png] [--dpmm 8] [--width 4] [--height 6] [--index n] | Render to PNG without a server (Viewer › Command line). Needs a license for the Viewer. |
labelviewer merge <template.zpl> <data.json> [-o out.zpl] [--missing empty] | Fill a template's {{placeholders}} (Editor › Templates). Needs a license for the Editor. |
labelviewer health [--port 10090] [--host 127.0.0.1] | Exit status 0 when the server answers /healthz: a health check without curl. |
labelviewer version | Version, SDK version and platform. |
labelviewer --help | Usage. |
As a service (systemd)#
# /etc/systemd/system/labelviewer.service
[Unit]
Description=labelviewer
After=network-online.target
[Service]
ExecStart=/usr/local/bin/labelviewer serve
Environment=PORT=10090
EnvironmentFile=-/etc/labelviewer.env
DynamicUser=yes
ProtectSystem=strict
NoNewPrivileges=yes
Restart=on-failure
[Install]
WantedBy=multi-user.target
Then systemctl enable --now labelviewer. Put LICENSE_KEY=... in /etc/labelviewer.env.
Run it with Docker#
Each Linux archive contains a Dockerfile that builds an image from the executable next to it:
tar -xzf labelviewer-1.0.1-linux-x64.tar.gz
cd labelviewer-1.0.1-linux-x64
docker build -t labelviewer:1.0.1 .
# with the included evaluation license (watermarked label images)
docker run -d --name labelviewer -p 10090:10090 --read-only labelviewer:1.0.1
# with your production license: the key in a variable ...
docker run -d --name labelviewer --restart unless-stopped \
-p 10090:10090 \
-e LICENSE_KEY="$(cat license.txt)" \
--read-only \
labelviewer:1.0.1
# ... or as a mounted file (for example a Docker or Kubernetes secret)
docker run -d --name labelviewer --restart unless-stopped \
-p 10090:10090 \
-v /etc/labelviewer/license.txt:/run/secrets/labelviewer-license:ro \
-e LICENSE_FILE=/run/secrets/labelviewer-license \
--read-only \
labelviewer:1.0.1
Then open http://<host>:10090/ for this developer portal; its Status page shows the license in use (see
License). If you received a ready-made image instead (from a registry, or a docker save tarball:
docker load < labelviewer-1.0.1-image.tar), run it the same way.
The archive's image:
- is
debian:bookworm-slimplus the executable in/usr/local/bin/labelviewer(no Node.js, no JavaScript files); - runs as an unprivileged user (uid 10001), with the executable owned by root;
- needs no volume: nothing is written to disk, so
--read-onlyworks; - listens on
0.0.0.0:10090, and itsHEALTHCHECKrunslabelviewer health; - starts
labelviewer serve; other commands run as one-off containers (docker run --rm labelviewer:1.0.1 version).
Build your own image#
The executable is an ordinary Linux program, so you can put it on the base image your organisation approves:
- Any glibc-based image works: Debian, Ubuntu, Red Hat UBI (also
ubi-minimal), Amazon Linux,gcr.io/distroless/cc-debian12. The executable needs glibc 2.28 or later,libstdc++.so.6andlibgcc_s.so.1(the newest symbol versions it asks for areGLIBC_2.28,GLIBCXX_3.4.21andCXXABI_1.3.11). The images above have them;scratchandgcr.io/distroless/staticdo not. - Alpine (musl) is not supported. The executable is linked against glibc (its loader is
/lib64/ld-linux-x86-64.so.2on x64,/lib/ld-linux-aarch64.so.1on ARM64). Alpine would need at least thegcompat,libstdc++andlibgccpackages, and even then glibc builds of Node.js-based programs are known to fail on it. We do not test or support that combination: use a glibc base. - Match the architecture: the
linux-x64archive forlinux/amd64images,linux-arm64forlinux/arm64. The archive's Dockerfile pins its platform in theFROMline, so building it on another architecture (for example an Apple silicon Mac) still gives the right image; it then only runs there under emulation. - Run it as any non-root user, without extra capabilities and with a read-only root filesystem: it needs no writable path.
- Configure it with environment variables only (Configuration). The only file it reads is
LICENSE_FILE, if you set it (mount the license as a secret). - Health check:
labelviewer healthin exec form works without a shell, curl or wget.
A minimal Dockerfile on your own base image (run docker build in the extracted archive directory):
FROM registry.example.internal/base/ubi9-minimal:latest
COPY labelviewer /usr/local/bin/labelviewer
USER 10001:10001
EXPOSE 10090
HEALTHCHECK --interval=30s --timeout=5s CMD ["/usr/local/bin/labelviewer", "health"]
ENTRYPOINT ["/usr/local/bin/labelviewer"]
CMD ["serve"]
One multi-arch image from both Linux archives, with docker buildx (one stage per architecture, so each image
contains only its own executable):
mkdir ctx
tar -xzf labelviewer-1.0.1-linux-x64.tar.gz -C ctx
tar -xzf labelviewer-1.0.1-linux-arm64.tar.gz -C ctx
cat > ctx/Dockerfile <<'DOCKERFILE'
FROM scratch AS bin-amd64
COPY labelviewer-1.0.1-linux-x64/labelviewer /labelviewer
FROM scratch AS bin-arm64
COPY labelviewer-1.0.1-linux-arm64/labelviewer /labelviewer
FROM bin-${TARGETARCH} AS bin
FROM debian:bookworm-slim
COPY --from=bin /labelviewer /usr/local/bin/labelviewer
USER 10001:10001
EXPOSE 10090
HEALTHCHECK --interval=30s --timeout=5s CMD ["/usr/local/bin/labelviewer", "health"]
ENTRYPOINT ["/usr/local/bin/labelviewer"]
CMD ["serve"]
DOCKERFILE
docker buildx build --platform linux/amd64,linux/arm64 -t registry.example.internal/labelviewer:1.0.1 --push ctx
Replace debian:bookworm-slim with your approved base (the same image must exist for both architectures).
Docker Compose#
services:
labelviewer:
image: labelviewer:1.0.1
restart: unless-stopped
ports:
- "10090:10090"
environment:
LICENSE_FILE: /run/secrets/labelviewer-license
RENDER_WORKERS: "4"
secrets:
- labelviewer-license
read_only: true
secrets:
labelviewer-license:
file: ./license.txt
Configuration#
Everything is set with environment variables; all are optional.
| Variable | Default | |
|---|---|---|
PORT | 10090 | Listening port. |
HOST | 0.0.0.0 | Listening address. 127.0.0.1 keeps it to the local machine (e.g. behind a reverse proxy on the same host). |
LICENSE_KEY | unset | Your license key (one line, LV2.…). Unset: the included evaluation license is used (License). |
LICENSE_FILE | unset | Path to a file holding the license key (instead of LICENSE_KEY), e.g. a mounted secret. |
RENDER_WORKERS | min(4, CPU cores) | Render worker threads. Each render runs in a worker so it can be stopped. 0 renders on the request thread, without a time budget (debugging only). |
RENDER_TIMEOUT_MS | 10000 | Time budget per render. When it runs out the worker is replaced and the request answers 422. |
RENDER_MAX_QUEUE | 64 | Renders that may wait for a free worker before requests answer 503. |
RENDER_MAX_HEAP_MB | 512 | Memory cap per render worker. |
REQUEST_TIMEOUT_MS | 30000 | Deadline for a whole request (headers + body). Headers must arrive within 10 s. |
ACCESS_LOG | on | false turns off the JSON access log on stdout. |
The executable ignores NODE_OPTIONS and other Node.js runtime options.
Sizing. Rendering is CPU work: one worker renders one request at a time, and large graphics and 600 dpi labels take
longer than plain 203 dpi ones. Give the machine or container one CPU per worker plus some headroom, and about
RENDER_WORKERS x RENDER_MAX_HEAP_MB of memory at most (most renders use far less). Merges are cheap and run on the
main thread. For more throughput, run several containers behind a load balancer: they share nothing.
License#
The download includes an evaluation license, so labelviewer works right away (label images are watermarked). To use your
production license, set LICENSE_KEY or LICENSE_FILE and restart. Everything about products, trials, expiry and what
happens when a license is not valid is on the License page.
Access control#
The API has no authentication, by design: it is meant for your internal network, next to the systems that print labels. Anything that can reach port 10090 can render and merge. Decide who can reach it:
- Internal network only. Run it on a private subnet or a cluster-internal service, and do not publish the port to the internet. Your backends call it directly; browsers of your users load the SDK from it.
- Behind a reverse proxy or gateway for TLS, IP allow lists, single sign-on or rate limits. Keep the API and the SDK
paths reachable for whoever needs them:
/v1/,/api/(backends, and browsers if you use Test with API),/sdk/(your users' browsers),/(the portal, for your developers) and/healthz(health checks).
Example with nginx: TLS, the SDK open to your users, the API only for internal addresses:
server {
listen 443 ssl;
server_name labelviewer.example.internal;
ssl_certificate /etc/nginx/tls/labelviewer.crt;
ssl_certificate_key /etc/nginx/tls/labelviewer.key;
client_max_body_size 8m;
location /sdk/ {
proxy_pass http://127.0.0.1:10090;
}
location / {
allow 10.0.0.0/8;
allow 192.168.0.0/16;
deny all;
proxy_pass http://127.0.0.1:10090;
proxy_set_header X-Request-Id $request_id;
}
}
The server sets Access-Control-Allow-Origin: * on the API and the SDK files itself; a proxy must pass these headers
through. If your gateway requires credentials, keep them on your servers: browsers must not hold them (see
Editor › Reference).
Health checks, logs and monitoring#
GET /healthzanswers200 {"status": "ok", ...}while the server is up.labelviewer healthchecks it (exit status 0 or 1); the image'sHEALTHCHECKruns that.GET /v1/infoshows the version, the SDK version and the license (status, products, expiry date, days left). Alert onlicense.daysLeftto renew in time; there is no grace period after the expiry date.- Access log: one JSON line per request on stdout:
time, requestId, method, path, status, durationMs, bytesIn, bytesOut, origin, error. Never query strings, bodies or label data.X-Request-Idfrom the request (if valid) is used and echoed, so you can correlate with your own logs. - Errors go to stderr as JSON with the request id and a stack trace.
- Responses
503 unavailablemean the render queue is full: add workers or containers.
Upgrades#
- Read the release notes, download the new archive and check it against its
SHA256SUMS. - Replace the program: stop the server, replace the
labelviewerexecutable and start it again; with Docker, build or pull the new image and replace the container (docker compose up -d, a rolling update, or stop and start). There is no data to migrate, and the configuration stays the same.labelviewer versionshows what runs. - Update the SDK URL in your apps. Each version serves its SDK at
/sdk/<version>/, and only the installed version exists: after the upgrade the old versioned URL answers 404. Change the URL and itsintegrityhash (both on the new SDK page) in your app's configuration, ideally in the same release as the server upgrade. Apps that use/sdk/latest/pick up the new SDK at once, without a change (and without a pinned hash). - If your apps use the downloaded TypeScript declarations, download them again.
For zero downtime with versioned URLs, run the old and new versions side by side behind the proxy for a while (route
/sdk/<old version>/ to the old container), or switch the apps and the server in one maintenance window.
Backups#
labelviewer stores nothing, so there is nothing of its own to back up. Back up:
- your database, which holds the templates (see Editor › Store templates);
- your license key, wherever you keep secrets;
- your deployment configuration (compose file, environment, proxy configuration).
Security checklist#
- The port is not reachable from the internet, or a gateway in front of it enforces your rules.
- TLS terminates at your proxy; browsers load the SDK over HTTPS (your app's CSP lists the install's origin).
- Your apps pin a versioned SDK URL with
integrity. - The server runs as an unprivileged user on a read-only filesystem; nothing needs to be writable.
- You checked each download against
SHA256SUMS. - Access logs go to your log system; they contain no label data.