Skip to content
dockettePublic

About

🐳 Safe noVNC-based file viewer based on Kasm & Neko

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

Β 

History

43 Commits

Folders and files

Repository files navigation

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
Loading

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:

Group Extensions Opens with
Media mp4, 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, mid VLC
Web / docs pdf, html, htm Chromium (new window)
Images png, jpg, jpeg, gif, webp, svg, bmp Chromium (new window)
Images (other) tif, tiff, heic, heif, avif, ico, tga Ristretto (downloaded locally first)
Office doc, docx, odt, rtf, xls, xlsx, ods, csv, ppt, pptx, odp, odg, vsd, vsdx, pub, pages, key, numbers LibreOffice
Ebooks epub, djvu, xps, oxps, cbz, cbr, cb7 Atril (downloaded locally first)
Text txt, log, json, yaml, yml, xml, md, ini, conf, cfg, toml Mousepad (downloaded locally first)
Archives zip, 7z, rar, tar, gz, tgz, bz2, tbz2, xz, txz, zst, tzst Xarchiver (downloaded locally first)
Email eml, msg Claws Mail (downloaded locally first; .msg converted via msgconvert)
Captures pcap, pcapng, cap Wireshark (downloaded locally first)
3D models stl, obj, ply, glb, gltf, 3ds, vtk, vtp F3D (downloaded locally first)
CAD dxf LibreCAD (downloaded locally first)
Fonts ttf, otf, ttc GNOME Font Viewer (downloaded locally first)
Binaries bin, exe, dll, so, o, elf, dat, dmp GHex (downloaded locally first)
Other anything else Chromium, 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

Method Path Purpose
GET / UI: iframe + URL bar + slot tabs
GET /healthz {ready, total} β€” slots whose desktop and sidecar answer; 503 when none
GET /api/slots slot table with per-slot vnc, sidecar and ready flags
POST /api/slot/{i}/params forwards {params:{url,…}} to slot i
POST /api/slot/{i}/restart restarts slot i's container (requires Content-Type: application/json)
GET /api/slot/{i}/status what 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

Var Default Purpose
LISTEN_ADDR :8080 Plain-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_SLOTS kasm://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_PORT 6901 VNC port used for bare entries and the kasm:// pseudo-scheme.
VIEWER_WEBTOP_DEFAULT_PORT 3000 VNC port used for the webtop:// pseudo-scheme.
VIEWER_SIDECAR_DEFAULT_PORT 7000 Sidecar 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_CERT Cert used When 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.
generate Self-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.
file TLS_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):

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

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

Input Parsed as
(omitted) http://<vnc-host>:7000
viewer-1 http://viewer-1:7000
viewer-1:7100 http://viewer-1:7100
http://other-host:7100 http://other-host:7100
https://sidecar-1:7443 https://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

Method Path Purpose
GET /healthz plain-text ok
POST /params validate + atomically write /tmp/viewdoc.{json,env}, fork viewdoc.sh
POST /restart reply 202, then exit with code 42 β€” the image turns that into a container exit
GET /status latest 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

Var Default Purpose
LISTEN_ADDR :7000 Sidecar 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 tag Meaning
dockette/viewdoc:<variant> latest master build
dockette/viewdoc:<variant>-2026.10.12 release 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.

About

🐳 Safe noVNC-based file viewer based on Kasm & Neko

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages