Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/workflows/build-desktop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Build desktop image

on:
push:
tags: ["desktop-v*"]
pull_request:
paths:
- "desktop/**"
- ".github/workflows/build-desktop.yml"
workflow_dispatch:

permissions:
contents: read
packages: write

env:
IMAGE: ghcr.io/janeliascicomp/fg-interactive-apps/desktop

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: docker/setup-buildx-action@v3

- name: Log in to GHCR
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Compute image tags
id: tags
run: |
if [[ "$GITHUB_REF" == refs/tags/desktop-v* ]]; then
version="${GITHUB_REF_NAME#desktop-v}"
echo "tags=${IMAGE}:${version},${IMAGE}:latest" >> "$GITHUB_OUTPUT"
else
echo "tags=${IMAGE}:dev" >> "$GITHUB_OUTPUT"
fi

- name: Build (and push, except on pull requests)
uses: docker/build-push-action@v6
with:
context: desktop
platforms: linux/amd64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.tags.outputs.tags }}
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Each app lives in its own subdirectory with its own `runnables.yaml` manifest. F
| App | Directory | Description |
|-----|-----------|-------------|
| **OpenVSCode** | [`vscode/`](vscode/) | Browser-based VS Code IDE ([openvscode-server](https://github.com/gitpod-io/openvscode-server)) running in an Apptainer container, with one-click tokenized access. |
| **Remote Desktop** | [`desktop/`](desktop/) | Full Linux desktop (XFCE) with [Fiji](https://fiji.sc/) preinstalled, streamed to the browser via VNC/noVNC from an Apptainer container. |
| **JupyterLab** | [`jupyterlab/`](jupyterlab/) | JupyterLab notebook server (SciPy stack) running in an Apptainer container. |
| **marimo** | [`marimo/`](marimo/) | [marimo](https://marimo.io/) reactive Python notebook server running in an Apptainer container. |
| **TensorBoard** | [`tensorboard/`](tensorboard/) | [TensorBoard](https://www.tensorflow.org/tensorboard) training-log dashboard running in an Apptainer container. |
Expand Down
68 changes: 68 additions & 0 deletions desktop/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Browser-accessible XFCE desktop with Fiji, for the Fileglancer Remote
# Desktop app. Runs read-only under rootless Apptainer on cluster nodes: no
# systemd/s6, nothing writes inside the image at runtime, and everything is
# started by /opt/fg-desktop/start-desktop.sh as the (non-root) invoking user.
FROM ubuntu:24.04

ENV DEBIAN_FRONTEND=noninteractive \
LANG=C.UTF-8

# Desktop + VNC stack, plus the X/GL/font libraries Fiji (Java Swing) needs.
# --no-install-recommends keeps the image lean and, importantly, keeps screen
# lockers (light-locker/xfce4-screensaver) out: a locked screen inside the
# container could never be unlocked.
RUN apt-get update && apt-get install -y --no-install-recommends \
xfce4 \
xfce4-terminal \
elementary-xfce-icon-theme \
dbus-x11 \
xauth \
x11-xserver-utils \
tigervnc-standalone-server \
python3 \
python3-pip \
ca-certificates \
curl \
unzip \
fonts-dejavu-core \
fontconfig \
libxext6 \
libxrender1 \
libxtst6 \
libxi6 \
libxrandr2 \
libgl1 \
libglu1-mesa \
&& rm -rf /var/lib/apt/lists/*

# websockify >= 0.11 provides the UnixDomainSocketDirectory token plugin that
# the launcher's security model depends on; assert it imports at build time.
RUN pip3 install --no-cache-dir --break-system-packages websockify==0.13.0 \
&& python3 -c 'from websockify.token_plugins import UnixDomainSocketDirectory'

# noVNC browser client (static files only; websockify does the serving)
ARG NOVNC_VERSION=1.6.0
RUN curl -fsSL "https://github.com/novnc/noVNC/archive/refs/tags/v${NOVNC_VERSION}.tar.gz" \
| tar -xz -C /opt \
&& mv "/opt/noVNC-${NOVNC_VERSION}" /opt/novnc

# Fiji, with bundled JDK. Only a rolling "latest" bundle is published these
# days (the versioned releases/ directory stopped at 2.14.0 in 2023), so
# reproducibility comes from this image's version tag, not the Fiji URL.
RUN curl -fsSL https://downloads.imagej.net/fiji/latest/fiji-latest-linux64-jdk.zip \
-o /tmp/fiji.zip \
&& unzip -q /tmp/fiji.zip -d /tmp/fiji-extract \
&& rm /tmp/fiji.zip \
&& d="$(find /tmp/fiji-extract -mindepth 1 -maxdepth 1 -type d)" \
&& mv "$d" /opt/fiji \
&& rmdir /tmp/fiji-extract \
&& { test -x /opt/fiji/fiji-linux-x64 \
|| test -x /opt/fiji/ImageJ-linux64 \
|| test -x /opt/fiji/fiji; }

COPY fiji-wrapper.sh /usr/local/bin/fiji
COPY fiji.desktop /usr/share/applications/fiji.desktop
COPY start-desktop.sh /opt/fg-desktop/start-desktop.sh
RUN chmod 755 /usr/local/bin/fiji /opt/fg-desktop/start-desktop.sh

CMD ["/opt/fg-desktop/start-desktop.sh"]
48 changes: 48 additions & 0 deletions desktop/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Remote Desktop

Runs a full Linux desktop (XFCE) with [Fiji](https://fiji.sc/) preinstalled on a cluster node as a Fileglancer **service**, streamed to your browser with VNC ([TigerVNC](https://tigervnc.org/) + [noVNC](https://novnc.com/)), entirely inside an [Apptainer](https://apptainer.org/) container. The only host requirement is Apptainer.

Your home directory is bind-mounted and writable, so files, XFCE settings, and anything you save persist across sessions. An optional **Data Folder** parameter bind-mounts an additional file-share folder and links it on the desktop.

## One-click access

Fileglancer mints a per-session secret (`$FG_SERVICE_TOKEN`) and publishes the service URL with the token embedded (`?path=websockify%3Ftoken%3D...`) once the server is ready, so clicking **Open Service** drops you straight onto the desktop — no password prompt. The desktop auto-resizes to fit your browser window (`resize=remote`).

## Security model

The session is protected at every hop, including against other users on the same shared compute node:

- **Network**: the only listener is websockify on the Fileglancer-provided port. It serves the noVNC page to anyone, but only tunnels WebSocket connections that present the correct per-job 192-bit token.
- **VNC server**: Xvnc listens **only on a unix domain socket** in a mode-0700 node-local directory (`-rfbport -1` disables TCP entirely). The socket file is named after the token, and websockify's `UnixDomainSocketDirectory` token plugin maps token → socket, so a wrong token matches nothing and local users cannot reach the VNC server at all — the kernel enforces the directory permissions.
- **X display**: the X server requires an xauth cookie (mode 0600), so other local users cannot attach to the display through `/tmp/.X11-unix`.
- The token never appears on a command line (only the socket *directory* does), so it cannot leak via `ps`.

**Caveats**: anyone with the tokenized URL gets the full desktop, including a terminal running as you, while the job runs — treat the URL as a secret and stop the service when finished. Traffic between your browser and the compute node is plain HTTP (unencrypted), the same trade-off as the other apps in this repository.

## How it works

- The image (`ghcr.io/janeliascicomp/fg-interactive-apps/desktop`) is pulled to your per-user Apptainer cache on first launch and reused afterwards. It is large (several GB), so the first launch takes a while — Fileglancer shows the "pulling image" phase during the wait.
- `start-desktop.sh` finds a free X display, starts Xvnc on a private unix socket with an xauth cookie, launches an XFCE session over D-Bus, and serves noVNC/websockify on `$FG_SERVICE_PORT` in the foreground — stopping the job tears everything down.
- Fiji lives at `/opt/fiji` inside the read-only image, with a launcher on the desktop and in the applications menu.

## Fiji notes

- The image install is **read-only**, so the ImageJ updater cannot add plugins or update sites there (automatic update checks are disabled). To use custom plugins/update sites, unpack your own Fiji into your home directory — it persists across sessions and appears in the same desktop.
- Fiji is refreshed to the then-current release whenever a new image version is built; pin behavior by keeping the `container:` tag in `runnables.yaml` at a version you have validated.

## Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| **Data Folder** | directory | — | Optional folder to show on the desktop. Must be within a mounted file share; bind-mounted into the container automatically. Your home directory is always available. |

## Building and releasing the image

The image is built by the [`build-desktop`](../.github/workflows/build-desktop.yml) GitHub Actions workflow. Pull requests touching `desktop/` get a validation build; pushing a tag like `desktop-v1.0.0` builds and publishes `ghcr.io/janeliascicomp/fg-interactive-apps/desktop:1.0.0` (and `:latest`).

After the first publish, set the GHCR package visibility to **public** (GitHub → org → Packages → `fg-interactive-apps/desktop` → settings) so Apptainer on the cluster can pull it anonymously. Then update the `container:` tag in `runnables.yaml` to match.

## Notes

- **Walltime** defaults to `08:00`. The desktop runs until you stop it or the walltime expires — raise it for longer sessions.
- Resolution defaults to 1920x1080 before the first browser connect; after that the desktop follows your browser window size.
14 changes: 14 additions & 0 deletions desktop/fiji-wrapper.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/bin/bash
# Launch Fiji from the read-only image install at /opt/fiji.
#
# The image is immutable under Apptainer, so the ImageJ updater cannot modify
# this install; automatic update checks are disabled below. Users who need
# extra plugins or update sites can unpack their own Fiji into $HOME (which
# persists across sessions) and run it from there instead.
for exe in /opt/fiji/fiji-linux-x64 /opt/fiji/ImageJ-linux64 /opt/fiji/fiji; do
if [ -x "$exe" ]; then
exec "$exe" -Dimagej.updater.disableAutocheck=true "$@"
fi
done
echo "ERROR: no Fiji launcher found in /opt/fiji" >&2
exit 1
9 changes: 9 additions & 0 deletions desktop/fiji.desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[Desktop Entry]
Type=Application
Name=Fiji
Comment=Fiji is just ImageJ - image processing and analysis
Exec=fiji %F
Icon=/opt/fiji/images/icon.png
Terminal=false
Categories=Science;ImageProcessing;Graphics;
StartupNotify=true
37 changes: 37 additions & 0 deletions desktop/runnables.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Remote Desktop
description: Run a full Linux desktop (XFCE) with Fiji preinstalled on a cluster node, via Apptainer.
requirements:
- apptainer

runnables:
- id: serve
name: Desktop
type: service
description: >-
Start an XFCE desktop with Fiji preinstalled and open it in your
browser. Your home directory is mounted and writable. Runs until
you stop it.
# Fileglancer publishes the service URL once the port is live (auto_url)
# and embeds the token it minted ($FG_SERVICE_TOKEN) via
# service_url_suffix, so "Open Service" connects in one click. Inside
# the container, websockify only tunnels WebSocket connections whose
# ?token= names the VNC unix socket in a private 0700 directory; see
# start-desktop.sh for the full security model.
auto_url: true
service_url_suffix: "/vnc.html?autoconnect=true&resize=remote&path=websockify%3Ftoken%3D${FG_SERVICE_TOKEN}"
container: docker://ghcr.io/janeliascicomp/fg-interactive-apps/desktop:1.0.0
command: /opt/fg-desktop/start-desktop.sh
parameters:
- name: Data Folder
type: directory
description: >-
Optional folder to show on the desktop. Must be within a
mounted file share; it is bind-mounted into the container
automatically. Your home directory is always available.
required: false
exists: true

resources:
cpus: 4
memory: "16 GB"
walltime: "08:00"
126 changes: 126 additions & 0 deletions desktop/start-desktop.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
#!/bin/bash
# Fileglancer Remote Desktop launcher: an XFCE session on a VNC server, served
# to the browser by noVNC/websockify on the single Fileglancer-provided port.
#
# Connection chain, and how each hop is protected on a shared compute node:
#
# browser --HTTP/WS, ?token=$FG_SERVICE_TOKEN--> websockify 0.0.0.0:$FG_SERVICE_PORT
# --unix socket named after the token, in a 0700 dir--> Xvnc (TCP disabled)
# --X11 display, xauth cookie required--> XFCE session, Fiji, ...
#
# - websockify's UnixDomainSocketDirectory token plugin only tunnels WebSocket
# connections whose ?token= matches a socket filename in the private
# directory, so network access requires the per-job 192-bit token.
# - Xvnc listens only on that unix socket (-rfbport -1 disables TCP). The 0700
# directory keeps other users on the node from reaching the VNC server at
# all, which is what makes -SecurityTypes None safe here.
# - The X display itself requires the xauth cookie (mode 0600), so other local
# users cannot attach through /tmp/.X11-unix either.

set -euo pipefail

: "${FG_SERVICE_PORT:?FG_SERVICE_PORT is not set - this app requires the Fileglancer service contract}"
: "${FG_SERVICE_TOKEN:?FG_SERVICE_TOKEN is not set - this app requires the Fileglancer service contract}"

DATA_DIR="${1:-}"
GEOMETRY="${FG_DESKTOP_GEOMETRY:-1920x1080}"

# Private per-session state (X auth cookie, VNC socket, XDG runtime). This must
# be on node-local disk - unix sockets are unreliable on NFS homes - and
# mktemp -d creates it mode 0700, which is what keeps other users out.
PRIV_DIR="$(mktemp -d /tmp/fg-desktop.XXXXXXXX)"
SOCK_DIR="$PRIV_DIR/sock"
XAUTH_FILE="$PRIV_DIR/Xauthority"
VNC_SOCKET="$SOCK_DIR/$FG_SERVICE_TOKEN"
export XDG_RUNTIME_DIR="$PRIV_DIR/run"
mkdir -m 700 "$SOCK_DIR" "$XDG_RUNTIME_DIR"

XVNC_PID=""
SESSION_PID=""
cleanup() {
if [ -n "$SESSION_PID" ]; then kill "$SESSION_PID" 2>/dev/null || true; fi
if [ -n "$XVNC_PID" ]; then kill "$XVNC_PID" 2>/dev/null || true; fi
rm -rf "$PRIV_DIR"
}
trap cleanup EXIT

# --- Xvnc --------------------------------------------------------------------
# Scan for a free X display (vncserver-style). Another user on the node can
# grab a display between our check and Xvnc's startup, so on failure move on
# to the next number.
DISPLAY_NUM=""
for n in $(seq 10 99); do
[ -e "/tmp/.X${n}-lock" ] && continue
[ -e "/tmp/.X11-unix/X${n}" ] && continue

# The cookie must exist before Xvnc starts: -auth is read at server startup.
rm -f "$XAUTH_FILE" && touch "$XAUTH_FILE" && chmod 600 "$XAUTH_FILE"
xauth -f "$XAUTH_FILE" add ":${n}" MIT-MAGIC-COOKIE-1 "$(mcookie)" 2>/dev/null

Xvnc ":${n}" \
-rfbunixpath "$VNC_SOCKET" -rfbunixmode 0600 \
-rfbport -1 \
-SecurityTypes None \
-auth "$XAUTH_FILE" \
-geometry "$GEOMETRY" -depth 24 \
-desktop "Fileglancer Desktop" &
XVNC_PID=$!

# Up when the VNC unix socket appears; dead (lost the display race) if Xvnc exits.
for _ in $(seq 1 50); do
[ -S "$VNC_SOCKET" ] && break
kill -0 "$XVNC_PID" 2>/dev/null || break
sleep 0.2
done
if [ -S "$VNC_SOCKET" ]; then
DISPLAY_NUM="$n"
break
fi
kill "$XVNC_PID" 2>/dev/null || true
wait "$XVNC_PID" 2>/dev/null || true
XVNC_PID=""
rm -f "$VNC_SOCKET"
done

if [ -z "$DISPLAY_NUM" ]; then
echo "ERROR: could not start Xvnc on any display (:10-:99)" >&2
exit 1
fi

export DISPLAY=":${DISPLAY_NUM}"
export XAUTHORITY="$XAUTH_FILE"
echo "Xvnc up on display ${DISPLAY} (unix socket only, no TCP)" >&2

# --- Desktop conveniences ------------------------------------------------
# $HOME is the user's real (bind-mounted) home, so these persist across sessions.
mkdir -p "$HOME/Desktop"
if [ ! -e "$HOME/Desktop/fiji.desktop" ]; then
cp /usr/share/applications/fiji.desktop "$HOME/Desktop/" 2>/dev/null || true
chmod +x "$HOME/Desktop/fiji.desktop" 2>/dev/null || true
fi

# Link the requested data folder onto the desktop (Fileglancer has already
# bind-mounted it into the container).
if [ -n "$DATA_DIR" ]; then
link_name="$HOME/Desktop/$(basename "$DATA_DIR")"
if [ ! -e "$link_name" ] || [ -L "$link_name" ]; then
ln -sfn "$DATA_DIR" "$link_name"
fi
fi

# --- XFCE session --------------------------------------------------------
dbus-launch --exit-with-session startxfce4 &
SESSION_PID=$!
echo "XFCE session started (pid ${SESSION_PID})" >&2

# --- websockify / noVNC ----------------------------------------------------
# Serves the noVNC client and tunnels token-authenticated WebSocket
# connections to the VNC unix socket. Runs in the foreground: stopping the
# job (or websockify dying) tears down the whole session via the EXIT trap.
echo "Serving noVNC on port ${FG_SERVICE_PORT}" >&2
websockify \
--web /opt/novnc \
--token-plugin UnixDomainSocketDirectory \
--token-source "$SOCK_DIR" \
--heartbeat 30 \
"0.0.0.0:${FG_SERVICE_PORT}"
Loading