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: 1gbThe 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 unihtmlunihtml.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.
| Limit | Default | Set by |
|---|---|---|
| Client HTTP timeout | 30s | Replaced by SetTimeoutDuration when set |
| Conversion timeout | 15s | SetTimeoutDuration raises it; a context on GetPdfPages can only shorten it |
| Server default timeout | 30s | --default-timeout |
| Maximum query time | 10m | Server-side ceiling |
| Upload expiry | 3m | --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:
| Flag | Default | Purpose |
|---|---|---|
--port | 8080 | Public API port |
--private-port | 8081 | Internal file host. No need to publish it. |
--prefix | none | URI prefix, if the server sits under a path |
--default-timeout | 30s | Default conversion timeout |
--file-expiration | 3m | How long an upload survives |
--min-file-size | 1MB | Above this, uploads go to disk instead of memory |
--ignore-cert-errors | false | Skip 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:202608Upgrade 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.