Skip to content
Documentation pages

Documentation

Templates

Where IsardVDI templates come from, and how to build your own from one YAML file with isard build.

Builds on Bastion.

Introduction

Every desktop you have made so far came from a template, and you picked it off a list somebody else wrote.

That list is your centre's, it is short, and it is old.

Sooner or later an exercise needs something that is not on it: a Fedora when the centre has only Ubuntu, a machine with Docker already installed so that thirty students do not each spend ten minutes installing it, a base image with your own SSH key baked in.

A template is not magic. It is a desktop somebody stopped and froze:

architecture-beta
     service iso(disk)[ISO]
     service desktop(server)[Desktop]
     service template(database)[Template]

     iso:R -- L:desktop
     desktop:R -- L:template

Three steps, and this page is those three:

  1. get an installer ISO into Isard — the media library;
  2. install an operating system onto a desktop from it;
  3. promote that desktop into a template.

Then it does them all for you in one command.

The media library

Isard calls an uploaded ISO a medium, and every account has a library of them.

$ isard media

-- Media --
NAME                             STATUS      SIZE   OWNER
Fedora-Server-netinst-x86_64-44  Downloaded  1049M  you
ubuntu-24.04.4-live-server       Downloaded  3052M  shared

shared means somebody at your centre published it for everybody. you means you added it and it counts against your quota.

Adding one is a URL, not a file:

$ isard media add ubuntu-24 --url https://releases.ubuntu.com/24.04/ubuntu-24.04.4-live-server-amd64.iso
✓ Media registered
✓ Media id: 2b8f61c4-...
✓ Downloaded: ubuntu-24

✓ Media 'ubuntu-24' ready (id: 2b8f61c4-...).

Isard fetches it, not you. There is no upload route at all — you give the platform an https:// address and its own downloader pulls the file.

Which is a good thing on a school connection, and a constraint the first time you have an ISO on a USB stick and nowhere to put it.

The command waits by default and prints progress. --no-wait returns immediately, and then isard media is how you check on it.

Deleting is what you expect:

$ isard media delete ubuntu-24

⚠ About to delete media:
  Name : ubuntu-24
  ID   : 2b8f61c4-...
  State: Downloaded
Delete this media? [y/N] y
✓ Media deleted

By hand, in the browser

Now that there is an ISO in the library, you can boot a desktop off it.

In the media view, the icon on the row generates a desktop from that medium:

It opens the ordinary create form, except that the boot device is the ISO rather than a template:

For a Fedora Server, the settings that matter are:

Boot CD/DVD
Disk bus virtio
Disk 100 GB
Network Default and Wireguard VPN

Start it, open the viewer, and install the operating system exactly as you would on a real machine.

Then — and this is the step people miss — stop the machine and edit it before doing anything else:

Boot Hard Disk
Media None
Disk bus Default

If you skip that, the firmware finds the installer ISO again on the next boot and cheerfully offers to reinstall over what you just built.

Promoting

You now have a desktop with an operating system on it. Turning it into a template is one command:

$ isard template promote fedora-base --name fedora-44-base -d 'Fedora 44 Server, docker preinstalled'
✓ Resolved desktop: fedora-base

✓ Template 'fedora-44-base' created.
  Template ID : 5c1e9a07-...
  Source      : fedora-base (8d3a2f61-...)
  Enabled     : yes

Two conditions, and both are checked by the platform rather than by the tool:

  • the desktop must be Stopped;
  • its disk must actually have been written — a desktop created and never booted is not a valid source.

--disabled creates the template hidden, which is what you want if you are going to check it before letting a class use it.

The whole thing, in one file

Everything above is four commands, a form, an install and a promote, with two chances to get the boot order wrong.

isard build is all of it, described in one YAML file.

# ubuntu.yaml
image:
  distro: ubuntu
  variant: server
  release: lts

template:
  name: ubuntu-lts-base
  description: Ubuntu LTS with docker and the class tooling
  vcpus: 4
  memory_gb: 8
  disk_gb: 100

user:
  name: isard
  password_env: ISARD_TEMPLATE_PASSWORD
  ssh_key: ~/.ssh/id_ed25519.pub

packages: [git, curl, docker.io]
run:
  - systemctl enable --now docker
  - usermod -aG docker isard

Look at the plan before you spend an afternoon on it:

$ isard build ubuntu.yaml --dry-run
✓ Ubuntu 26.04.1 LTS Server (live-server)

Template : ubuntu-lts-base
Image    : Ubuntu 26.04.1 LTS Server (live-server), 2.9 GB
URL      : https://releases.ubuntu.com/resolute/ubuntu-26.04.1-live-server-amd64.iso
Hardware : 4 vCPU, 8 GB RAM, 100 GB disk
User     : isard
Install  : by hand in the viewer (user, password, SSH server)
Provision: hostname/locale/timezone, 3 package(s), 0 file(s), 2 command(s) over SSH

--dry-run touches the vendor's release index and nothing else — no session, no API call, no download.

