Deployment¶
Host VisFEM on a Linux server:
- Landing page →
https://visfem.de/ - Web app →
https://app.visfem.de/
The setup is three pieces behind Caddy: a static landing page, the app in Docker, and Caddy routing by hostname and handling HTTPS.
Until the server is live, visfem.de is served by GitHub Pages: the documentation workflow publishes the interim landing page of deploy/pages/ at the root and this documentation below /docs/, see Documentation. Section 2 repoints the visfem.de DNS record from GitHub Pages to the server IP. Before that switch, move the custom domain of the GitHub Pages site to docs.visfem.de and set site_url in zensical.toml accordingly, so the documentation stays online.
1. Overview¶
┌──────────────────────────────────────────┐
Internet ─────────► │ Caddy (HTTPS + routing by hostname) │
└──┬───────────────────────┬────────────────┘
│ │
visfem.de app.visfem.de
│ │
┌─────────▼─────────┐ ┌──────────▼─────────────────┐
│ static landing │ │ Docker container "visfem" │
│ /var/www/landing │ │ (the VisFEM web app) │
└───────────────────┘ └──────────┬─────────────────┘
│ reads (read-only)
/opt/visfem/data/datasets/
- Landing page: static files (
deploy/landing/). Tiles link to the app with?model=<name>to open a specific model. - Web app: Docker container
visfem. Each visitor gets an isolated backend process; concurrency is set insetup/launcher.json(section 9). - Caddy: routes by hostname and manages Let's Encrypt certificates.
Requirements:
- Linux server (Ubuntu/Debian assumed) with a public IP and
sudo. - Control over the
visfem.deDNS records. - Ports 80 and 443 open (80 for the certificate challenge, 443 for HTTPS):
- The dataset files (meshes), which live on the lab NAS. Section 5 stages them.
2. DNS¶
Create two A records pointing at the server IP, one per hostname Caddy routes on:
| Type | Name | Value |
|---|---|---|
| A | visfem.de |
<server IP> |
| A | app.visfem.de |
<server IP> |
If visfem.de points at GitHub Pages, change it to the server IP. app.visfem.de is new. Add matching AAAA records if the server has IPv6.
Caddy issues certificates once both names resolve to the server. Verify:
Both print the server IP once DNS has propagated.
3. Server prerequisites¶
Docker + Compose¶
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER # grants $USER docker access; re-login to apply
Re-login (or newgrp docker), then confirm:
Caddy¶
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
| sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
| sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install -y caddy
Caddy runs as a systemd service and reads /etc/caddy/Caddyfile (written in section 7). Confirm with systemctl status caddy.
4. Code checkout¶
sudo mkdir -p /opt && cd /opt
sudo git clone https://github.com/matthiaskoenig/visfem
sudo chown -R $USER:$USER /opt/visfem
cd /opt/visfem
All later commands run from /opt/visfem.
5. Dataset files¶
Copy the dataset files from the lab NAS into the repo's data folder.
# Set the source to the NAS mount or host; trailing slashes copy folder contents into the target folder.
rsync -av <nas>:/path/to/data/datasets/ /opt/visfem/data/datasets/
Stage all datasets. The allowlist in compose.deploy.yml (section 6) selects the public subset.
6. Web app¶
This reads compose.deploy.yml and:
- builds the image
visfem:localfrom theDockerfile; - starts the container
visfemon127.0.0.1:8080, the loopback port Caddy proxies to; - mounts
./dataread-only and points the app at it viaDATA_DIR; - sets
TRAME_USE_HOST: "wss://app.visfem.de", the public address the browser opens its WebSocket to; - restricts the public app to the datasets listed in
VISFEM_DATASETS, a comma-separated allowlist of dataset keys (a key is a descriptor's.jsonfilename stem, e.g.heart). Unlisted datasets stay hidden.
Confirm it is up:
Change a setting by editing compose.deploy.yml and re-running docker compose -f compose.deploy.yml up -d.
7. Caddy configuration¶
Copy the landing page to Caddy's web root:
The landing page (deploy/landing/) ships with tiles that point at https://app.visfem.de/?model=....
Back up any existing Caddy config, then write the new one:
sudo cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.bak-$(date +%Y%m%d-%H%M%S) 2>/dev/null || true
sudo tee /etc/caddy/Caddyfile >/dev/null <<'EOF'
visfem.de {
root * /var/www/landing
file_server
}
app.visfem.de {
# the trame client reads its connection settings from the URL as well; a
# link must not point a visitor's browser at another session server
@session_override expression `{query.sessionURL} != "" || {query.sessionManagerURL} != "" || {query.secret} != ""`
respond @session_override "Bad Request" 400
header {
# the browser connects to this host only, the app is never framed
Content-Security-Policy "connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
Strict-Transport-Security "max-age=31536000"
X-Content-Type-Options "nosniff"
Referrer-Policy "strict-origin-when-cross-origin"
-Server
}
reverse_proxy 127.0.0.1:8080
}
EOF
This serves the static landing page on visfem.de and forwards app.visfem.de traffic to the app container. The app block rejects links which override the session settings of the trame client and sends the security headers; connect-src 'self' keeps the browser from opening a WebSocket to any other host. Validate and load it:
Caddy obtains and renews Let's Encrypt certificates for both names automatically, provided the section 2 DNS points at this server and ports 80/443 are open.
8. Verification¶
https://visfem.de/shows the landing page with dataset tiles.https://app.visfem.de/loads the app; selecting a dataset renders its mesh.- Clicking a landing-page tile opens the app with that model loaded.
The first request to each hostname may take a few seconds while Caddy fetches the certificate. Logs:
cd /opt/visfem && docker compose -f compose.deploy.yml logs -f # app
sudo journalctl -u caddy -f # Caddy
9. Operation¶
Run from /opt/visfem:
docker compose -f compose.deploy.yml up -d # start / apply config changes
docker compose -f compose.deploy.yml restart # restart
docker compose -f compose.deploy.yml logs -f # logs
docker compose -f compose.deploy.yml down # stop
docker compose -f compose.deploy.yml up -d --build # rebuild after code/config changes
restart: unless-stoppedbrings the container back after a reboot or crash.- Update the code with
git pull, then rebuild (docker compose -f compose.deploy.yml up -d --build). Staged data and the landing page stay in place. - Concurrency: set in
setup/launcher.json, fieldresources→port_range. Each visitor holds one port from the range; the port count is the session cap. The default[9001, 9006]is 6 sessions. Widening the range (e.g.[9001, 9020]) needs a rebuild:launcher.jsonis baked into the image."timeout": 60in the same file is how long the launcher waits for a new session to start. A session ends 30 seconds after its last client disconnected, set by--timeoutinsetup/apps.yml; a session which is requested but never connected holds its port for that long, too. Memory scales with sessions and dataset size; checkdocker stats visfemunder real use before widening. - Abuse: anybody can request sessions from the launcher without logging in, and 6 requests take every port for 30 seconds. Caddy has no rate limiting built in; if sessions run out without real visitors, rate limit
/paraviewwith the caddy-ratelimit module or put the site behind basic auth (Appendix A). Addingmem_limit,cpusandpids_limittocompose.deploy.ymlcaps what the container can take from the server.
Appendix A: private site during testing¶
Password-protect with Caddy basic auth. Generate a hash:
Wrap either site block in /etc/caddy/Caddyfile with basic_auth:
Reload Caddy (sudo systemctl reload caddy). Remove the block and reload to go public.
Appendix B: deployment files in the repo¶
Dockerfile: builds the app image from thekitware/trame:uvbase (Python 3.14) and installs OSMesa for software 3D rendering.compose.deploy.yml: the production Compose file (image name, port binding, data mount, and theDATA_DIR/TRAME_USE_HOST/VISFEM_DATASETSsettings).setup/launcher.json: the trame launcher config (concurrencyport_range, starttimeout), baked into the image at build.setup/apps.yml: the session command of the launcher,visfemwith the idle--timeout.setup/requirements.txt: the pinned dependencies of the image, exported fromuv.lock, see Deployment requirements.deploy/landing/: the static landing page.src/visfem/engine/discovery.py: readsDATA_DIRand theVISFEM_DATASETSallowlist.src/visfem/log.py: readsVISFEM_LOG_LEVEL(defaultWARNING), the level of the container log.src/visfem/app.pyandsrc/visfem/ui/layout.py: handle the?model=<key>URL parameter and on-demand mesh loading.