Bulk-provision Hetzner Cloud VMs, Cloudflare DNS records, and Docker on each host (n8n, Caddy, external task runners) from a CSV of students. Each student gets https://<student_id>.n8n.irn.hk/ (zone root irn.hk; override with DOMAIN_SUFFIX if you fork this).
Successful runs append rows to provisioned.csv (or a path you set). That file is the source of truth for deprovision (delete server + remove DNS).
| Tool | Role |
|---|---|
hcloud |
Hetzner Cloud CLI — create/delete servers |
curl, python3, openssl |
Cloudflare API, templating, secrets |
ssh, scp |
Post-boot install on each VM |
cp secrets.env.example secrets.env
cp students.csv.example students.csvEdit students.csv: keep the header student_id,first_name,last_name, one student per line. student_id drives the hostname and DNS label.
- In the console, create a project (e.g. one per cohort).
- Security → SSH keys: upload the public key you will use to SSH as
student@on new servers. The label in Hetzner must match what the script passes tohcloud server create(defaulthetzner_macbook_pro_rudy). If yours differs, setHCLOUD_SSH_KEY_NAMEinsecrets.env. - Security → API tokens: create a token in that project and put it in
secrets.envasHCLOUD_TOKEN.
There is no project ID in this repository. The API token is scoped to a project, so that token alone decides where servers are created and deleted.
Local key file: the script embeds your public key in cloud-init from SSH_KEY_PATH (default ~/.ssh/hetzner_macbook_pro_rudy). That file’s .pub must exist when you run provision.
In secrets.env set:
CLOUDFLARE_ZONE_IDfor theirn.hkzoneCLOUDFLARE_API_TOKENwith permission to edit DNS for that zone
DNS records are created as A records for <student_id>.n8n under that zone (full name <student_id>.n8n.irn.hk with default suffix).
If you prefer not to store the token in secrets.env, run hcloud context create <name> once with a token from the right project. If HCLOUD_TOKEN is set in secrets.env, it overrides the active context for that script run. Check:
hcloud context activeDefaults (repo root): reads students.csv, secrets.env, appends to provisioned.csv.
./provision_students.shCohort folder (separate list and log, same secrets):
STUDENTS_CSV=AC_FT_26_12/students.csv \
PROVISIONED_CSV=AC_FT_26_12/provisioned.csv \
./provision_students.shOther overrides
| Variable | Meaning |
|---|---|
SECRETS_ENV |
Path to env file (default ./secrets.env) |
STUDENTS_CSV |
Input CSV (default ./students.csv) |
PROVISIONED_CSV |
Output/skip list (default ./provisioned.csv) |
SSH_KEY_PATH |
Private key used for ssh/scp (default ~/.ssh/hetzner_macbook_pro_rudy) |
DOMAIN_SUFFIX |
DNS suffix (default n8n.irn.hk) |
Any student_id already present in provisioned.csv is skipped so you can re-run safely after partial failures.
Deletes the Hetzner server named in each row and removes the matching Cloudflare A record. Uses the same secrets.env (and HCLOUD_TOKEN / context rules) as provision.
./deprovision_students.shWith an explicit list:
./deprovision_students.sh AC_FT_26_12/provisioned.csvEquivalent:
PROVISIONED_CSV=AC_FT_26_12/provisioned.csv ./deprovision_students.shsecrets.env.example documents optional variables: server type/region (HCLOUD_TYPE, HCLOUD_LOCATION), IPv4 toggle, HCLOUD_COHORT_LABEL, N8N_IMAGE_TAG, task-runner token, timezone, etc. Defaults for VM shape and image are set at the top of provision_students.sh.
n8n / runners version: if you change N8N_IMAGE_TAG, align n8n-task-runners.json with the upstream file for that release (see comments in provision_students.sh). On existing VMs after a tag bump: docker compose pull && docker compose up -d in the deployed directory.
server location disabled: setHCLOUD_LOCATIONinsecrets.env(e.g.nbg1,fsn1,ash) per Hetzner availability.- URLs not loading immediately: allow a minute or two for DNS and TLS after the first boot.
- SSH / cloud-init: ensure the Hetzner SSH key name matches
HCLOUD_SSH_KEY_NAMEand that${SSH_KEY_PATH}.pubexists locally.