It is the command to run after every edit to the file, because a typo in a key is an error, not a warning:

$ isard build ubuntu.yaml --dry-run
error: ubuntu.yaml: unknown key in `profile`: pakages
Allowed: files, hostname, image, keyboard, locale, packages, run, template, timezone, user

That strictness is deliberate. A silently ignored key is a key you discover twenty minutes into a 3 GB download.

Then run it:

$ isard build ubuntu.yaml
✓ Ubuntu 26.04.1 LTS Server (live-server)
...
  URL: https://releases.ubuntu.com/resolute/ubuntu-26.04.1-live-server-amd64.iso
✓ No existing media — will register
✓ Media registered
✓ Media id: 7d41c2e8-...
✓ Downloaded: ubuntu-26.04.1-live-server-amd64
✓ Created ubuntu-lts-base (c0a9e7b2-...)
✓ Bastion armed (applies at the next boot)
✓ Bastion will log in as 'isard'
✓ Desktop starting
✓ ubuntu-lts-base is now Started

In the viewer, install the OS. Three things have to match; the rest is yours:

  1. username     isard
  2. password     una-contrasenya-llarga
  3. SSH server   tick "Install OpenSSH server" in the installer

Then shut the machine down — the build resumes by itself the moment it stops.
Nothing else is needed: hostname, locale, timezone, packages, your SSH key
and passwordless sudo are all applied automatically afterwards.

The one step that is yours

The OS installer cannot be driven from the outside, so the build does not pretend to: it hands you the viewer and waits.

Three things have to match, and everything else is up to you:

what where
1 the username from user.name the installer's account screen
2 the password from user.password the same screen
3 an SSH server Ubuntu: tick Install OpenSSH server. Fedora: pick the Server environment

Then shut the machine down. Do not reboot it.

The build is watching for the desktop to reach Stopped — it does not prompt you for anything, so you can walk away and it will carry on without you.

✓ ubuntu-lts-base is now Stopped
✓ Booting from disk
✓ Desktop starting
✓ ubuntu-lts-base is now Started
✓ SSH is up

Applying the profile over SSH:

==> hostname ubuntu-lts-base
==> timezone Europe/Madrid
==> account isard
==> authorising the SSH key for isard
==> installing 3 package(s)
...
==> systemctl enable --now docker
==> usermod -aG docker isard
==> provisioning complete
✓ Guest account 'isard' is ready
✓ Stop requested
✓ ubuntu-lts-base is now Stopped
✓ Bastion will log in as 'isard'
✓ Deleted 'ubuntu-26.04.1-live-server-amd64'

✓ Template 'ubuntu-lts-base' is ready.
  Template ID : 4f6b0d1a-...
  Guest user  : isard
  Create one  : isard create my-box --template ubuntu-lts-base

The username and password matter because the bastion logs into the guest with them, and it is how the build gets in afterwards to do the rest.

Get them wrong in the installer and the configure phase cannot log in — which is the one failure mode of this command that costs you the whole install.

When SSH never answers

Almost always the OpenSSH option was missed on the installer's screen.

The build knows what that looks like, and rather than timing out it prints the fix and waits for you to run it in the desktop's own console:

$ curl -fsSL https://isard.xtec.dev/guest.sh | sudo sh -s isard mypassword

That is the same one-liner from Bastion, and it is short on purpose — the console you have to type it into has no working clipboard yet, which is one of the things it fixes.

What else you can put in it

key what
hostname, timezone, locale, keyboard the boring ones, applied over SSH
packages a list, installed with the guest's own package manager
run shell commands, in order, as root
files paths and contents, written into the guest

And three flags worth knowing:

--keep-media leave the ISO in the library, so the next build of the same release does not download 3 GB again
--no-promote stop after the machine is built and configured, and leave it as a desktop
--no-viewer do not launch a viewer; open the machine yourself

Unattended, by hand

isard build handles the case you will actually meet.

Underneath it there is a lower-level path worth knowing exists, because it is how an install becomes fully unattended with nobody at the viewer at all.

Both Ubuntu's installer and Fedora's read their configuration off a second CD, and they find it by its volume label:

label file read by
isard seed cidata CIDATA user-data cloud-init — Ubuntu autoinstall
isard seed oemdrv OEMDRV ks.cfg Anaconda — Fedora, RHEL, Rocky

Neither needs a kernel command line argument, which is the point: Isard's hardware form has nowhere to put one.

So you build the little ISO locally:

$ isard seed oemdrv --kickstart my-ks.cfg -o seed.iso
Wrote OEMDRV seed → seed.iso
Anaconda auto-loads ks.cfg from any volume labelled OEMDRV — no
kernel command-line tweak needed. Host the ISO, register via
`isard media add`, and attach alongside the Fedora installer ISO.

Host it somewhere over HTTPS, and hand the URL over:

$ isard template create --seed-url https://files.example.com/seed.iso

The installer boots, finds the second CD, reads it and installs without asking anything.

