XNAT deployment on k3s with NFS-backed storage for the Australian Imaging Service.
- Ubuntu 20.04+ or similar Linux distribution
- Minimum 4GB RAM, 2 CPU cores
- 600GB+ storage for NFS server
- Helm 3.x installed
ais-devstack/
├── README.md # This file
├── manifests/
│ ├── pv.yaml # Persistent Volumes (NFS-backed)
│ ├── pvc.yaml # Persistent Volume Claims
│ ├── configmap.yaml # XNAT init script ConfigMap
│ ├── kustomization.yaml # Kustomize patches for StatefulSet
│ ├── kustomize.sh # Helm post-renderer script
│ └── values.yaml # XNAT Helm chart values (domain config)
├── nfs-server/
│ └── values.yaml # NFS server Helm chart values
├── plugins/
│ └── container-service-*.jar # XNAT plugins (auto-copied during install)
├── jupyterhub/ # JupyterHub integration (git subtree)
│ ├── INSTALL.sh # JupyterHub orchestrator
│ ├── 5-jupyterhub-values.yaml.template # JupyterHub config template
│ └── ... # See jupyterhub/README.md
└── scripts/
├── install.sh # XNAT install (prompts for JupyterHub)
├── install-jupyterhub.sh # JupyterHub installation
├── uninstall.sh # XNAT uninstallation
└── uninstall-jupyterhub.sh # JupyterHub uninstallation
cd /home/ubuntu/ais-devstack
chmod +x scripts/*.sh manifests/kustomize.sh
./scripts/install.shThe uninstall script automatically detects whether you're running MicroK8s or k3s:
./scripts/uninstall.shIt will:
- Remove XNAT and related resources
- Optionally remove NFS server and CSI driver
- Optionally remove the entire Kubernetes distribution (MicroK8s or k3s)
If you prefer step-by-step installation:
curl -sfL https://get.k3s.io | sh -
# Configure kubectl
mkdir -p ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown $USER:$USER ~/.kube/configcurl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bashk3s comes with Traefik by default, but XNAT requires nginx-specific annotations for large file uploads:
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
helm install ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx \
--create-namespace \
--set controller.publishService.enabled=truehelm repo add csi-driver-nfs https://raw.githubusercontent.com/kubernetes-csi/csi-driver-nfs/master/charts
helm install csi-driver-nfs csi-driver-nfs/csi-driver-nfs \
--namespace kube-system \
--set kubeletDir=/var/lib/kubeletkubectl create namespace storage
# Deploy using the nfs-server helm chart
helm install nfs-server ./nfs-server \
--namespace storage \
--values nfs-server/values.yaml
# Wait for pod to be ready
kubectl -n storage get pods -w
# Create required directories
kubectl -n storage exec deploy/nfs-server -- mkdir -p \
/exports/gpfs /exports/xnat/data/build /exports/xnat/plugins# Create namespace
kubectl create namespace ais-xnat
# Apply storage resources
kubectl apply -f manifests/pv.yaml
kubectl apply -f manifests/pvc.yaml
kubectl apply -f manifests/configmap.yaml
# Add AIS Helm repo
helm repo add ais https://australian-imaging-service.github.io/charts
helm repo update
# Install XNAT with kustomize patches
chmod +x manifests/kustomize.sh
helm install xnat-web ais/xnat \
--namespace ais-xnat \
--values manifests/values.yaml \
--post-renderer ./manifests/kustomize.sh# Watch pods start up
kubectl -n ais-xnat get pods -w
# Port forward to access locally
kubectl -n ais-xnat port-forward svc/xnat-web 8080:80Access XNAT at http://localhost:8080 (default: admin/admin)
| Component | MicroK8s | k3s |
|---|---|---|
| Kubelet path | /var/snap/microk8s/common/var/lib/kubelet |
/var/lib/kubelet |
| Default storage class | microk8s-hostpath |
local-path |
| Default ingress | nginx (addon) | Traefik (requires nginx install) |
| kubectl | microk8s kubectl |
kubectl |
| helm | microk8s helm |
helm |
| Config location | Snap-based | /etc/rancher/k3s/ |
Edit manifests/values.yaml to change the ingress hostname:
xnat-web:
ingress:
hosts:
- host: your-domain.example.comBoth XNAT and JupyterHub use OIDC for authentication. Choose one provider and configure both services to use it.
Supported Providers:
- Google OIDC - Recommended for development and testing (easy setup, works worldwide)
- AAF - Australian Access Federation (for Australian research institutions)
Important: Both XNAT and JupyterHub must use the same OIDC provider. Users need to exist in both systems with matching usernames.
Step 1: Create Google OAuth Credentials
-
Go to Google Cloud Console
-
Create a new project or select an existing one
-
Navigate to APIs & Services > Credentials
-
Click Create Credentials > OAuth client ID
-
Select Web application
-
Configure the OAuth client:
Field Value Name XNAT + JupyterHub Authorized JavaScript origins https://your-domain.example.comAuthorized redirect URIs https://your-domain.example.com/openid-loginhttps://your-domain.example.com/hub/oauth_callback -
Click Create and note down the Client ID and Client Secret
Step 2: Configure XNAT for Google
Update manifests/values.yaml:
xnat-web:
plugins:
openid-auth-plugin:
- name: "Google Authentication"
provider:
id: google
enabled: "google"
siteUrl: "https://your-domain.example.com"
openid:
google:
clientId: "your-google-client-id.apps.googleusercontent.com"
clientSecret: "your-google-client-secret"
# Other settings already configured in templateStep 3: Configure JupyterHub for Google
Update jupyterhub/5-jupyterhub-values.yaml:
hub:
config:
GenericOAuthenticator:
client_id: "your-google-client-id.apps.googleusercontent.com"
client_secret: "your-google-client-secret"
oauth_callback_url: "https://your-domain.example.com/hub/oauth_callback"
# Google URLs already configured in template
extraEnv:
OIDC_PROVIDER_PREFIX: "google" # Ensures username format matches XNATStep 1: Register with AAF
-
Go to AAF Service Manager (production) or Test AAF (testing)
-
Register two separate services:
Service Callback URL XNAT https://your-domain.example.com/openid-loginJupyterHub https://your-domain.example.com/hub/oauth_callback -
Note down the Client ID and Client Secret for each service
Step 2: Configure XNAT for AAF
Update manifests/values.yaml - comment out Google, uncomment AAF:
xnat-web:
plugins:
openid-auth-plugin:
- name: "AAF Authentication"
provider:
id: aaf
enabled: "aaf"
siteUrl: "https://your-domain.example.com"
openid:
# google: ... (comment out)
aaf:
accessTokenUri: https://central.aaf.edu.au/providers/op/token
userAuthUri: https://central.aaf.edu.au/providers/op/authorize
clientId: "your-aaf-xnat-client-id"
clientSecret: "your-aaf-xnat-client-secret"
scopes: "openid,profile,email"
# Other settings already configured in templateStep 3: Configure JupyterHub for AAF
Update jupyterhub/5-jupyterhub-values.yaml - comment out Google, uncomment AAF:
hub:
config:
GenericOAuthenticator:
# Google settings (comment out)
# client_id: ...
# AAF settings (uncomment)
client_id: "your-aaf-jupyterhub-client-id"
client_secret: "your-aaf-jupyterhub-client-secret"
oauth_callback_url: "https://your-domain.example.com/hub/oauth_callback"
authorize_url: "https://central.aaf.edu.au/providers/op/authorize"
token_url: "https://central.aaf.edu.au/providers/op/token"
userdata_url: "https://central.aaf.edu.au/providers/op/userinfo"
login_service: "AAF"
scope: [openid, profile, email, eduperson_principal_name]
extraEnv:
OIDC_PROVIDER_PREFIX: "aaf" # Ensures username format matches XNATAfter updating the configuration files:
# Upgrade XNAT
helm upgrade xnat-web ais/xnat \
--namespace ais-xnat \
--values manifests/values.yaml \
--post-renderer ./manifests/kustomize.sh
# Upgrade JupyterHub
helm upgrade jupyterhub jupyterhub/jupyterhub -n jupyter \
--values jupyterhub/5-jupyterhub-values.yamlEdit nfs-server/values.yaml:
persistence:
size: 600Gi # Adjust as neededkubectl -n ais-xnat describe pod xnat-web-0
kubectl -n ais-xnat logs xnat-web-0 -c home-init# Check NFS server is running
kubectl -n storage get pods
kubectl -n storage logs deploy/nfs-server
# Check PV/PVC binding
kubectl get pv
kubectl -n ais-xnat get pvckubectl -n ais-xnat logs xnat-web-0-postgresql-0Plugins in the plugins/ directory are automatically copied to the NFS server during installation.
Pre-installed plugins:
container-service-3.7.2-uq-fat.jar- Container service plugin
To add more plugins:
- Place JAR files in
plugins/before running install, OR - Copy manually after installation:
# Copy plugin jar to NFS server
kubectl -n storage cp my-plugin.jar \
$(kubectl -n storage get pods -l app=nfs-server -o name | cut -d/ -f2):/exports/xnat/plugins/
# Restart XNAT to load new plugins
kubectl -n ais-xnat rollout restart statefulset xnat-webJupyterHub provides interactive Jupyter notebooks integrated with XNAT. The jupyterhub/ directory contains the JupyterHub deployment as a git subtree from ais-jupyterhub.
Option 1: During XNAT installation, answer "y" when prompted:
Install JupyterHub? (y/N): y
Option 2: Install separately after XNAT is running:
./scripts/install-jupyterhub.shThe install script automatically reads the domain from manifests/values.yaml and configures JupyterHub to use the same domain.
./scripts/uninstall-jupyterhub.shThe jupyterhub/ directory is a git subtree. To pull updates from the upstream ais-jupyterhub repository:
git subtree pull --prefix=jupyterhub \
https://github.com/Australian-Imaging-Service/ais-jupyterhub.git \
Development_AB --squashTo push local changes back to upstream (if you have write access):
git subtree push --prefix=jupyterhub \
https://github.com/Australian-Imaging-Service/ais-jupyterhub.git \
Development_ABSee the following files in jupyterhub/ for more details:
README.md- Architecture overviewXNAT-CONFIGURATION.md- XNAT plugin setupTROUBLESHOOTING.md- Common issues and solutions