
Typing CLI commands to create an instance is fine for learning; describing it as Infrastructure as Code is what makes an infrastructure reproducible. The lxc/incus provider, official to the LXC project, manages Incus instances, profiles, networks and pools with Terraform or OpenTofu. This guide writes a first main.tf, applies it to an Incus server, then targets a remote server by token, and lists the available resources. Tested on Incus 7.0 with OpenTofu 1.12 and provider 1.1.1.
What you will learn
- Declare the
lxc/incusprovider, local or remote. - Create an instance and a profile with
incus_instanceandincus_profile. - Target a remote server through a trust token.
- The available resources and the common traps.
Prerequisites
- Terraform or OpenTofu installed (the commands below use
tofu,terraformbehaves identically). - A reachable Incus server: either local through the Unix socket, or remote with the API exposed (
core.https_address). - The Incus basics: see First steps with Incus.
Declaring the provider
The provider is named lxc/incus on the Terraform and OpenTofu registries. On a local server it connects to the Incus Unix socket; no token is needed.
terraform { required_providers { incus = { source = "lxc/incus" version = "~> 1.1" } }}
provider "incus" { remote { name = "local" address = "unix://" } default_remote = "local"}The remote block declares an Incus server. unix:// targets the local daemon; the remote variant with a token comes further down.
Creating a first instance
You describe a profile (reusable configuration) and an instance that applies it. The image field follows the remote:alias format: here images:debian/13 pulls from the community image server.
resource "incus_profile" "web" { name = "tf-web-profile" config = { "limits.memory" = "512MiB" }}
resource "incus_instance" "web" { name = "tf-web" image = "images:debian/13" type = "container" profiles = ["default", incus_profile.web.name] config = { "boot.autostart" = "true" "limits.cpu" = "1" }}-
Initialise the directory:
tofu initdownloads the provider.- Installing lxc/incus v1.1.1...- Installed lxc/incus v1.1.1 (signed, key ID C638974D64792D67)OpenTofu has been successfully initialized! -
Apply:
tofu apply -auto-approvecreates the profile then the instance.incus_profile.web: Creation complete after 0s [name=tf-web-profile]incus_instance.web: Creation complete after 5s [name=tf-web]Apply complete! Resources: 2 added, 0 changed, 0 destroyed. -
Check on the Incus side: the instance runs, with both profiles applied.
+--------+---------+-----------+----------+| NAME | STATE | TYPE | LOCATION |+--------+---------+-----------+----------+| tf-web | RUNNING | CONTAINER | node1 |+--------+---------+-----------+----------+
On a cluster, the instance is placed automatically (here node1) unless you set target = "node2" in the resource. The profiles column of incus config show tf-web confirms default then tf-web-profile.
Targeting a remote server
To drive a remote Incus server, you declare its address and a single-use trust token, generated on the server with incus config trust add.
provider "incus" { generate_client_certificates = true accept_remote_certificate = true default_remote = "cluster"
remote { name = "cluster" address = "https://192.168.10.184:8443" token = "eyJjbGllbnRfbmFtZSI6..." }}The available resources
The provider covers the essentials of the Incus object model. Every resource accepts a remote field to target one of the declared servers.
| Resource | Role |
|---|---|
incus_instance | Container or VM (type = "virtual-machine") |
incus_profile | Reusable configuration profile |
incus_network | Network (bridge, OVN) |
incus_storage_pool | Storage pool (dir, ZFS, LVM) |
incus_storage_volume | Custom volume |
incus_project | Project (compartmentalisation) |
incus_image | Image managed or copied into the pool |
The traps worth knowing
Three defaults regularly surprise people, and are worth keeping in mind to avoid unexpected apply runs.
typedefaults tocontainer: for a virtual machine you have to spell outtype = "virtual-machine".- An explicit
profileslist replaces the default: if you pass a list, thedefaultprofile is not added automatically; include it when you need it, as above. - The image must be reachable from the remote:
image = "images:debian/13"assumes theimagesremote exists on the server side, which it does by default.
Destroying the infrastructure
Like any Terraform setup, the configuration is reversible: tofu destroy removes the instance and the profile it created. That is the point of IaC against manual clicking: the state is described, versioned and disposable.
tofu destroy -auto-approveKey points
- The
lxc/incusprovider manages Incus as code with Terraform or OpenTofu. - Locally it uses the Unix socket (
address = "unix://") with no token. - Remotely: a single-use trust token, then a client certificate that is remembered.
- Trap:
typedefaults tocontainer; an explicitprofileslist does not adddefault. - Resources:
incus_instance,incus_profile,incus_network,incus_storage_pool,incus_project.
FAQ: common questions about Terraform and Incus
The lxc/incus provider
The official provider of the LXC project is lxc/incus, available on the Terraform and OpenTofu registries.
terraform {
required_providers {
incus = {
source = "lxc/incus"
version = "~> 1.1"
}
}
}
It manages incus_instance, incus_profile, incus_network, incus_storage_pool and incus_project. Locally it uses the Unix socket (address = "unix://"), remotely a trust token.
The incus_instance resource
resource "incus_instance" "web" {
name = "tf-web"
image = "images:debian/13"
type = "container"
profiles = ["default"]
config = {
"limits.cpu" = "1"
}
}
The image field follows the remote:alias format. type defaults to container; for a virtual machine, set type = "virtual-machine". A tofu apply creates the instance:
incus_instance.web: Creation complete after 5s [name=tf-web]
Apply complete! Resources: 1 added.
Remote block and token
provider "incus" {
accept_remote_certificate = true
default_remote = "cluster"
remote {
name = "cluster"
address = "https://192.168.10.184:8443"
token = "eyJjbGllbnRfbmFtZSI6..."
}
}
The token (generated by incus config trust add) only authenticates the first connection: Terraform generates a client certificate that Incus remembers. On later apply runs the token is no longer used.
Either one works
The lxc/incus provider is published on the Terraform registry and the OpenTofu registry. The tofu and terraform commands are interchangeable: init, plan, apply and destroy behave the same.
OpenTofu is the community open source fork of Terraform, under the MPL licence. For a project aligned with open source, as Incus is as a fork of LXD, it is a consistent choice that changes nothing in the HCL.
Next steps
- Automating Incus image builds: feed your resources your own images rather than those of the public registry.
- Terraform: a three-tier stack: the same provider, describing a whole private cloud.
- Ansible connection plugin: configure the inside of the instances this provider creates.