Sign inSign up

dockette/viewdoc

Sponsored OSS

By dockette

โ€ขUpdated about 14 hours ago

๐Ÿณ Safe noVNC-based file viewer based on Kasm & Neko

Image
0

7.0K

dockette/viewdoc repository overview

โ Dockette / Viewdoc

๐Ÿ“„ ๐ŸŽž๏ธ ๐Ÿ–ผ๏ธ Browser-embeddable remote viewer for PDFs, office docs, audio, video, and images. A small Go control-center fronts a fixed pool of Kasmโ  / LinuxServer Webtopโ  desktop containers โ€” pass ?url=... and the file opens in the right app inside an iframe.

๐Ÿ•น f3l1x.ioโ  | ๐Ÿ’ป f3l1xโ  | ๐Ÿฆ @xf3l1xโ 

GitHub Actions Docker Hub pulls GitHub Sponsors Support/Discussions


Kasm desktop viewer Webtop desktop viewer


โ Why

Embedding rich file viewers in your product is a thousand papercuts: PDF.js for some types, an office viewer for others, a media player for the rest โ€” each with its own quirks, sandbox story, and asset list. Viewdoc skips it. Open a real desktop in a tab, let the desktop open the file with its native app, and stream the pixels back. One iframe, every file type.

flowchart LR
    browser["Browser<br/>localhost:8080/?url=โ€ฆ"]

    subgraph cc["control-center (Go)"]
        proxy["reverse-proxy<br/>/slot/{i}/*"]
        api["POST /api/slot/{i}/params"]
    end

    subgraph viewer["viewer-i (Kasm / Webtop)"]
        novnc["KasmVNC :6901<br/>or Webtop :3000"]
        sidecar["sidecar :7000"]
        hook["viewdoc.sh"]
        apps["chromium ยท libreoffice ยท vlc<br/>xarchiver ยท wireshark ยท claws-mail"]
    end

    browser -->|iframe src=/slot/i/| proxy
    browser -->|"{url:โ€ฆ}"| api
    proxy -->|HTTP + WS| novnc
    api -->|POST /params| sidecar
    sidecar -->|fork| hook
    hook -->|dispatch by ext| apps

โ Quickstart

docker compose build
docker compose up -d

open https://localhost:8443/                                   # iframe + URL bar + slot tabs
open "https://localhost:8443/?url=https://example.com/x.pdf"   # auto-opens in slot 0
curl -sk https://localhost:8443/healthz                        # {"ready":4,"total":4}

Default pool (VIEWER_SLOTS): 2ร— Kasm + 2ร— Webtop.

โ File Types

The dispatcher routes by extension:

GroupExtensionsOpens with
Mediamp4, mkv, webm, mov, avi, mpeg, mpg, m4v, 3gp, 3g2, ts, mts, m2ts, vob, wmv, asf, divx, ogv, mp3, wav, flac, ogg, m4a, aac, opus, mka, wma, aiff, ape, midVLC
Web / docspdf, html, htmChromium (new window)
Imagespng, jpg, jpeg, gif, webp, svg, bmpChromium (new window)
Images (other)tif, tiff, heic, heif, avif, ico, tgaRistretto (downloaded locally first)
Officedoc, docx, odt, rtf, xls, xlsx, ods, csv, ppt, pptx, odp, odg, vsd, vsdx, pub, pages, key, numbersLibreOffice
Ebooksepub, djvu, xps, oxps, cbz, cbr, cb7Atril (downloaded locally first)
Texttxt, log, json, yaml, yml, xml, md, ini, conf, cfg, tomlMousepad (downloaded locally first)
Archiveszip, 7z, rar, tar, gz, tgz, bz2, tbz2, xz, txz, zst, tzstXarchiver (downloaded locally first)
Emaileml, msgClaws Mail (downloaded locally first; .msg converted via msgconvert)
Capturespcap, pcapng, capWireshark (downloaded locally first)
3D modelsstl, obj, ply, glb, gltf, 3ds, vtk, vtpF3D (downloaded locally first)
CADdxfLibreCAD (downloaded locally first)
Fontsttf, otf, ttcGNOME Font Viewer (downloaded locally first)
Binariesbin, exe, dll, so, o, elf, dat, dmpGHex (downloaded locally first)
Otheranything elseChromium, otherwise xdg-open

Only http:// and https:// URLs are accepted; anything else is refused before dispatch. Both images also ship exiftool for metadata inspection from a terminal, and Office-metric fonts (Liberation, Carlito, Caladea) so documents keep their original layout.

โ Services

The system exposes two HTTP services. The control-center is the public entrypoint; the sidecar runs inside every viewer image on the compose network.

