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:

PlatformArchive
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 siliconlabelviewer-<version>-darwin-arm64.tar.gz
macOS, Intellabelviewer-<version>-darwin-x64.tar.gz
Windows x64labelviewer-<version>-win-x64.zip
allSHA256SUMS

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 versionVersion, SDK version and platform.
labelviewer --helpUsage.

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-slim plus 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-only works;
  • listens on 0.0.0.0:10090, and its HEALTHCHECK runs labelviewer 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.6 and libgcc_s.so.1 (the newest symbol versions it asks for are GLIBC_2.28, GLIBCXX_3.4.21 and CXXABI_1.3.11). The images above have them; scratch and gcr.io/distroless/static do not.
  • Alpine (musl) is not supported. The executable is linked against glibc (its loader is /lib64/ld-linux-x86-64.so.2 on x64, /lib/ld-linux-aarch64.so.1 on ARM64). Alpine would need at least the gcompat, libstdc++ and libgcc packages, 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-x64 archive for linux/amd64 images, linux-arm64 for linux/arm64. The archive's Dockerfile pins its platform in the FROM line, 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 health in 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.

VariableDefault
PORT10090Listening port.
HOST0.0.0.0Listening address. 127.0.0.1 keeps it to the local machine (e.g. behind a reverse proxy on the same host).
LICENSE_KEYunsetYour license key (one line, LV2.…). Unset: the included evaluation license is used (License).
LICENSE_FILEunsetPath to a file holding the license key (instead of LICENSE_KEY), e.g. a mounted secret.
RENDER_WORKERSmin(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_MS10000Time budget per render. When it runs out the worker is replaced and the request answers 422.
RENDER_MAX_QUEUE64Renders that may wait for a free worker before requests answer 503.
RENDER_MAX_HEAP_MB512Memory cap per render worker.
REQUEST_TIMEOUT_MS30000Deadline for a whole request (headers + body). Headers must arrive within 10 s.
ACCESS_LOGonfalse 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 /healthz answers 200 {"status": "ok", ...} while the server is up. labelviewer health checks it (exit status 0 or 1); the image's HEALTHCHECK runs that.
  • GET /v1/info shows the version, the SDK version and the license (status, products, expiry date, days left). Alert on license.daysLeft to 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-Id from 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 unavailable mean the render queue is full: add workers or containers.

Upgrades#

  1. Read the release notes, download the new archive and check it against its SHA256SUMS.
  2. Replace the program: stop the server, replace the labelviewer executable 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 version shows what runs.
  3. 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 its integrity hash (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).
  4. 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.