New apps start in the server’s default mode,
metadata. Nothing runs while idle in any mode: there is no resident browser, and a screenshot capture starts a container, takes one picture and removes the container again.
Choose a mode
- UI
- CLI
Open the app, then Deploy settings, then Deploy previews, and pick a mode.
preview enable without --mode means screenshot, as enabled: true always did. The next successful deploy gets a thumbnail. Recapture in deploy history (or levelrail-cli preview capture) redoes the current release on demand. Each thumbnail carries a badge saying where it came from: Screenshot, Site image or Card.
What the metadata mode does
- Requests the configured path (default
/) on the app itself, through the app’s published loopback port, the same route the ingress uses. It never contacts the public internet for the page, and you never supply a URL, only a path. - Sends no cookies or credentials. A 401 or 403, or a redirect to a login page, is recorded as skipped with a reason. Redirects that leave the app are refused, and at most 3 are followed.
- Reads at most 512 KB of HTML with a real HTML tokenizer and keeps the title,
og:title,og:description,og:image,twitter:image,theme-colorand icon links. - If there is an
og:image(ortwitter:image), it is resolved against the page, must be anhttporhttpsURL, and is downloaded through the same outbound guard the webhooks use: loopback, private, link-local and metadata-service addresses are refused at connect time, including after redirects. The app’s own origin and its own domains are read from the app itself. Limit 2 MB. - JPEG and PNG images are supported. The image is decoded, scaled down to fit 640 by 400 (never up) and centered on a neutral background, then stored as a JPEG. Images under 120 pixels on a side and single-color images are rejected.
- When there is no usable image, the preview is a card: the deploy history composes it from the page title, description and theme color, with the app name and commit. A card stores about 100 bytes of text and no image file.
What a screenshot capture does
- Pulls the browser image the first time it is needed (
docker.io/chromedp/headless-shell, pinned to a version, about 140 MB compressed). - Starts one container with a 512 MB memory cap, 1 CPU, a read-only root filesystem, a small tmpfs, all capabilities dropped, a non-root user and a process limit.
- Attaches it to the app’s own Docker network and nothing else. It reaches the app by service name and port. You never supply a URL, only a path on the app.
- Loads the page at 1280x800 without cookies or credentials, waits for the page to settle (at most about 10 seconds), takes a screenshot, and stops the container. The whole capture has a 30 second limit.
- Shrinks the screenshot to a 640 pixel wide JPEG, usually 20 to 60 KB.
Cost
Screenshot captures are skipped, with the reason recorded, when the host has less than 768 MB of free memory or less than 2 GB of free disk. The metadata mode is not gated on either, because it needs neither.
To use a different browser image, point
APP_PREVIEW_IMAGE at it. It must expose the DevTools port on 9222 like headless-shell does.
Privacy
A preview is a picture of what the app served, so treat it like the app.- Metadata is on by default and screenshots are opt-in. The default reads only the public head of your page and, at most, one image the page itself names. A screenshot is never taken unless you choose it for that app.
- Internal pages can leak. If the path you pick shows internal data to anyone who can reach the app, the thumbnail shows it too. Pick a public page, such as
/. - The browser never logs in. It sends no cookies and no tokens, and a 401 or 403, or a redirect to a login page, is recorded as skipped with no image stored.
- The image has the same access as the app. It is served only to people who can read the app, through the authenticated API. It is never public and never appears on the public status page.
- This is not a sandbox for hostile apps. The page is loaded by a real browser next to the app. The container limits above contain a browser bug, but only capture apps you run.
- The browser’s debugging port is published on the host loopback interface for the length of a capture, on a random port, so other processes on the host can reach it during that window.
What is skipped
The deploy history shows a “No preview” note with the reason when a capture does not produce an image:
A failed recapture never replaces a preview that already exists (image or card), and a card never replaces a site image or screenshot.
Retention and cleanup
These rules apply to every source alike: screenshots, site images and cards.- The newest 5 previews of each app are kept, plus the preview of the release now serving.
- Previews older than 30 days are deleted, except the live release’s.
- If all previews together pass 200 MB, the least recently viewed go first.
- Previews are deleted when the app is deleted.
- Every capture container carries a role label. On startup and every hour, any older than twice the capture timeout (left by a crash) is removed.
- The browser image is removed after 14 idle days, or at the next cleanup pass once the server switch is off, but only if this control plane pulled it and no container uses it. An image you pulled yourself is never touched.
- Prune now (or
levelrail-cli preview prune <app>) applies these rules immediately.--alldeletes every preview of the app.
Settings
Set these on the control plane. Unset or malformed values fall back to the default.API
Reading needs the app read ability. Changing settings, capturing and pruning need write.
GET /api/v1/apps/{name}/deploy-attempts includes preview_image_url on each attempt that has a thumbnail.
Limits
- Local node only. Apps on other nodes are skipped.
- Screenshots skip apps without a per-app network or a declared port. Metadata needs only a running container with a published port.
- WebP and other formats are not decoded for site images, only JPEG and PNG.
- The page is requested by service name, so an app that routes by
Hostheader shows its default site. - Only the viewport is captured, not the full page. Cookie banners and late-loading content appear as the app renders them.