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

30 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} reachability summary
GET /api/slots slot table with per-slot reachability
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)
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 (self-signed cert)
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).

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

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         # docker compose build (all three images)
make cert          # regenerate certs/server.{crt,key} for your host IPs

CI (.github/workflows/docker.yml) 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:

Tag Meaning
dockette/viewdoc:<variant> latest master build
dockette/viewdoc:<variant>-<short-sha> immutable build of that commit

<variant> is control-center, viewer-kasm or viewer-webtop. The Kasm and Webtop base images are pinned by digest; Dependabot opens weekly PRs for new digests, Go builder images and GitHub Actions.


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