Skip to content
How do I run the UniHTML server in production?

How do I run the UniHTML server in production?

The same image you develop against, with a health check, a private network, and enough memory for Chrome. There is no separate production build.

services:
  unihtml:
    image: unidoccloud/unihtml:202608
    environment:
      UNIDOC_METERED_API_KEY: ${UNIDOC_LICENSE_API_KEY}
    # No ports block. The application container reaches this by service name.
    # For local testing only, bind loopback rather than every interface:
    #   ports:
    #     - "127.0.0.1:8080:8080"
    healthcheck:
      test: ["CMD", "curl", "-fsS", "-o", "/dev/null", "http://localhost:8080/health"]
      interval: 30s
      timeout: 5s
      retries: 3
    restart: unless-stopped
    shm_size: 1gb

The pinned tag, the absent ports block and shm_size are the parts that differ from a development setup, and each has a section below.

Keep it off the public network

The conversion endpoint has no authentication. The only credential the server checks is its own license, so anything that can reach port 8080 can convert HTML, and can point the URL method at addresses inside your network.

Publish it to your application only. On a single host that means dropping the ports block entirely and letting the application container reach it by service name. On Kubernetes it means a ClusterIP service and no ingress. Put a proxy in front of it if it has to cross a boundary, and use that proxy for TLS and for whatever authentication you need.

Give Chrome shared memory

shm_size: 1gb is the line people leave out. Chrome uses /dev/shm heavily and Docker’s default is 64MB, which is enough for a small page and not enough for a large or image-heavy one. The symptom is a renderer that crashes on some documents and works on others, which reads like a bug in the HTML.

Budget around 1GB of memory for the container beyond that, more if you convert large documents or run several at once.

Health checks

GET /health returns 200 when the server is up. Use it for the container health check and the readiness probe. Note that an unlicensed server exits at startup rather than serving errors, so a container that will not stay up is usually a license problem:

docker logs unihtml

unihtml.Connect health-checks on the client side too, with a five second timeout, so a long-lived process should call it at startup and check the error. A failed Connect still installs a client pointed at the bad address, so an ignored error turns into a failure on every conversion instead of one at boot.

Timeouts

Five separate limits apply, and they interact.

LimitDefaultSet by
Client HTTP timeout30sReplaced by SetTimeoutDuration when set
Conversion timeout15sSetTimeoutDuration raises it; a context on GetPdfPages can only shorten it
Server default timeout30s--default-timeout
Maximum query time10mServer-side ceiling
Upload expiry3m--file-expiration

Raise SetTimeoutDuration for pages that legitimately take a while, and pass a context deadline on GetPdfPages so an abandoned HTTP request of your own stops occupying a rendering slot.

Waiting for a selector does not extend the budget, so a page that needs 20 seconds of rendering needs the timeout raised as well as the wait set.

Concurrency and scale

Each conversion drives a browser, which is expensive compared with the rest of your stack. Rendering is where the time goes, not the HTTP hop.

Scale out rather than up: run more replicas behind a round-robin service. The server keeps no state between requests beyond the short-lived upload, so replicas need no coordination and no shared storage.

Bound the concurrency on your side too. Without a limit, a burst of requests turns into a burst of browser instances and the container runs out of memory rather than queueing. A semaphore sized to what the container can actually handle is the simplest thing that works.

Flags and environment variables

The server takes flags, or environment variables with a UNIHTML_ prefix:

FlagDefaultPurpose
--port8080Public API port
--private-port8081Internal file host. No need to publish it.
--prefixnoneURI prefix, if the server sits under a path
--default-timeout30sDefault conversion timeout
--file-expiration3mHow long an upload survives
--min-file-size1MBAbove this, uploads go to disk instead of memory
--ignore-cert-errorsfalseSkip TLS verification on fetched URLs

--ignore-cert-errors exists for self-signed certificates in a test environment. Leave it off elsewhere: it applies to every URL the renderer fetches, not just yours.

If you run the server under a path prefix, connect with the prefix in the address rather than through ConnectOptions, whose Prefix field is not currently passed through to the client:

unihtml.Connect("https://internal.example.com:8080/unihtml")

Version pinning

:latest is convenient for development and a poor choice for a deployment you want to be reproducible, because the image moves under you without a review. Server images are tagged YYYYMM, so pin the month you tested against:

image: unidoccloud/unihtml:202608

Upgrade the client and the server together, and re-run whatever check you have for the pages you care about; the renderer is a browser, and browsers change how they lay things out.

Last updated on