Ansible playbooks for provisioning a JupyterHub instance on an EOSC EU Node (PSNC OpenStack) VM.
JupyterHub runs containerised via Docker Compose, with per-user datascience-notebook containers spawned by DockerSpawner, behind an nginx reverse proxy with Let's Encrypt TLS.
Before running the playbooks, the VM must be provisioned and networked on OpenStack. Two options:
- Manual: follow eosc-vm-setup.md step by step via the Horizon dashboard.
- Automated: run
provision_vm.yamlusing OpenStack's Mistral workflow service. Edit theinputdefaults at the top of the file (instance name, flavour, network names, keypair) then execute via the Mistral CLI or Horizon. The workflow handles network, subnet, router, instance launch, and floating IP association in sequence, and outputs the assigned floating IP for use ininventory/hosts.yml.
On your local machine:
- Ansible ≥ 2.14
community.dockercollection:ansible-galaxy collection install community.docker
On the EOSC VM:
- Ubuntu 24.04, floating IP assigned
- Security group with ingress on TCP 22, 80, and 443
- A domain name resolving to the floating IP (required for Let's Encrypt)
Edit inventory/hosts.yml and fill in:
| Key | Description |
|---|---|
ansible_host |
Floating IP of the VM |
server_domain |
Domain name resolving to the floating IP |
letsencrypt_email |
Contact email for Let's Encrypt |
Key variables (with defaults in group_vars/all.yml):
| Variable | Default | Description |
|---|---|---|
primary_user |
ubuntu |
SSH user on the VM |
jupyterhub_admin_user |
admin |
First JupyterHub admin account |
jupyterhub_notebook_image |
quay.io/jupyter/datascience-notebook:latest |
Image spawned per user |
jupyterhub_docker_network |
jupyterhub_network |
Docker network name |
jupyterhub_user_mem_limit |
8G |
Per-user notebook container memory limit (c.DockerSpawner.mem_limit) |
jupyterhub_user_cpu_limit |
4 |
Per-user notebook container CPU limit (c.DockerSpawner.cpu_limit) |
ansible-playbook playbooks/setup-server.ymlRoles applied in order:
| Role | What it does |
|---|---|
users |
Ensures ubuntu is in the docker group; optionally copies authorized_keys |
system |
Upgrades packages, installs Docker CE + Compose plugin |
security |
fail2ban SSH jail, disables root login and password auth |
nginx |
Installs nginx, obtains Let's Encrypt cert, configures HTTPS reverse proxy |
jupyterhub |
Builds hub image, deploys Docker Compose stack |
Run a subset of roles with --tags, e.g.:
ansible-playbook playbooks/setup-server.yml --tags nginx,jupyterhub- Go to
https://<server_domain>/hub/signup - Create the
adminaccount (or whateverjupyterhub_admin_useris set to) - Log in — the admin account is pre-authorised and does not require approval
- Approve other users at
/hub/authorize
After config or Dockerfile changes:
# Rebuild image and restart — preserves user accounts
ansible-playbook playbooks/redeploy-jupyterhub.yml
# Full wipe — resets user database (use if locked out)
ansible-playbook playbooks/redeploy-jupyterhub.yml -e wipe_state=trueansible.cfg Ansible defaults and SSH settings
inventory/hosts.yml Target host and variables (fill in before use)
group_vars/all.yml Variable defaults
playbooks/
setup-server.yml Full server provisioning
redeploy-jupyterhub.yml Rebuild and restart JupyterHub only
roles/
users/ SSH key deployment, docker group membership
system/ Package upgrades, Docker CE installation
security/ fail2ban, SSH hardening
nginx/ nginx + Let's Encrypt TLS
jupyterhub/ JupyterHub Docker Compose stack
provision_vm.yaml Mistral workflow for OpenStack VM provisioning
eosc-vm-setup.md Manual VM setup guide for EOSC/PSNC OpenStack
Internet → nginx (:443 TLS) → localhost:8000 → jupyterhub container
↓ DockerSpawner
per-user notebook containers
(on jupyterhub_network)
- Hub state (user DB, cookie secret) persists in Docker volume
jupyterhub_jupyterhub_data - Per-user work directories persist in volumes named
jupyterhub-user-<username> - The hub container has read-only access to
/var/run/docker.sockto spawn user containers
Change jupyterhub_notebook_image in inventory/hosts.yml and run:
ansible-playbook playbooks/redeploy-jupyterhub.ymlExisting user volumes are preserved; new image takes effect on next server spawn.