โ Control Center โ€” :8080 (plain), :8443 (TLS)
โ Endpoints
MethodPathPurpose
GET/UI: iframe + URL bar + slot tabs
GET/healthz{ready, total} โ€” slots whose desktop and sidecar answer; 503 when none
GET/api/slotsslot table with per-slot vnc, sidecar and ready flags
POST/api/slot/{i}/paramsforwards {params:{url,โ€ฆ}} to slot i
POST/api/slot/{i}/restartrestarts slot i's container (requires Content-Type: application/json)
GET/api/slot/{i}/statuswhat happened to slot i's latest request (see sidecar /status)
ANY/slot/{i}/*reverse-proxy โ†’ <slot[i]>:port (HTTP + WS)
GET/*embedded UI assets (/app.js, /style.css)
โ Environment
VarDefaultPurpose
LISTEN_ADDR:8080Plain-HTTP bind address
TLS_ADDR(unset)If set, also bind TLS. The cert comes from TLS_CERT (see TLS certificate below).
TLS_CERT(inferred)embedded, generate or file. Unset: file if TLS_CERT_FILE is set, generate if TLS_CERT_DIR is set, else embedded.
TLS_CERT_DIR(unset)generate: keep the cert here (server.crt + server.key) so it survives restarts. Unset = new in-memory cert on every start.
TLS_HOSTS(unset)generate: extra comma-separated IPs / DNS names for a new cert.
TLS_CERT_FILE, TLS_KEY_FILE(unset)file: paths to your own PEM cert (full chain) and key.
VIEWER_SLOTSkasm://viewer-kasm-1,kasm://viewer-kasm-2,webtop://viewer-webtop-1,webtop://viewer-webtop-2 (the compose services)Comma-separated slot list. See Slot configuration below for the full grammar.
VIEWER_KASM_DEFAULT_PORT6901VNC port used for bare entries and the kasm:// pseudo-scheme.
VIEWER_WEBTOP_DEFAULT_PORT3000VNC port used for the webtop:// pseudo-scheme.
VIEWER_SIDECAR_DEFAULT_PORT7000Sidecar port used when the sidecar half of a slot entry is omitted (or specifies no port).
โ TLS certificate

TLS_ADDR serves the same UI over HTTPS (browsers only allow clipboard access in a secure context). The startup log prints which cert is used and its SHA-256 fingerprint.

TLS_CERTCert usedWhen to use it
embedded (default)Pre-generated self-signed pair from certs/, embedded in the image (make cert regenerates it).Local and trusted networks: same fingerprint everywhere, so the browser's "accept once" sticks. The key is public in this repo, so it gives no real protection.
generateSelf-signed pair generated on first start. With TLS_CERT_DIR it's written there (key 0600) and reused, so the fingerprint stays stable; compose mounts a certs volume at /certs for this. Without it, a new in-memory cert on every start. Delete the files to regenerate.A private key per deployment. Always covers localhost, 127.0.0.1, ::1, plus TLS_HOSTS.
fileTLS_CERT_FILE + TLS_KEY_FILE, read at startup.Your own cert, e.g. from your CA or Let's Encrypt (mount the files read-only).
โ Slot configuration

Each entry in VIEWER_SLOTS has the shape <vnc>[|<sidecar>]. The pipe is optional โ€” omit it when the sidecar lives on the same host as the VNC endpoint (the compose default).

VNC half โ€” accepted forms (port falls back to VIEWER_KASM_DEFAULT_PORT, except webtop:// which uses VIEWER_WEBTOP_DEFAULT_PORT):

InputParsed as
viewer-1http://viewer-1:6901
viewer-1:9000http://viewer-1:9000
http://viewer-1http://viewer-1:6901
https://viewer-1https://viewer-1:6901
https://viewer-1:6901https://viewer-1:6901 (canonical Kasm)
kasm://viewer-1https://viewer-1:6901 (kasm pseudo-scheme = https+kasm)
kasm://viewer-1:9000https://viewer-1:9000
webtop://viewer-1http://viewer-1:3000 (webtop pseudo-scheme = http+webtop)
webtop://viewer-1:9001http://viewer-1:9001

Sidecar half โ€” accepted forms (port falls back to VIEWER_SIDECAR_DEFAULT_PORT; kasm:// / webtop:// are not allowed here):

InputParsed as
(omitted)http://<vnc-host>:7000
viewer-1http://viewer-1:7000
viewer-1:7100http://viewer-1:7100
http://other-host:7100http://other-host:7100
https://sidecar-1:7443https://sidecar-1:7443

Worked examples

# Compose (DNS-friendly, sidecar implicit on same host:7000)
VIEWER_SLOTS="kasm://viewer-kasm-1,kasm://viewer-kasm-2,webtop://viewer-webtop-1,webtop://viewer-webtop-2"

# Equivalent fully-explicit form
VIEWER_SLOTS="https://viewer-kasm-1:6901,https://viewer-kasm-2:6901,http://viewer-webtop-1:3000,http://viewer-webtop-2:3000"

# Nomad / dynamic ports โ€” VNC and sidecar published on different host ports
VIEWER_SLOTS="kasm://10.0.0.5:23456|10.0.0.5:34567,kasm://10.0.0.6:11111|10.0.0.6:22222"

# Override the pool-wide defaults so bare entries become 7901 / 3100 / 7900
VIEWER_KASM_DEFAULT_PORT=7901
VIEWER_WEBTOP_DEFAULT_PORT=3100
VIEWER_SIDECAR_DEFAULT_PORT=7900
VIEWER_SLOTS="kasm://node-1,webtop://node-2"
# โ†’ slot 0 vnc=https://node-1:7901 sidecar=http://node-1:7900
# โ†’ slot 1 vnc=http://node-2:3100  sidecar=http://node-2:7900
โ Sidecar โ€” :7000 (inside each viewer)
โ Endpoints
MethodPathPurpose
GET/healthzplain-text ok
POST/paramsvalidate + atomically write /tmp/viewdoc.{json,env}, fork viewdoc.sh
POST/restartreply 202, then exit with code 42 โ€” the image turns that into a container exit
GET/statuslatest request: phase (idle, queued, refused, deduped, downloading, failed, launched), url, app, error, updated, and for launched apps app_state (running, handoff, exited) + exit_code
โ Environment
VarDefaultPurpose
LISTEN_ADDR:7000Sidecar bind address (fixed by compose convention)
โ Restarting a slot

The Restart button (POST /api/slot/{i}/restart) makes the viewer container exit; whatever supervises it brings up a fresh one. The sidecar can't kill the container itself (on Webtop it runs as abc, PID 1 is root), so it exits with code 42 and the image's wrapper does the rest:

  • Kasm โ€” custom_startup.sh loops on the sidecar; on 42 it sends SIGTERM to PID 1 (vnc_startup.sh, same uid), which exits.
  • Webtop โ€” the s6 finish script (runs as root) calls /run/s6/basedir/bin/halt; the container exits with code 42.

Any other sidecar exit just respawns the sidecar. Restarting requires a restart policy: compose's restart: unless-stopped works as-is. On Nomad, the default restart stanza (attempts = 2 per 30m, mode = "fail") runs out after a few clicks, and the allocation gets rescheduled โ€” possibly to another node with new dynamic ports, which breaks a static VIEWER_SLOTS. Allow many quick in-place restarts:

restart {
  attempts = 100
  interval = "30m"
  delay    = "2s"
  mode     = "delay"
}

โ Development

Requires Go 1.27.2+ (go.mod pins the toolchain; older Go downloads it automatically) and Docker with Compose v2.

make test          # go test ./...
go vet ./...
make build         # build all three images, stamped with VERSION
make cert          # regenerate the embedded certs/server.{crt,key} for your host IPs
make version       # print VERSION
make release       # set VERSION to today's date (see Versions)

CI (.github/workflows/docker.yml) checks the VERSION format and runs go vet, go test -race, staticcheck, govulncheck, shellcheck and hadolint, then builds every image. Pushes to master publish linux/amd64 + linux/arm64 images to Docker Hub (see Versions). The Kasm and Webtop base images are pinned by digest; Dependabot opens weekly PRs for new digests, Go builder images and GitHub Actions.

โ Versions

viewdoc is versioned by release date: YYYY.MM.DD, or YYYY.MM.DD.N for another release on the same day. The current version is in VERSIONโ . It is built into both binaries (logged at startup, returned by the control-center's /healthz and the sidecar's /status) and set as the org.opencontainers.image.version label.

Image tagMeaning
dockette/viewdoc:<variant>latest master build
dockette/viewdoc:<variant>-2026.10.12release 2026.10.12 โ€” pin this
dockette/viewdoc:<variant>-<short-sha>immutable build of that commit

<variant> is control-center, viewer-kasm or viewer-webtop. Like other dockette images, release tags are rebuilt by the weekly schedule to pick up base-image updates; use the <short-sha> tag for a byte-identical image.

To release: make release sets VERSION to today's date (UTC; .1, .2, โ€ฆ if that date is already tagged), then commit and push to master. Once all images are pushed, CI tags the commit and creates the GitHub release <VERSION> with generated notes. A plain docker compose build stamps dev; make build uses VERSION.


โ Maintenance

See how to contributeโ  to this package. Consider to supportโ  f3l1x. Thank you for using this package.

Tag summary

Content type

Image

Digest

sha256:960ed39e8โ€ฆ

Size

4.4 GB

Last updated

about 14 hours ago

docker pull dockette/viewdoc:viewer-kasm-a45754d

This week's pulls

Pulls:

111

Sep 28 to Oct 4