Skip to content
Français
Conteneurs & Orchestration medium

Automating Incus image builds

30 min de lecture

Read this page in French

incus logo

Building Incus images by hand does not scale. This guide industrialises that build through two routes: Packer with its Incus builder, and Ansible with the connection plugin, to produce reproducible and publishable images. It covers a complete Packer template, the equivalent Ansible playbook, and publishing the image into Incus. The commands apply to Incus 7.0.

What you will learn

  • Build an image with a Packer template (the Incus builder).
  • Do the same with an Ansible playbook.
  • Publish the configured instance as a reusable image.
  • Choose between the two approaches.

Building a custom image with Packer

This section walks through creating a custom image with Packer using the Incus builder. That builder produces container images compatible with Incus while giving the automation and reproducibility Packer provides.

Prerequisites

You first need Packer installed. It is available from the official site and through package managers such as apt on Linux or brew on macOS, or as a binary on Windows.

Then make sure Incus is properly installed and configured in your environment. The Packer builder for Incus needs Incus reachable to generate container images. See installing Incus if needed.

The Packer configuration file

Building an image with Packer rests on a template in JSON or HCL that says how the image will be built. Here is a simple HCL file for an Incus image:

debian-incus.pkr.hcl
packer {
required_plugins {
incus = {
version = ">= 1.0.0"
source = "github.com/bketelsen/incus"
}
}
}
source "incus" "trixie" {
image = "images:debian/13"
output_image = "debian-ansible"
reuse = true
}
build {
sources = ["incus.trixie"]
provisioner "shell" {
scripts = [
"scripts/debian/init.sh",
]
}
}

Here the Incus builder takes a Debian 13 base image to create a new one. The provisioner block runs commands that prepare the image for use with Ansible.

The shell script:

scripts/debian/init.sh
apt update
apt install -y openssh-server python3 sudo
useradd -m -s /bin/bash ansible
mkdir -p /home/ansible/.ssh
echo 'ssh-ed25519 xxxxxxxxxxxx' > /home/ansible/.ssh/authorized_keys
chown -R ansible:ansible /home/ansible/.ssh
chmod 600 /home/ansible/.ssh/authorized_keys
echo 'ansible ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/ansible
PASSWD=$(date | md5sum | cut -c1-8)
echo "ansible:$PASSWD" | chpasswd

Running the build

To build the image, run the following from the directory holding the HCL file:

Fenêtre de terminal
packer build debian-incus.pkr.hcl

Packer reads the template, uses the incus builder to download the base image, runs the provisioning script to configure it, then makes it available locally.

Checking the generated image

Once built, you find the image in Incus:

Fenêtre de terminal
incus image list debian-ansible
+----------------+--------------+--------+--------------------------------------+--------------+-----------+-----------+----------------------+
| ALIAS | FINGERPRINT | PUBLIC | DESCRIPTION | ARCHITECTURE | TYPE | SIZE | UPLOAD DATE |
+----------------+--------------+--------+--------------------------------------+--------------+-----------+-----------+----------------------+
| debian-ansible | 0fb77aa067dc | no | Debian trixie amd64 (20260630_05:24) | x86_64 | CONTAINER | 184.26MiB | 2026/07/01 07:59 UTC |
+----------------+--------------+--------+--------------------------------------+--------------+-----------+-----------+----------------------+

Extra builder options

Beyond the options above, the Incus builder offers several optional parameters:

  1. reuse: reuse an existing image alias.
  2. publish_remote_name: publish the image on a specific Incus remote.
  3. init_sleep: the delay, in seconds, between launch and provisioning.
  4. virtual_machine: build a virtual machine image rather than a container image.

A field note: pairing the Packer Ansible provisioner with the Incus connection stays fragile. Packer does create and start the container, but Ansible provisioning through the Incus plugin does not always follow correctly. When that pairing gives trouble, an all-Ansible approach is more robust and drops Packer entirely.

Building an image with Ansible

You can use Ansible with the Incus connection plugin to automate creating and configuring images directly on containers, without Packer. The plugin lets Ansible treat Incus containers as remote machines, which makes running roles and playbooks straightforward. How the connection plugin works is covered in managing Incus with Ansible.

First install the community.general collection:

Fenêtre de terminal
ansible-galaxy collection install community.general

Here is the playbook doing the same work as the Packer template:

---
- name: Create instance
hosts: localhost
connection: local
tasks:
- name: Create instance
ansible.builtin.shell:
cmd: incus launch images:debian/13 debian-ansible
creates: debian-ansible.ok
- name: Start instance
ansible.builtin.shell:
cmd: incus start debian-ansible
- name: Create flag
ansible.builtin.file:
path: debian-ansible.ok
state: touch
- name: Run command in container
hosts: debian-ansible
connection: community.general.incus
gather_facts: false
vars:
pwd: "{{ lookup('password', '/dev/null length=15 chars=ascii_letters') }}"
tasks:
- name: Install packages
ansible.builtin.raw: apt install -y python3
args:
executable: /usr/bin/bash
- name: Create user
ansible.builtin.user:
name: ansible
shell: /usr/bin/bash
password: "{{ pwd }}"
state: present
create_home: true
home: /home/ansible
- name: Copy SSH key
ansible.posix.authorized_key:
user: ansible
state: present
key: "{{ lookup('file', item) }}"
with_fileglob:
- ~/.ssh/id_ed25519.pub
- name: Add user to sudoers
community.general.sudoers:
user: ansible
name: ansible
nopassword: true
commands: ALL
state: present
- name: Publish the image
hosts: localhost
connection: local
tasks:
- name: Stop instance
ansible.builtin.shell:
cmd: incus stop debian-ansible
removes: debian-ansible.ok
- name: Create image
ansible.builtin.shell:
cmd: incus publish --alias debian_ansible debian-ansible --reuse
removes: debian-ansible.ok
- name: Delete flag
ansible.builtin.file:
path: debian-ansible.ok
state: absent

The playbook splits into three stages:

  1. Creating an instance: it launches a Debian 13 instance with Incus and creates a flag file marking successful creation. The instance is then started.

  2. Running commands in the container: Ansible connects through Incus and runs several tasks: install Python 3, create an ansible user with a random password, copy an SSH key and add the user to sudoers.

  3. Creating the image: once configured, the instance is stopped and published as an image named debian_ansible in Incus. The flag file is removed.

The command stays ordinary, since the parameters live in the playbook:

Fenêtre de terminal
ansible-playbook incus.yml -i debian-ansible,

The inventory can also be populated dynamically by the community.general.incus inventory plugin, rather than passed on the command line. This image build then fits naturally into a CI/CD pipeline to publish each new image automatically.

Key points

  • Two routes to industrialise: the Incus Packer builder, or an end-to-end Ansible playbook.
  • The Packer plus Ansible provisioner pairing stays fragile; the all-Ansible approach is more robust.
  • The playbook launches an instance, configures it through the connection plugin, then publishes it as an image.
  • incus publish --alias turns a stopped instance into a reusable image.
  • The published image appears in incus image list with its fingerprint, size and date.

FAQ: common questions about automating Incus images

Next steps

Is this site useful to you?

Fewer than 1% of readers support this site.

I maintain more than 700 free guides, with no ads and no tracking. Any support, even a symbolic one, helps cover hosting and keeps these resources free. Thank you for the help.

The form does not show? Open Ko-fi in a new tab.

Subscribe and follow my DevSecOps work on LinkedIn