A small Debian container for trying terminal web browsers side by side over SSH: Lynx, Links 2, the stock ELinks, and elinks-js, a self-built ELinks 0.20 with the QuickJS-ng JavaScript engine. It was built to compare how each one reads with a screen reader. This post is the complete rebuild guide; every code block has a Copy button and long blocks scroll inside their box.
How it works
- You SSH into the container (SecureCRT, PuTTY, or any terminal) as a normal user.
- You start one of four browsers with a web address:
lynx: the most linear output, numbered links, no JavaScript.links2: better table/frame layout, no JavaScript (its graphics mode needs a real display, so it is text-only over SSH).elinks: Debian's ELinks 0.13.2, tabs and menus, no JavaScript.elinks-js: ELinks 0.20.0 built from source with QuickJS-ng, so modern JavaScript (arrow functions,const, spread) runs on the page.
- elinks-js lives entirely in
/opt/elinks-js, so it never touches the Debian packages. A tiny wrapper in/usr/local/bin/elinks-jsstarts it with JavaScript switched on.
What to expect from JavaScript: simple scripts work, but ELinks only implements part of the browser DOM, so heavy sites (Gmail, Facebook, React apps) still break. Google search blocks every text browser ("Your browser isn't supported anymore"), with or without JavaScript. DuckDuckGo Lite works very well in all four.
Containers involved
| Container | Role | LAN IP |
|---|---|---|
| CT 124 (TextBrowser) | Debian 12, the four browsers, SSH login | x.x.0.237 (DHCP) |
Sizing: 2 cores, 2 GB RAM, 2 GB swap, 8 GB disk, unprivileged, nesting=1. The build uses about 1 GB of disk; 1 core and 512 MB is enough just to browse. No Caddy route, no DNS name, no Uptime Kuma monitor: it is only reached over SSH on the LAN.
Where to run each command
- Proxmox host shell: in the Proxmox web UI click pve → Shell (or SSH to the host as root).
- Inside CT 124 (TextBrowser): Proxmox web UI → 124 (TextBrowser) → Console, or
pct enter 124from the host shell. - Your PC: your SSH client (SecureCRT etc.).
Rebuild, step by step
1. Get the Debian 12 template
Proxmox host shell:
pveam update
pveam download local debian-12-standard_12.12-1_amd64.tar.zst
(If that exact version is gone, pveam available | grep debian-12 shows the current name; use it in the next step too.)
2. Create and start CT 124 (TextBrowser)
Proxmox host shell. Bulk is the storage name on this host; use yours (local-lvm works too).
pct create 124 local:vztmpl/debian-12-standard_12.12-1_amd64.tar.zst --hostname TextBrowser --cores 2 --memory 2048 --swap 2048 --rootfs Bulk:8 --net0 name=eth0,bridge=vmbr0,firewall=1,ip=dhcp --unprivileged 1 --features nesting=1 --start 1
3. Make a working folder on the host
Proxmox host shell:
mkdir -p /root/textbrowser && cd /root/textbrowser
4. Create both files
Paste each block into the Proxmox host shell. Each one writes one file into /root/textbrowser/.
install_browsers.sh: installs Lynx, Links 2 and ELinks from Debian, plus a UTF-8 locale.
cat > /root/textbrowser/install_browsers.sh <<'EOF'
#!/bin/bash
# Installs the three stock text browsers from Debian, plus a UTF-8 locale.
set -e
export DEBIAN_FRONTEND=noninteractive
apt-get update -q
apt-get install -y -q lynx links2 elinks locales
sed -i 's/^# *en_US.UTF-8/en_US.UTF-8/' /etc/locale.gen
locale-gen
update-locale LANG=en_US.UTF-8
echo "--- versions ---"
lynx -version | head -1
links2 -version | head -1
elinks --version | head -1
EOF
build_elinks_js.sh: builds the NetSurf libraries (libdom, libcss and friends), QuickJS-ng and ELinks 0.20.0 into /opt/elinks-js, then installs the elinks-js command.
cat > /root/textbrowser/build_elinks_js.sh <<'EOF'
#!/bin/bash
# Builds ELinks with the QuickJS-ng JavaScript engine into /opt/elinks-js
# and installs an "elinks-js" command that starts it with JavaScript on.
# The stock Debian elinks is left untouched.
set -e
export DEBIAN_FRONTEND=noninteractive
PREFIX=/opt/elinks-js
ELINKS_TAG=v0.20.0
QJS_TAG=v0.17.0
NS=https://download.netsurf-browser.org/libs/releases
echo "=== build dependencies ==="
apt-get update -q
apt-get install -y -q build-essential git meson ninja-build pkg-config cmake wget xxd \
gperf flex bison perl gettext libssl-dev zlib1g-dev libbz2-dev liblzma-dev \
libzstd-dev libbrotli-dev libexpat1-dev libidn2-dev libgpm-dev \
libcurl4-openssl-dev libsqlite3-dev libtre-dev libevent-dev libev-dev \
libncurses-dev >/dev/null
export PKG_CONFIG_PATH="$PREFIX/lib/pkgconfig"
mkdir -p "$PREFIX/lib/pkgconfig"
echo "$PREFIX/lib" > /etc/ld.so.conf.d/elinks-js.conf
# NetSurf libraries: ELinks needs libdom + libcss for JavaScript and CSS.
echo "=== NetSurf libraries ==="
mkdir -p /usr/local/src/netsurf
cd /usr/local/src/netsurf
[ -f buildsystem-1.10.tar.gz ] || wget -q "$NS/buildsystem-1.10.tar.gz"
rm -rf buildsystem-1.10
tar -xf buildsystem-1.10.tar.gz
make -C buildsystem-1.10 install Q= PREFIX="$PREFIX" >/dev/null
for p in libparserutils-0.2.5 libwapcaplet-0.4.3 libhubbub-0.3.8 libdom-0.4.2 libcss-0.9.2; do
echo "--- $p"
[ -f "$p-src.tar.gz" ] || wget -q "$NS/$p-src.tar.gz"
rm -rf "$p"
tar -xf "$p-src.tar.gz"
CFLAGS="-O2 -Wno-error" make -C "$p" install -j2 Q= PREFIX="$PREFIX" LIBDIR=lib \
COMPONENT_TYPE=lib-shared NSSHARED="$PREFIX/share/netsurf-buildsystem" >/dev/null
done
ldconfig
# QuickJS-ng: modern JavaScript engine (mujs from Debian is ES5 only).
echo "=== QuickJS-ng $QJS_TAG ==="
cd /usr/local/src
[ -d quickjs-ng ] || git clone -q https://github.com/quickjs-ng/quickjs.git quickjs-ng
cd quickjs-ng
git fetch -q --tags
git checkout -q "$QJS_TAG"
rm -rf build
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON \
-DCMAKE_INSTALL_PREFIX="$PREFIX" -DCMAKE_INSTALL_LIBDIR=lib >/dev/null
cmake --build build -j2 >/dev/null
cmake --install build >/dev/null
ldconfig
# quickjs-ng installs no pkg-config file, and ELinks' meson looks for one.
cat > "$PREFIX/lib/pkgconfig/qjs.pc" <<PC
prefix=$PREFIX
libdir=\${prefix}/lib
includedir=\${prefix}/include
Name: qjs
Description: QuickJS-ng JavaScript engine
Version: ${QJS_TAG#v}
Libs: -L\${libdir} -lqjs -lm -lpthread
Cflags: -I\${includedir}
PC
echo "=== ELinks $ELINKS_TAG ==="
cd /usr/local/src
[ -d elinks ] || git clone -q https://github.com/rkd77/elinks.git
cd elinks
git fetch -q --tags
git checkout -q "$ELINKS_TAG"
rm -rf build-qjs
meson setup build-qjs --prefix="$PREFIX" -Dquickjs=true -Dmujs=false \
-Dlibsixel=false -Ddoc=false -Dnls=false > /root/elinks_meson.log 2>&1 \
|| { tail -30 /root/elinks_meson.log; exit 1; }
ninja -C build-qjs > /root/elinks_ninja.log 2>&1 \
|| { grep -m20 -i error /root/elinks_ninja.log; exit 1; }
ninja -C build-qjs install >/dev/null
ldconfig
cat > /usr/local/bin/elinks-js <<'WRAP'
#!/bin/sh
# ELinks built with QuickJS-ng; JavaScript on by default
exec /opt/elinks-js/bin/elinks -eval 'set ecmascript.enable = 1' "$@"
WRAP
chmod 755 /usr/local/bin/elinks-js
echo "=== done ==="
grep -E "quickjs|libdom|libcss" /root/elinks_meson.log | grep -i -E "found: YES|: true" || true
/usr/local/bin/elinks-js --version | head -1
EOF
5. Push the scripts into CT 124
Proxmox host shell:
pct push 124 /root/textbrowser/install_browsers.sh /root/install_browsers.sh
pct push 124 /root/textbrowser/build_elinks_js.sh /root/build_elinks_js.sh
6. Install the stock browsers (about 1 minute)
Proxmox host shell. It ends by printing three version lines.
pct exec 124 -- bash /root/install_browsers.sh
7. Build elinks-js (about 5 to 10 minutes on 2 cores)
Proxmox host shell. It prints a line per stage; at the end you should see libcss found: YES, libdom found: YES, quickjs : true and ELinks 0.20.0.
pct exec 124 -- bash /root/build_elinks_js.sh
8. Add a login user
Inside CT 124 (TextBrowser) console. It asks for a password twice; the other questions can be left empty with Enter. Replace yourname with the user you want.
adduser yourname
Debian's SSH server allows password logins for normal users but not for root, so log in as this user.
9. Find the IP and connect
Proxmox host shell:
pct exec 124 -- hostname -I
On your PC, point your SSH client at that address, port 22, with the user from step 8. Optionally give the container a DHCP reservation in your router so the address never changes.
Using the browsers
Search with DuckDuckGo Lite (from the SSH session):
elinks-js https://lite.duckduckgo.com/lite/
Search straight from the command line (change the words after q=, use + for spaces):
lynx "https://lite.duckduckgo.com/lite/?q=proxmox+backup"
Quit keys: Lynx q then y; Links 2 q then y; ELinks / elinks-js q then Enter on Yes. In ELinks, Escape opens the menu bar; Setup → Options manager is where to turn off the tab bar and status bar for less screen-reader clutter (Save before leaving).
Troubleshooting: where the logs are
elinks-js build (meson configure step)
- Where:
/root/elinks_meson.loginside CT 124 (TextBrowser) - Open: CT 124 console
grep -i -E "error|not found|found: NO" /root/elinks_meson.log
- Look for:
ERROR: Dependency "xyz" not foundmeans a missing-devpackage: install it with apt and rerun step 7. Manyfound: NOlines (gnutls, lua, cmake for quickjs-ng, execinfo) are normal; only a line starting withERRORstops the build.
elinks-js build (compile step)
- Where:
/root/elinks_ninja.loginside CT 124 (TextBrowser) - Open: CT 124 console
grep -i -m20 "error" /root/elinks_ninja.log
- Look for: a C compile error names the file and line. Warnings are harmless. A healthy log ends with a
Linking target src/elinksline.
elinks-js won't start
- Where: the shared-library loader
- Open: CT 124 console
ldd /opt/elinks-js/bin/elinks | grep "not found"
- Look for: no output is healthy.
libparserutils.so.0 => not found(or any other/opt/elinks-jslibrary) means/etc/ld.so.conf.d/elinks-js.confis missing: recreate it with/opt/elinks-js/libin it and runldconfig.
JavaScript errors on a page
- Where: the ELinks status line (bottom row) while the page is open
- Open: your SSH session
- Look for: a
Jin the status indicators means a script failed;SyntaxError: unexpected tokenmeans the engine could not parse the script (this is what mujs printed before the switch to QuickJS-ng). Pages that use DOM features ELinks has not implemented simply show their non-JavaScript content.
SSH login refused
- Where: sshd journal inside CT 124 (TextBrowser)
- Open: CT 124 console
journalctl -u ssh -n 30 --no-pager
- Look for:
Accepted password for yournameis a good login.Failed passwordis a wrong password.ROOT LOGIN REFUSED/Failed password for rootmeans you tried root, which is blocked for passwords by default; use the user from step 8.
Quick health check
Proxmox host shell; should print four version lines:
pct exec 124 -- sh -c "lynx -version | head -1; links2 -version | head -1; elinks --version | head -1; /usr/local/bin/elinks-js --version | head -1"
Notes
- mujs is the wrong engine. Debian ships
libmujs-devand ELinks builds with it fine, but mujs only speaks ES5: modern pages fail withSyntaxError. QuickJS-ng fixed it. elinks -dumpnever runs JavaScript, so it can't be used to test the build. Test interactively, or in a detachedtmuxsession withtmux capture-pane -p.- quickjs-ng installs no pkg-config file; the build script writes
/opt/elinks-js/lib/pkgconfig/qjs.pcby hand, otherwise meson stops withDependency "qjs" not found. - Meson strips the rpath at install, so the binary can't find the NetSurf libraries unless
/opt/elinks-js/libis registered withldconfig(done by the script). - NetSurf's buildsystem tarball is
buildsystem-1.10.tar.gz(no-src, unlike the libraries), and its git address ongit.netsurf-browser.orgover https does not exist. - ELinks' meson option for images is
libsixel, notsixel. It's off here: SecureCRT's sixel support is uncertain and images don't help a screen reader. - ELinks 0.20 also needs
xxd,libtre-dev,libevent-dev/libev-devandlibncurses-dev, all in the script's apt line. - To update later: change
ELINKS_TAG/QJS_TAGat the top ofbuild_elinks_js.sh, push it again (step 5) and rerun step 7. - To remove elinks-js completely:
rm -rf /opt/elinks-js /usr/local/src/elinks /usr/local/src/quickjs-ng /usr/local/src/netsurf /usr/local/bin/elinks-js /etc/ld.so.conf.d/elinks-js.confandldconfig.