Cloud-Init on Ubuntu WSL: Deterministic First-Boot Provisioning
Provision Ubuntu on WSL at first boot with cloud-init user-data, named instances, execution semantics, diagnostics, and repeatable acceptance checks.
Cloud-init can provision Ubuntu on WSL when an instance is initialized for the first time. That makes a named distribution reproducible: create a Linux user, install packages, write configuration, and run setup commands from cloud-config instead of repeating the same interactive sequence on every workstation. The important operational distinction is that cloud-init is a first-boot provisioning mechanism, not a continuously reconciling configuration manager. Editing the Windows-side user-data file after an instance has completed setup does not automatically turn it into a new instance.
The feature is specific to Ubuntu on WSL and depends on a compatible WSL package and Ubuntu image. Canonical’s current documentation covers Ubuntu 22.04 and later, recommends Windows 11 (with Windows 10 version 21H2 or later also listed), and notes that cloud-init requires systemd and Windows interoperability to be enabled. The upstream WSL datasource also requires Windows-drive automount because it reads user-data from the Windows filesystem. These WSL settings are enabled by default, but disabling any of them can prevent provisioning. Confirm the requirements for the exact distribution image and WSL release you deploy; do not infer capability solely from the Ubuntu version string.
The configuration and commands below target Windows plus Ubuntu on WSL. They cannot be executed on the macOS host used to author this article; validate them in a disposable Windows/WSL environment before adopting them.
Understand the two sides of the handoff
The seed file lives in the Windows user’s home directory, while cloud-init and the resulting configuration run inside the Linux distribution. For a distro instance named Ubuntu-24.04, Canonical documents a matching file at %UserProfile%\.cloud-init\Ubuntu-24.04.user-data. The name is part of the lookup: a file for Ubuntu-24.04 does not automatically configure a separately named instance such as UbuntuWebDev.
Cloud-init must see the seed before the instance’s first setup completes. Recent WSL install flows launch the distro and prompt through its initial user setup; if that first-launch flow has already completed, cloud-init will not provision it as a new instance. Microsoft provides --no-launch specifically to install a distribution without automatically launching it. That allows automation to stage user-data first and then start the distro deliberately.
$distro = 'Ubuntu-24.04'
$seedDirectory = Join-Path $env:USERPROFILE '.cloud-init'
New-Item -ItemType Directory -Force -Path $seedDirectory | Out-Null
@'
#cloud-config
locale: en_US.UTF-8
users:
- name: wslops
gecos: WSL Operations
groups: [adm, dialout, cdrom, floppy, sudo, audio, video, plugdev, netdev]
sudo: "ALL=(ALL) NOPASSWD:ALL"
shell: /bin/bash
write_files:
- path: /etc/wsl.conf
append: true
content: |
[user]
default=wslops
packages:
- git
- jq
package_update: true
runcmd:
- [install, -d, -o, wslops, -g, wslops, /srv/project]
'@ | Set-Content -Encoding utf8 (Join-Path $seedDirectory "$distro.user-data")
wsl.exe --install --distribution $distro --no-launch
wsl.exe --distribution $distro
The example provisions a non-root default account, writes a per-distro WSL setting, updates apt metadata, installs two packages, and creates an application directory owned by that account. The exact groups available can vary by image, so validate the group list against the target root filesystem. The runcmd list form passes an argument vector rather than asking a shell to reinterpret a command string; it runs late in cloud-init’s first-boot process. Cloud-init executes these setup stages with system-level privileges, so only put intended bootstrap actions in user-data.
PowerShell’s Set-Content encoding behavior varies across Windows PowerShell and PowerShell 7. If a WSL image rejects the seed due to a byte-order mark or unexpected encoding, write UTF-8 without a BOM and inspect the first line inside Windows before installing. Keep the #cloud-config header as the first non-empty line. An invalid YAML file can prevent the expected modules from applying even when the distro itself launches normally.
Treat user-data as a versioned first-boot input
The cloud-init data source matches user-data to the named Ubuntu WSL instance. The payload may contain normal cloud-config keys such as users, packages, write_files, and runcmd. These are not arbitrary YAML keys: cloud-init validates against a schema and dispatches supported keys through its modules. A valid YAML document can still contain an unsupported or misspelled cloud-config key, so syntax checking alone is insufficient.
Use stable distro instance names for durable developer environments and distinct names for parallel project environments. For example, a build workspace and a data-science workspace can both originate from Ubuntu 24.04 while receiving separate .user-data files. Make the distribution instance name, seed file name, base image, and provisioning revision visible in your automation logs. If they drift independently, it becomes difficult to explain why two Ubuntu instances with the same release have different packages or defaults.
Keep package installation and shell commands idempotent where practical. Cloud-init modules have execution frequencies, and many setup operations run once per instance. A machine restart is not a request to replay all provisioning. A command that appends a line every time, creates a new account unconditionally, or assumes a clean directory can fail or produce duplicate configuration if it is run manually or through a separate automation path. Prefer declarative keys such as packages or write_files when they describe the desired state, and use runcmd for limited first-boot actions that need imperative commands.
Cloud-init is not a secret store. Treat user-data as configuration that may be visible in local files and diagnostic artifacts; avoid embedding passwords, private keys, access tokens, or other long-lived credentials in it. Provision access through an appropriate separate mechanism and keep reusable images free of machine-specific credentials. This is especially important when exporting or cloning a configured distro: the copied Linux filesystem is a separate artifact from the Windows seed directory, and either side may retain information that an operator did not intend to distribute.
Verify execution rather than trusting a successful launch
The Windows process returning from wsl.exe proves only that WSL launched a command. Wait for cloud-init to finish and inspect its status from inside the target distro. The --wait option blocks until the boot stages complete; --long adds detailed status information. Check the return code as well as the text, because cloud-init can finish with recoverable errors rather than a clean success.
wsl.exe --distribution Ubuntu-24.04 -- sudo cloud-init status --wait --long
wsl.exe --distribution Ubuntu-24.04 -- sudo cloud-init schema --system
wsl.exe --distribution Ubuntu-24.04 -- whoami
wsl.exe --distribution Ubuntu-24.04 -- dpkg-query -W git jq
wsl.exe --distribution Ubuntu-24.04 -- stat -c '%U:%G %a %n' /srv/project
The expected checks are that cloud-init reports a completed, non-error state; schema validation accepts the system user-data; the session uses wslops; the requested packages are installed; and the application directory has the intended owner and group. Also inspect /var/log/cloud-init.log and /var/log/cloud-init-output.log when a module failed or a command did not produce the expected result. Use cloud-init analyze blame on supported cloud-init versions to identify which boot stage consumed time before optimizing a long provisioning path.
Do not declare provisioning successful based only on the greeting printed by the distro launcher. The launcher can complete its own work while a cloud-init module has failed. Conversely, a slow package mirror can make a correct cloud-config appear stuck. A useful acceptance record stores the WSL version, Windows build, distro name and release, user-data revision or checksum, cloud-init status, installed package list, and the verification command outputs. That evidence lets an administrator distinguish a bad seed from a transient package repository or host problem.
Handle first-run mistakes safely
If the user-data file is wrong before first launch, fix the file and then start the distro. If the initial WSL setup already ran, simply editing the file does not replay the same first-boot workflow. For an experimental instance, the cleanest test loop is usually to create a new named instance from a known base and give that instance a matching seed file. Keep valuable working distributions out of this loop and take a verified export before any operation that removes a registration.
Cloud-init offers clean and module-level rerun commands, but they are debugging tools with different effects, not a routine production reset button. Upstream warns that cleaning can make cloud-init behave as if the machine had never initialized, and re-running modules may not be idempotent. In WSL, resetting user-data state without understanding the WSL data source and the distribution’s existing system configuration can produce duplicate users, overwritten files, or inconsistent default-user behavior. Prefer a disposable clone for experiments and document exactly which instance you reset.
For a provisioning failure, first check the name match, whether first-run setup already completed, and whether the target Ubuntu image has cloud-init and its WSL data source enabled. Then validate the YAML and inspect the cloud-init logs. If the failure is in a later shell command, isolate that command and execute it manually in a test distro with the same user, environment, and filesystem paths. This avoids repeatedly resetting an entire instance when the defect is a package name or command exit status.
When cloud-init is the wrong layer
Cloud-init is appropriate for initial creation of a repeatable Ubuntu WSL instance. It does not manage every later package update, enforce a long-lived desired state, or control the shared Windows host’s global .wslconfig. Use Ubuntu’s package-management and service tools for ongoing Linux changes, and use WSL’s host-side configuration for VM-wide resource and networking settings. For multi-machine deployment, Canonical documents Landscape and Ubuntu Pro for WSL as a separate remote-management workflow that can use cloud-init during child-instance creation.
The operational boundary is useful: cloud-init creates the initial per-instance baseline; WSL registers and starts the distribution; later lifecycle and fleet controls belong to their respective WSL or Ubuntu management layers. Keeping those responsibilities distinct prevents repeated first-boot scripts from becoming a fragile substitute for configuration management.
Related:
- Inside a WSL Distribution Launcher: Registration, Default Users, and First Run
- How to Install and Manage Multiple Linux Distros in WSL
Sources:
- Automatic setup of Ubuntu on WSL with cloud-init
- Ubuntu on WSL instance configuration reference
- Microsoft WSL install and command reference
- Microsoft WSL advanced settings configuration
- Cloud-init module reference
- Cloud-init status and CLI reference
- Cloud-init: how to re-run configuration
- Cloud-init WSL data source reference