Run Interactive Services on TES¶
This page shows how to run interactive web services, such as RStudio Server, JupyterLab, or OHDSI Atlas with an OMOP CDM database, as tasks of a GA4GH TES service, and how to open them in your browser. A TES service can't publish the port of a task, so each task connects to a tunnel hub on a virtual machine that you run. The commands page has all commands without the explanations.
Under construction
The deployment offers no tunnel hub, so you need your own virtual machine that the tasks can reach over HTTPS. The scripts were tested with one TES service only, the TESK of ELIXIR-FI, and not with the TES services of other Nodes or with proTES (see How this page was tested). For help, write to cloud-service@elixir-europe.org.
Use synthetic data only
The TESK of ELIXIR-FI has no authentication. Anyone can read a task with
?view=FULL, including the passwords of the hub and of the database that
the scripts put into it. RStudio, JupyterLab and Atlas have no login.
Don't use real or sensitive data with this setup.
How it works¶
A TESK task can only make outbound HTTPS connections. Each task therefore
downloads chisel and connects over
HTTPS to a chisel server on your virtual machine, the hub. nginx on the
virtual machine accepts the connection on the public HTTPS port and passes
it to the hub, which listens on 127.0.0.1:8090 only. The port of the
service then appears as 127.0.0.1:<PORT> on the virtual machine, and you
forward it to your computer with SSH:
On the virtual machine <HOST>:
browser --ssh -L--> 127.0.0.1:8787 <-- hub 127.0.0.1:8090 <-- nginx :443 /tes-tunnel/
^
| websocket over HTTPS
In the TES task: |
RStudio 127.0.0.1:8787 <-- chisel client ------------------------+
- One hub user per service: each service gets its own user and password on the hub, and may open only its own ports. A password that someone reads from a task can't open other ports.
- The task checks the hub: the task gets the fingerprint of the hub's key, and refuses a server with another key.
- A keep-alive loop: a loop on the virtual machine sends a new task when the old one ends. If the old pod still holds the port, the loop first locks it out (see the next point).
- Stopping locks the old task out: on the TESK of ELIXIR-FI, a cancelled
task is marked
CANCELED, but its pod keeps running and its tunnel stays connected.downtherefore first gives the service a new password and restarts the hub, and then cancels the task. The other tunnels reconnect within about 30 seconds.
| File | What it does |
|---|---|
tes-hub/tes-hub.sh |
Runs the hub on the virtual machine. |
tes-hub/common.sh |
Runs first in every task. Downloads chisel and opens the tunnel. |
tes-hub/tes-service.sh |
Runs a service that fits into one container image, from a directory with a service.conf and a pod.sh. |
rstudio-tesk/service.conf, rstudio-tesk/pod.sh |
RStudio Server, with the image rocker/rstudio. |
omop-tesk/omop-tesk-fi.sh |
Runs the Atlas stack as three tasks, which reach each other through the hub. |
omop-tesk/pod/db.sh |
Postgres with the Eunomia GiBleed demo CDM. Registers the CDM as a data source of WebAPI. |
omop-tesk/pod/webapi.sh |
OHDSI WebAPI. |
omop-tesk/pod/atlas.sh |
The Atlas web interface. Forwards /WebAPI/ to the WebAPI task. |
Each script keeps what it generates in a folder .state next to it: the
passwords (readable only by you), the task IDs and the logs.
What you need¶
- A Linux virtual machine (x86-64 or ARM64) with
bash,curl,python3,ss,setsidandflock, that you can reach with SSH. The services run on the TES service, not on the virtual machine. - A public host name for the virtual machine (
<HOST>below), and port 443 open to the internet. The tasks must reach this port: the TESK of ELIXIR-FI allows outbound HTTPS, and other ports were not tested. - nginx and a TLS certificate for
<HOST>on the virtual machine, for example from Let's Encrypt with Certbot. The hub itself speaks plain HTTP. - A TES service whose tasks can make outbound HTTPS connections. The scripts
use the TESK of ELIXIR-FI (
https://csc-tesk-noauth.rahtiapp.fi). To use another service, setTES_URL.
Set up the hub¶
1. Install the scripts¶
Copy the scripts to the virtual machine, in the folders of the table above,
for example into ~/eoc_services. The commands
page has a block
that creates all files when you paste it. Then make the launchers
executable:
2. Start the hub¶
Replace <HOST> with the public host name of the virtual machine. The last
argument makes the hub listen on 127.0.0.1 only, so that it is reachable
through nginx only:
setup downloads chisel, creates the key of the hub and its list of users,
prints the configuration for nginx (step 3) and starts the hub on port
8090. The hub doesn't start again after a reboot of the virtual machine:
then run ./tes-hub/tes-hub.sh start.
3. Add the path to nginx¶
Add the block that setup printed to the HTTPS server block of <HOST>
(the one with listen 443 ssl), and reload nginx with
sudo systemctl reload nginx:
location /tes-tunnel/ {
proxy_pass http://127.0.0.1:8090/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 1d;
proxy_send_timeout 1d;
proxy_buffering off;
}
The long timeouts keep tunnels open while no one uses them. Don't open port
8090, or the ports of the services, in the firewall: they must stay on
127.0.0.1.
4. Check the hub¶
The output must contain proxy route OK. The hub answers requests without
a tunnel with Not found, so curl https://<HOST>/tes-tunnel/ must return
exactly this text.
Start RStudio Server¶
./tes-hub/tes-service.sh rstudio-tesk selftest # optional: a short task without tunnel; fails unless it prints SELFTEST_OK
./tes-hub/tes-service.sh rstudio-tesk up
up creates the hub user rstudio the first time, sends the task, starts
the keep-alive loop and waits until the tunnel is up, usually 1 to 3
minutes. It doesn't send a second task while one runs. On your computer,
forward the port, then open http://localhost:8787:
The task runs as a random user without root, so the image can't start
RStudio as usual. pod.sh starts RStudio Server directly, without login,
and gives R a home folder in /tmp/home.
Start Atlas with OMOP demo data¶
./omop-tesk/omop-tesk-fi.sh selftest db # optional: loads the CDM in a short task; fails unless it prints SELFTEST_OK
./omop-tesk/omop-tesk-fi.sh up
up creates the hub user omop and a password for the database the first
time, sends three tasks and starts the keep-alive loop:
| Task | Image | Port on the virtual machine |
|---|---|---|
db |
postgres:16-alpine, with the Eunomia GiBleed CDM 5.3 |
15432 |
webapi |
ohdsi/webapi:2.15.2. Reaches the database through the hub. |
18081 |
atlas |
ohdsi/atlas:2.15.0. Reaches WebAPI through the hub. |
8080 (HUB_ATLAS_PORT) |
When WebAPI runs, the db task creates the results schema and registers
the CDM as the data source EUNOMIA. The first start takes 10 to 15
minutes, because WebAPI creates its tables. up waits up to 40 minutes,
until Atlas answers and WebAPI lists the data source. All three tasks share
one hub user, so the keep-alive loop replaces the whole stack when the
database ends: a new database is empty.
Then forward the port and open http://localhost:8080/atlas/:
If another program uses port 8080 on the virtual machine, choose another
port before the first up, for example export HUB_ATLAS_PORT=8088, and
keep it set, for example in ~/.bashrc.
To check Atlas, create a cohort through WebAPI with the Python script on the commands page. It must report 479 persons.
Add your own service¶
tes-service.sh runs any web service that fits into one container image.
Create a folder next to tes-hub with two files:
service.conf:NAME(the hub user and the name of the task),IMAGE, andPORT(the port of the service in the task). Optional:HUB_PORT(the port on the virtual machine, by defaultPORT),RAM_GBandCPU_CORES(by default 1), andPOD_SHELL(by default/bin/sh). The task gets every otherKEY=valueas an environment variable.pod.sh: runs in the task aftercommon.sh. It starts the service on127.0.0.1:$PORT, callsstart_tunnel "R:127.0.0.1:${HUB_PORT}:127.0.0.1:${PORT}"and keeps running. WithSELFTEST=1, it checks the service and ends instead.
The commands
page
has an example for JupyterLab. Start it with
./tes-hub/tes-service.sh jupyter-tesk up and forward port 8888.
Manage the services¶
tes-service.sh and omop-tesk-fi.sh have the same commands:
| Command | What it does |
|---|---|
status |
Shows the state of the task, the tunnel and the keep-alive loop. |
logs |
Shows the logs of the task. The TESK of ELIXIR-FI fills them only when the task ends. |
print |
Shows the TES task that the script would send. |
restart |
Replaces the service with a new one (down, then up). |
down |
Stops the keep-alive loop, locks the old task out of the hub and cancels it. |
For example: ./tes-hub/tes-service.sh rstudio-tesk status or
./omop-tesk/omop-tesk-fi.sh logs webapi. ./tes-hub/tes-hub.sh status
shows the hub, the path in nginx and all tunnels.
After a reboot of the virtual machine, start the hub, then run up for each
service. Running tasks reconnect by themselves, and up starts the
keep-alive loop again.
Limits¶
- 1 GB of memory and no disk: on the TESK of ELIXIR-FI, tasks that asked
for more than 1 GB of memory or for any disk (
disk_gb) didn't start (October 2026). WebAPI therefore runs with a small Java heap (WEBAPI_JAVA_OPTS). - Nothing is kept: files, R packages and the database are in
/tmpof the task. They are lost when the task ends or is replaced. Copy out what you want to keep. - WebAPI 2 only: the WebAPI 3 images (
ohdsi/webapi:latestsince November 2026) start differently, andpod/webapi.shdoesn't support them yet. KeepWEBAPI_IMAGEat a 2.x version. - Outbound HTTPS only: tasks can download files, for example from GitHub, but can't use other ports, for example SSH.
- Old tasks keep running: locked-out tasks keep running and use resources of the TES service, until its operators delete them. When you are done, ask them to delete the tasks.
Settings¶
To change a default, set the environment variable before you run a script:
| Variable | Default | Used by |
|---|---|---|
TES_URL |
https://csc-tesk-noauth.rahtiapp.fi |
all scripts |
HUB_LISTEN_PORT |
8090 (setup saves it) |
tes-hub.sh |
CHISEL_VERSION |
1.12.0 |
tes-hub.sh |
WATCH_INTERVAL |
60 (seconds) |
the keep-alive loops |
HUB_ATLAS_PORT, HUB_WEBAPI_PORT, HUB_DB_PORT |
8080, 18081, 15432 |
omop-tesk-fi.sh |
DB_IMAGE, WEBAPI_IMAGE, ATLAS_IMAGE |
postgres:16-alpine, ohdsi/webapi:2.15.2, ohdsi/atlas:2.15.0 |
omop-tesk-fi.sh |
EUNOMIA_URL |
the GiBleed CDM 5.3 of EunomiaDatasets | omop-tesk-fi.sh |
WEBAPI_RAM_GB, WEBAPI_JAVA_OPTS |
1, -Xmx640m -XX:MaxMetaspaceSize=192m ... |
omop-tesk-fi.sh |
Troubleshooting¶
proxy route not working: the hub doesn't run (start it with./tes-hub/tes-hub.sh start), or the path is missing in nginx. Ifcurl https://<HOST>/tes-tunnel/returns HTML or a redirect, the request doesn't reach the hub.tunnel not connected, but the task isRUNNING: an old task holds the port. The hub logtes-hub/.state/hub.logthen showscannot listen on R:127.0.0.1:<PORT>. Runrestartfor the service.Login failed for user "<NAME>"in the hub log: a locked-out old task tries to reconnect. It does no harm, but uses resources of the TES service.- The task stays
QUEUED, orRUNNINGwithout output: the task asks for too many resources. KeepRAM_GB=1and don't ask for disk. - RStudio shows
Unable to connect to service: the R session can't start. Runselftestand read its output. - Atlas has no data source: WebAPI is still starting, or the
dbtask ended. Check with./omop-tesk/omop-tesk-fi.sh status. If the database ended while WebAPI runs, runrestart: a new database is empty.
How this page was tested¶
During the BioHackathon Europe 2026 (project 27), the project team ran the
scripts on one virtual machine with the TESK of ELIXIR-FI, behind the
reverse proxy of elixir-on.cloud. According to the notes of the project,
RStudio Server, JupyterLab, and Atlas with the Eunomia data worked, and a
cohort created through WebAPI had 479 persons.
This page fixes and changes the scripts of the project. Among others, the
script of the database task is named db.sh, rstudio-tesk/service.conf
is filled in, and the OMOP images are pinned to ohdsi/webapi:2.15.2 and
ohdsi/atlas:2.15.0, because ohdsi/webapi:latest (WebAPI 3.0.1) doesn't
start with pod/webapi.sh.
We tested the changed scripts on a second virtual machine, without TESK:
a small local TES service ran each task as a Docker container with a
random user ID and 1 GB of memory, and, like the TESK of ELIXIR-FI, kept a
container running when its task was cancelled. It also gave the random
user ID an entry in /etc/passwd, which RStudio Server needs. The hub ran
on the virtual machine, and the tasks connected to it over HTTP, without
nginx:
- Self-tests:
selftestpassed for RStudio,dbandatlas, and failed for RStudio without the entry in/etc/passwd. - Services: RStudio answered through the tunnel. The Atlas stack was ready within a few minutes, and a cohort created through WebAPI had 479 persons.
- Replacing and stopping: after a cancel, the keep-alive loop locked the
still-running container out and the new task got the port. After
down, no task was sent again, and all tunnels were closed.
We did not run the changed scripts on the TESK of ELIXIR-FI, and did not test nginx with TLS or JupyterLab.
Where to go next¶
- Interactive Services on TES: Commands
- chisel, TESK, and the TESK page for operators.
- OHDSI Atlas, WebAPI and the Rocker images.
- To send other tasks to a TES service: Use the Command Line.
- Questions: the Discussions or the About page.