Before using the scripts in this repository, set up a fresh MicroCloud host with a working LXD environment and a storage pool named zpool.
On the MicroCloud host, ensure the following are available:
lxclxdbashjqpython3curl
The scripts in this repository assume that:
- the host has an available storage pool named
zpool(can be changed in deploy-project.sh) - an uplink physical network named
UPLINK-NATalready exists (can be changed in deploy-project.sh) - the
defaultproject is available for image discovery
Create a physical uplink network for the host that will act as the external-facing network for OVN traffic. The network should be configured as a physical network on the host NIC that will carry your traffic upstream.
In practice, that means:
- pick the host physical interface that should connect to the external network
- create a physical network object named
UPLINK-NAT - set the network to use that interface
- configure DNS servers for the uplink
- configure a gateway and the correct IPv4 routes for the upstream network
- reserve the OVN IPv4 address range that the project networks should use for NAT and routing
The important idea is that UPLINK-NAT should represent a real physical uplink path, not an isolated bridge or a private virtual network.
A typical configuration pattern looks like this conceptually, using an uplink network of 172.31.232.0/21:
access_entitlements:
- can_edit
- can_delete
project: default
name: UPLINK-NAT
description: ''
type: physical
config:
dns.nameservers: 1.1.1.1,9.9.9.9
ipv4.gateway: 172.31.232.1/21
ipv4.routes: 172.31.233.0/24,172.31.234.0/24,172.31.235.0/24,172.31.236.0/24, 172.31.237.0/24, 172.31.238.0/24, 172.31.239.0/24, 172.31.232.128/25
ipv4.ovn.ranges: 172.31.232.2-172.31.232.127Use values that match your site network planning. The exact address block should be chosen to fit the external network that the host is attached to.
Create or confirm the storage pool named zpool before running the deployment scripts.
This repository expects the root disk for profiles and instances to come from that storage pool.
Once the host is ready, verify that:
lxc network show UPLINK-NATsucceedslxc storage show zpoolsucceedslxc image list --project defaultreturns images that can be used for instance creation
These scripts are intended for a project-based workflow on a MicroCloud host.
deploy-project.shcreates a project, network, and Linux/Windows profiles.create-instance.shcreates instances inside the selected project using the chosen profile and image set.resize-instance.shresizes an existing instance in the selected project.delete-instance.shremoves a single instance.delete-project.shdeletes all profiles in a project, removes the network, and destroys the project.backup/backup-instances.shexports every instance running on the local cluster member to NFS storage. Intended to run frommicrocloud-backup.timeron every node.backup/restore-instance.shrestores an instance from a backup written bybackup-instances.sh.dns/technitium-dns.shis a shared helper, sourced (not run directly) bycreate-instance.sh,delete-instance.sh,delete-project.sh, andsync-dns-records.shto manage DNS records in Technitium.dns/sync-dns-records.shaudits every instance across the whole cluster and creates or corrects any missing/stale Technitium DNS record.
.env stays at the repository root and is shared by every script, including those in dns/ and backup/.
All of the scripts now accept command-line arguments and keep the original prompt-driven flow as an interactive fallback when you omit the matching arguments.
Create a new project, OVN network, and both profiles.
Examples:
./deploy-project.sh --project-name demo --project-id 42If you omit the options, the script will prompt for them in the original interactive way.
Create a new instance inside an existing project.
Examples:
./create-instance.sh --project-id 42 --environment d --service-code tstng --profile-type linux --image-alias ubuntu2604 --cpu 1 --ram 2 --disk 20 --description-suffix 'site-a'Supported arguments:
--project-idselects the target project by numeric project ID.--environmentusesp,t,q, ord. (Prod, Test, QA, Dev)--service-codemust be exactly five alphanumeric characters.--profile-typeislinuxorwin.--cpu,--ram, and--diskoverride the profile defaults.--image-indexselects the image from the numbered filtered list.--image-aliascan be supplied instead of--image-indexwhen you know the exact alias.--description-suffixappends an optional text suffix to the instance description.
If you omit any of these positional choices, the script will prompt for the missing values. The description suffix is the one exception: it's only prompted for when the script is run with no arguments at all. As soon as any argument is passed, an omitted --description-suffix is treated as empty rather than prompted for, since it's a genuinely optional field and a scripted invocation shouldn't block on stdin for it.
If Technitium is configured in .env (see DNS registration), the script also registers <instance-name>.<zone> pointing at the instance's forward IP.
Resize an existing instance in a project.
Examples:
./resize-instance.sh --project-id 42 --instance-index 1 --cpu 4 --ram 8 --boot-disk 60 --yesSupported arguments:
--project-idselects the project by numeric project ID.--instance-indexselects the instance from the numbered list shown by the script.--cpu,--ram, and--boot-diskset the new values.--yesskips the final confirmation prompt.
If you omit the selection or resource values, the script will prompt for them interactively.
Delete a single instance safely.
Examples:
./delete-instance.sh --project-id 42 --instance-name p42-tstng-ct01 --yesSupported arguments:
--project-idselects the source project by numeric project ID.--instance-indexselects the instance from the numbered list.--instance-namedeletes the instance directly by exact LXD instance name.--yesskips the final confirmation prompt.
If Technitium is configured in .env, the script also removes the instance's <instance-name>.<zone> record.
Delete all profiles, remove the OVN network, and remove an entire project.
Examples:
./delete-project.sh --project-id 42
./delete-project.sh --project-id 42 --delete-instancesSupported arguments:
--project-idselects the project by numeric project ID.--delete-instancesstops and deletes every instance in the project before the project itself is removed.--yesskips the confirmation prompt for the instance cleanup. Probably shouldn't use this.
When --delete-instances is used and Technitium is configured in .env, each deleted instance's DNS record is removed alongside its network forward.
A normal delete run will refuse to proceed if the project still contains instances unless you pass --delete-instances.
Audit every instance in every project (across the whole cluster, not just the local node) and create or correct its Technitium A record.
Examples:
./dns/sync-dns-records.sh --dry-run
./dns/sync-dns-records.shSupported arguments:
--dry-runreports what would be created or corrected without writing to Technitium.
Instances without a stored forward IP are skipped. Existing records that already match are left alone. Use this to backfill DNS for instances created before this feature existed, or to recover after a Technitium outage caused a create-instance.sh registration to fail.
Export instances to the NFS backup directory, then prune backups on a tiered retention schedule (see Backup layout and retention). By default it covers the instances on the cluster member the lxc client talks to (the local node when run on a host). From outside the cluster, for example the MicroCloud Vault VM, it can back up every member through the LXD API.
Examples:
./backup/backup-instances.sh
./backup/backup-instances.sh --retention-days 14
./backup/backup-instances.sh --dry-run
./backup/backup-instances.sh --project p42-testing --instance p42-tstng-ct01 --tag adhoc --description 'before upgrade' --no-prune
./backup/backup-instances.sh --all-members # every instance in the cluster
./backup/backup-instances.sh --member mc-node2 --dry-run # only instances on mc-node2Supported arguments:
--retention-daysoverridesBACKUP_RETENTION_DAYSfrom.envfor this run (length of the daily tier).--weekly-weeksoverridesBACKUP_WEEKLY_RETENTION_WEEKS(number of weekly buckets after the daily tier).--retention-monthsoverridesBACKUP_RETENTION_MONTHS(total age, in months, after which a backup is deleted outright).--all-membersbacks up instances on every cluster member, not just the one the client talks to.--memberbacks up only instances located on the named member (can't be combined with--all-members).--projectlimits the run to one project.--instancelimits the run to one instance, which must be within the member scope above. The run fails if it isn't found.--tagappends a tag to the file name (<instance>_<timestamp>_<tag>.tar.gz), e.g.adhocfor manual backups.--descriptionstores a free-text note in the backup's.jsonmetadata sidecar.--requested-byrecords who asked for the run, in the sidecar and the run record.--no-pruneskips retention pruning for this run (backups and run records).--dry-runprints what would be backed up and pruned without doing it.
This script takes no interactive input and is meant to run unattended from microcloud-backup.timer. See Backups below for setup.
Restore an instance from a backup written by backup-instances.sh.
Examples:
./backup/restore-instance.sh --project-id 42 --instance-name p42-tstng-ct01
./backup/restore-instance.sh --project-id 42 --instance-name p42-tstng-ct01 --backup-index 1 --new-name p42-tstng-ct02 --yesSupported arguments:
--project-idselects the project the backup belongs to, by numeric project ID.--instance-nameis the original instance name; used to locate its backups.--backup-indexselects a backup from the numbered list (newest first).--backup-filerestores an exact tarball path instead of browsing.--new-namerestores under a different instance name (default: original name).--yesskips the confirmation prompt.
If you omit the project, instance, or backup selection, the script will prompt for them interactively. The script refuses to overwrite an existing instance with the same name - use --new-name or delete the existing instance first. The restored instance is imported stopped; start it manually once you've verified it. If the original instance had a network forward, recreate it with lxc network forward create after the restore.
- Run
deploy-project.shwith--project-nameand--project-id, or let it prompt if you omit them. - The script creates:
- the project
- the OVN network for the project
- the Linux profile
- the Windows profile
- The Linux profile uses the cloud-init payload from
cloud-init-user-data.yaml. - The Windows profile is created without cloud-init and uses a larger boot disk size.
- Run
create-instance.shwith the required argument set, or let it prompt for the missing values. - The script selects the project, environment, and service code.
- It chooses either the Linux or Windows profile.
- It applies the chosen CPU, RAM, and disk values.
- It selects an image from the filtered list shown for the chosen profile family.
- The script creates the instance, allocates a forward IP, and stores the created description text.
- Run
resize-instance.shand let it prompt interactively. - The existing description is preserved as-is.
- If the new boot disk is larger while the instance is running, the script will stop and restart it automatically.
- Run
delete-project.shwith--project-id, or let it prompt if you omit it. - The script removes every profile in the project first.
- It then removes the project network.
- Finally, it deletes the project itself.
create-instance.sh and delete-instance.sh/delete-project.sh can register and remove an A record in Technitium DNS for each instance, pointing <instance-name>.<zone> (e.g. p42-tstng-ct01.infnet) at the instance's forward IP - its external, NAT'd address, not its internal OVN address. This is entirely driven by technitium-dns.sh, a small shared helper sourced by all three scripts.
DNS registration is best-effort and non-blocking: if Technitium is unreachable or misconfigured, the affected script prints a warning to stderr and continues rather than failing the deployment or decom.
In the Technitium web console, go to Administration > Sessions > Create Token, and create a token for a user with permission to manage the zone in question. Unlike a login session, a token doesn't expire.
Set these values in .env on the host(s) that run create-instance.sh, delete-instance.sh, and delete-project.sh:
TECHNITIUM_URL='http://dns.example.infnet:5380'
TECHNITIUM_API_TOKEN='...'
TECHNITIUM_ZONE='infnet'
TECHNITIUM_DNS_TTL='3600'
The infnet zone must already exist in Technitium; the scripts only add and remove records within it, they don't create the zone itself.
If TECHNITIUM_URL, TECHNITIUM_API_TOKEN, or TECHNITIUM_ZONE is left blank, DNS registration is skipped entirely (with a warning) and the scripts behave exactly as they did before this feature existed.
create-instance.shregisters<instance-name>.<zone>-> the instance's forward IP right after the network forward is created.delete-instance.shanddelete-project.sh --delete-instancesremove that same record when they delete the instance's network forward.- Record management uses the instance's LXD name as the DNS hostname; renaming an instance in LXD does not update DNS.
sync-dns-records.shbackfills or corrects records for instances that predate this feature, or whose registration failed at creation time (e.g. Technitium was unreachable). Seesync-dns-records.shabove.
backup-instances.sh and restore-instance.sh back instances up to a shared NFS export and restore them from it. Every node in the cluster runs the same script on its own timer; each node only backs up the instances currently located on itself, so the full cluster's instances are covered without any single node having to reach across to the others.
On the NFS server, export a directory that all cluster nodes can reach. On each MicroCloud node (nfs-common/nfs-utils is already installed by post-install.sh), mount that export at the same path used by NFS_BACKUP_DIR in .env, for example:
# /etc/fstab, on every node
nfs-server.example.com:/export/microcloud-backups /mnt/microcloud-backups nfs defaults,_netdev 0 0
mkdir -p /mnt/microcloud-backups
mount /mnt/microcloud-backupsbackup-instances.sh refuses to run if NFS_BACKUP_DIR isn't an actual mount point, so it won't silently fill up the local root disk if the NFS mount is down.
Set (or confirm) these values in .env on each node - they should match on every node in the cluster:
NFS_BACKUP_DIR='/mnt/microcloud-backups'
BACKUP_RETENTION_DAYS='7'
BACKUP_WEEKLY_RETENTION_WEEKS='3'
BACKUP_RETENTION_MONTHS='6'
Deploy this repository (including .env) to the same path on every node, e.g. /opt/microcloud-maintenance. Then install the timer unit:
cp backup/systemd/microcloud-backup.service backup/systemd/microcloud-backup.timer /etc/systemd/system/
systemctl daemon-reload
systemctl enable --now microcloud-backup.timerThe shipped units assume the repository lives at /opt/microcloud-maintenance and that NFS_BACKUP_DIR is /mnt/microcloud-backups. If either differs on your site, update ExecStart/WorkingDirectory in microcloud-backup.service and ConditionPathIsMountPoint to match before copying them in.
By default the timer runs nightly at 02:00 with up to a 20 minute random delay (RandomizedDelaySec), so all cluster nodes don't hit the NFS server at the exact same moment. Check a node's recent runs with:
systemctl status microcloud-backup.timer
journalctl -u microcloud-backup.serviceBackups are written as:
${NFS_BACKUP_DIR}/<project>/<instance>/<instance>_<timestamp>[_<tag>].tar.gz
${NFS_BACKUP_DIR}/<project>/<instance>/<instance>_<timestamp>[_<tag>].tar.gz.json
Exports are written as <file>.tar.gz.partial and renamed only once lxc export succeeds, so an interrupted export never looks like a restorable backup. A failed export is logged and the run moves on to the next instance. The script then exits non-zero and lists the failures in its run record.
The .json sidecar records:
Every run that isn't a dry run also writes a run record to ${NFS_BACKUP_DIR}/.runs/<UTC timestamp>_<runner>_<pid>.json, whether it succeeds or fails. The record holds the scope, the result, the files written, any per-instance failures, and what was pruned. Because it lives on the shared NFS export, the history covers every node and the Vault VM, which journald can't do. Run records older than BACKUP_RUN_HISTORY_DAYS (default 90) are pruned along with backups.
Tagged (e.g. ad-hoc) backups follow the same retention as scheduled ones.
Each run prunes that instance's directory on a tiered (grandfather-father-son) schedule, driven by three .env settings:
| Tier | Setting (default) | Behavior |
|---|---|---|
| Daily | BACKUP_RETENTION_DAYS (7) |
Every backup younger than this many days is kept. |
| Weekly | BACKUP_WEEKLY_RETENTION_WEEKS (3) |
For this many weeks after the daily tier, only the newest backup in each 7-day bucket survives. |
| Monthly | BACKUP_RETENTION_MONTHS (6) |
Beyond the daily+weekly window, only the newest backup in each ~30-day bucket survives, up to this many months of total age from today. Anything older is deleted outright. |
With the defaults, a backup's total lifespan looks like:
0-7d every backup kept (daily)
8-28d newest per 7-day bucket kept (weekly, 3 buckets)
29-180d newest per ~30-day bucket kept (monthly, ~5 buckets)
>180d deleted
Bucket boundaries are rolling day-counts from the moment each run starts, not calendar weeks/months. Buckets are computed independently per instance, so an instance with only occasional backups just keeps whichever ones it has; nothing is deleted to "fill" a schedule that was never met. Each run's retention_days/weekly_weeks/retention_months are recorded in its run record for auditing.
Each run also deletes files in that instance's directory outside this window. Backups are point-in-time exports of the instance's storage volume via lxc export --optimized-storage, taken without stopping the instance first - treat them as crash-consistent, not necessarily transaction-consistent for things like databases.
Run backup/restore-instance.sh on any node - it doesn't need to be the node the backup was taken on. See restore-instance.sh above for usage.
| Variable | Default | Purpose |
|---|---|---|
BACKUP_RUN_HISTORY_DAYS |
90 |
How long run records in .runs/ are kept |
BACKUP_RUNNER_NAME |
short hostname | Name recorded as runner in sidecars and run records |
LXD_CONF (exported) |
lxc default | Client config dir, e.g. for the Vault VM's dedicated cluster remote |
MicroCloud Vault deploys a small VM that holds a trusted lxc client certificate and mounts the same NFS export. It calls backup-instances.sh --all-members for ad-hoc backups and restore-instance.sh for restores. You can keep the per-node microcloud-backup.timer (the default), or let the VM run the nightly backup for the whole cluster instead. If you switch to the VM, disable the timer on every node.
- The Linux profile is intended for cloud-init based image deployment.
- The Windows profile is intended for Windows-capable images and uses a
64GiBroot disk. - The image picker filters the list based on the chosen profile family so that Linux selections exclude names containing
win, and Windows selections only show images whose names includewin. - The following can be used to set image aliases:
lxc image list local: -c LFd
lxc image alias create NAME FINGERPRINT --project default
{ "project": "p42-testing", "instance": "p42-tstng-ct01", "member": "mc-node1", // where the instance was located "runner": "microcloud-vault", // host that ran the backup (BACKUP_RUNNER_NAME or hostname) "run_id": "20260926-174851_microcloud-vault_5144", "tag": "adhoc", "description": "before upgrade", "requested_by": "admin", "created_at": "2026-09-26T17:48:55Z", "size_bytes": 1652555776 }