cigo turns a plain cloud-config file into a repeatable LXD container deploy. Your template stays valid cloud-config; cigo only adds ordered actions, safe secrets and re-deploys that actually change things.
One file. No install, no inventory, no state file, no new language to learn.
The file stays a normal cloud-config. cigo directives are one-line
cigo: statements that are stripped before upload, so existing
cloud-init knowledge and tooling keep working.
First boot belongs to cloud-init. Everything repeatable — packages, files, renders, service restarts — goes into the pipeline and runs on every call.
redeploy reapplies config, devices and actions.
configure changes only LXD state — no actions, no cloud-init —
so it is fast and safe on a running container.
Secrets come from a protected file or an environment variable and are only available to the renderer. Never in user-data, never in arguments, never printed.
Jinja2 runs on the host and only the result is pushed into the container. No container needs Python, and a broken template fails before anything changes.
cigo: from old-container creates a replacement on a newer image
and carries over eligible config and devices — the manual part of a distro upgrade.
| Layer | Runs |
|---|---|
| Bootstrap (cloud-config) | first boot only |
Pipeline (cigo:) | every invocation |
That split is the whole idea. Anything that must survive a re-deploy lives in the pipeline; anything that really is first-boot-only stays in cloud-config.
Each cigo: line is one action or one piece of container state.
They run in the order you write them, and the same file works for the first
deploy and every re-deploy after it.
#cloud-config
cigo: image ubuntu:24.04
cigo: config limits.memory=2GiB
cigo: device disk www source=/srv/www path=/var/www
cigo: render files/vhost.j2 /etc/apache2/site.conf 0644
cigo: render files/extra.conf.j2 /etc/apache2/conf-available/extra.conf 0644
cigo: run systemctl reload apache2
package_update: true
package_upgrade: true
--dry-run prints the whole plan, --check validates
everything it can without touching LXD, --dump-user-data shows the
exact cloud-config that will be uploaded.
| Verb | Target | Applies |
|---|---|---|
create | must not exist | config, devices, cloud-init, actions |
redeploy | cigo-managed | config, devices, actions |
configure | any existing | config and devices only |
adopt | created elsewhere | config and devices |
create refuses a container that already exists, and
redeploy refuses one cigo did not build. The old ambiguity —
a typo in --name silently turning a create into a partial
update — is gone.
--check turns any of them into a read-only drift report and
exits non-zero when the container no longer matches the template, so it
doubles as a CI gate.
# change live LXD state only, no actions, no cloud-init
./cigo.sh configure --name web-01 --template templates/lamp --check
# take over a container that was created by hand
./cigo.sh adopt --name legacy-web --template templates/lamp
Apache, PHP and MySQL for a small site.
One virtual host, static document root.
Reverse proxy in front of an app container.
Standalone database, no remote access by default.
SFTP-only, host key supplied as a secret.
Virtual mailboxes backed by MySQL.
VPN endpoint with the private key as a secret.
Authoritative zone, rendered and checked.
They are starting points, not hardened appliances. Public mail, DNS, VPN and file-transfer services need their own DNS, firewall, TLS, monitoring and backups.
./cigo.sh create --name web-01 --template templates/lamp.
Later, redeploy applies template changes and
configure changes container settings only. Add
--yes in scripts.
On the host: Bash, the lxc client, jq and
python3-jinja2 for rendering. In the container: an image with
cloud-init, plus rsync and tar for copy/sync actions.
cloud-init on the host is optional — it only adds schema
validation to --check.
No loops, no conditions, no roles, no inventory, no cluster support, no Incus. cigo targets small labs and infrastructures with a handful to a few dozen single-purpose containers. For anything larger, reach for Ansible or Terraform.
A template runs commands as root in a container and can mount host paths through LXD. Templates are not sandboxed or signed, so read them before running them — including the ones shipped here.
cigo is short for CI and go: run it, then get on with your work. In Czech, cigo also happens to be slang for a cigarette — so locally the pitch is “run cigo, then go for a cigo”: the container is up, take a break. It is a pun, not an endorsement of smoking.
The second meaning of go is the point: the tool should disappear from your attention the moment the container is ready.