Exercises

TaskRead the library

List your media and say, for each one, whether it is yours or your centre's.

Show the solution
$ isard media

-- Media --
NAME                             STATUS      SIZE   OWNER
Fedora-Server-netinst-x86_64-44  Downloaded  1049M  you
ubuntu-24.04.4-live-server       Downloaded  3052M  shared

The OWNER column is the answer, and it matters for two reasons: a shared medium costs you no quota, and you cannot delete one.

TaskA profile that fails cheaply

Write a build profile for a Fedora Server template with git and htop, and check it with --dry-run without building anything.

Then break a key on purpose and check again.

Show the solution
# fedora.yaml
image:
  distro: fedora
  variant: server
  release: latest

template:
  name: fedora-class
  vcpus: 2
  memory_gb: 4
  disk_gb: 50

user:
  name: isard
  password_env: ISARD_TEMPLATE_PASSWORD

packages: [git, htop]
$ isard build fedora.yaml --dry-run
✓ Fedora 44 Server (Net Install)

Template : fedora-class
Image    : Fedora 44 Server (Net Install)
URL      : https://download.fedoraproject.org/pub/fedora/linux/releases/44/Server/x86_64/iso/Fedora-Server-netinst-x86_64-44-1.7.iso
Hardware : 2 vCPU, 4 GB RAM, 50 GB disk
User     : isard
Install  : by hand in the viewer (user, password, SSH server)
Provision: hostname/locale/timezone, 2 package(s), 0 file(s), 0 command(s) over SSH

$ isard build fedora.yaml --dry-run   # after renaming `packages` to `packge`
error: fedora.yaml: unknown key in `profile`: packge
Allowed: files, hostname, image, keyboard, locale, packages, run, template, timezone, user

The second run is the exercise. A profile that is wrong should be wrong in two seconds, not in twenty minutes.

TaskThree steps by hand

Without using isard build, turn a desktop into a template.

Use a machine you have finished with.

Show the solution
$ isard stop lab --wait
✓ Resolved desktop: lab (state: Started)
✓ lab → Stopping
✓ lab is now Stopped

$ isard template promote lab --name lab-base -d 'Class base image'
✓ Resolved desktop: lab

✓ Template 'lab-base' created.
  Template ID : 0e7d3b52-...
  Source      : lab (6a19c4f0-...)
  Enabled     : yes

$ isard template --filter lab

-- Available Templates (filtered by 'lab') --
NAME      DESCRIPTION       OS
lab-base  Class base image  ubuntu

And now check what isard list says about lab:

$ isard list --name lab
No desktops found.

The desktop is gone. It was consumed by the promote, and that is the behaviour to remember from this page.

TaskWhy not just share the disk

Your classmate wants the machine you built. You could promote it to a template, or you could give them the login and let them use yours.

Give two reasons the template is the right answer.

Show the solution

Any two of these:

  • A template is a copy. Ten people each get their own machine and can break it. One shared machine is one person working and nine watching.
  • It costs quota once per person, not once. A shared machine holds your quota for the whole class.
  • It is reproducible. A machine you configured by hand over three weeks cannot be rebuilt; a template can be made again from a profile.
  • Nobody has to trust anybody. Sharing a login means sharing a password, and the account it belongs to is yours.

The last one generalises well past this page, and the third one is the reason isard build exists at all.

Project

Build the machine your class will actually use.

  1. Write a build profile for an Ubuntu LTS server with the tools your course needs — say git, curl, docker.io and python3.
  2. Add a run: step that adds the user to the docker group, and a files: entry writing a /etc/motd that names the template and the date.
  3. Check it with --dry-run until it is clean.
  4. Build it with --keep-media, so a second build does not download the ISO again.
  5. Make a desktop from the finished template and prove that docker run hello-world works without sudo.
Show the solution
# class.yaml
image:
  distro: ubuntu
  variant: server
  release: lts

template:
  name: class-base
  description: Ubuntu LTS with docker and python
  vcpus: 4
  memory_gb: 8
  disk_gb: 100

user:
  name: isard
  password_env: ISARD_TEMPLATE_PASSWORD
  ssh_key: ~/.ssh/id_ed25519.pub

hostname: class-base
timezone: Europe/Madrid

packages: [git, curl, docker.io, python3]
run:
  - systemctl enable --now docker
  - usermod -aG docker isard
files:
  /etc/motd: |
    class-base — Ubuntu LTS, docker + python
    Built 2026-08-14 with `isard build`.
$ export ISARD_TEMPLATE_PASSWORD='una-contrasenya-llarga'
$ isard build class.yaml --dry-run
$ isard build class.yaml --keep-media

Then:

$ isard create demo --template class-base --yes
$ isard start demo --wait
$ isard ssh demo -- 'docker run --rm hello-world'
Hello from Docker!

docker run without sudo is what proves the usermod ran, and it is worth checking rather than assuming: a group change only takes effect on a new login, and this is a new login.