runtime  /  boot & sequencer machined PID 1
Chapter 01

Boot & the Sequencer

A Talos machine has no shell, no systemd, no package manager — and yet it comes alive from a kernel hand-off in a few seconds. It does so through two PID-1 programs sharing one binary and an imperative sequencer that runs three ordered lifecycle sequences. This chapter follows the machine from init in the initramfs to a node that is up and serving the API.

#Two PID 1s, one binary

Talos boots in two distinct PID-1 programs that live in the same binary tree but run one after the other. The kernel is unpacked from a UKI, the initramfs is extracted, and the kernel execs init. That is the first PID 1, and its whole job is to prepare and pivot into the real root.

internal/app/init/main.go drives this. Its run() mounts the pseudo filesystems (/proc, /sys, /dev), wires kernel logging to /dev/kmsg under the [talos] [initramfs] tag, seeds the RNG from the TPM, and — critically for measured boot — extends PCR 11 with enter-initrd. Only then does it mount the real root filesystem.

mountRootFS() has two modes. The plain case is a read-only squashfs mount of constants.RootfsAsset. When system extensions are configured, it instead composes a read-only overlay — extension squashfs layers stacked on top of the root squashfs — so the immutable base image is never modified, only layered over. It bind-mounts firmware and /.extra, then calls switchroot.Switch(constants.NewRoot, pseudo) to pivot.

init never returns.

switchroot execs the next stage in place, so control never comes back to the initramfs program. Its only fallback path is recovery(), which reboots on panic — unless the kernel was booted with panic=0, in which case it sleeps so an operator can inspect the failure.

kernel UKI + initramfs init initramfs · PID 1 switch-root exec NewRoot machined real PID 1 · reaper running node serving apid mount /proc /sys /dev PCR 11 ← enter-initrd squashfs + overlay sequencer runs three sequences in order Initialize StartContainerd Install if !installed Boot StartAllServices
kernel → init (initramfs PID 1) → switch-root → machined (PID 1) → Initialize / Install / Boot → running.

#machined: the real PID 1

After switch-root, the long-lived supervisor is internal/app/machined/main.go. The same binary serves several daemons; main() dispatches on filepath.Base(os.Args[0]), so re-exec'ing the binary under a different name is the way Talos launches apid, trustd, the dashboard, or a poweroff/shutdown helper. Everything else (init, machined) falls through to the supervisor entrypoint.

internal/app/machined/main.gogo
switch filepath.Base(os.Args[0]) {
case "apid":
	apid.Main()
	return
case "trustd":
	trustd.Main()
	return
case "poweroff", "shutdown":
	poweroff.Main(os.Args)
	return
case "init", "machined":
	// fall through to the main machined entrypoint
}

Because machined is PID 1, it installs a panic recovery() handler and immediately starts the process reaper (reaper.Run()) so orphaned zombies are reaped. From there, run() (main.go:165) constructs the controller, runs early startup tasks (startup.DefaultTasks()), and enters runEntrypoint.

The COSI runtime starts here, in a goroutine.

Before any boot sequence runs, the entrypoint creates a runtime.Drainer and starts the declarative COSI controller runtime with c.V1Alpha2().Run(ctx, drainer) on its own goroutine — roughly 200 controllers reconciling a shared resource state. The sequencer's tasks read and write the same resources those controllers reconcile. We keep this chapter on the imperative lifecycle; the controller graph, the service manager, and how the two runtimes share state are the next chapter.

#Three sequences, in order

The entrypoint runs three lifecycle sequences strictly in order — SequenceInitializeSequenceInstallSequenceBoot (main.go:274–292) — then blocks watching the runtime's event stream, translating SequenceEvent/RestartEvent into either a fatal error or a runtime.RebootError.

internal/app/machined/main.go:274go
if err := c.Run(ctx, runtime.SequenceInitialize, nil); err != nil { ... }
if !initializeCanceled {
	if err := c.Run(ctx, runtime.SequenceInstall, nil); err != nil { return err }
	if err := c.Run(ctx, runtime.SequenceBoot, nil); err != nil && !errors.Is(err, context.Canceled) { return err }
}

The ordering matters: if Initialize is canceled (preempted by, say, a reset request), Install and Boot are skipped entirely. Each sequence is a list of named phases produced by the Sequencer.

Initialize lays the groundwork: systemRequirements (EnforceKSPPRequirements) → earlyServices (StartUdevd, StartMachined, StartApid, StartAuditd, StartSyslogd, StartContainerd) → usb (WaitForUSB) → meta (ReloadMeta) → deferred cleanupBootloader/dashboard/wipeDisks/haltIfInstalledconfig (LoadConfig).

Install does real work only if the machine is not yet installed, ending in InstallFlushMetaTeardownVolumeLifecycleStopAllServicesKexecPrepareReboot — i.e. it installs to disk and reboots into the installed system.

Boot is the sequence that brings a node up. Its phases, in order:

#PhaseTask(s)Notes
1memorySizeCheckMemorySizeCheck
2diskSizeCheckDiskSizeCheck
3envSetUserEnvVars, WaitForCARoots
4dbusStartDBus
5sharedFilesystemsSetupSharedFilesystemscontainer mode only
6ephemeralMountEphemeralPartition
7udevSetupWriteUdevRulesnon-container
8userDisksMountUserDisksnon-container
9userSetupWriteUserFiles
10startEverythingStartAllServiceskubelet, etcd, …

#Phases serial, tasks concurrent

A Sequencer returns []Phase for each operation. A Phase is a name plus a slice of TaskSetupFunc and an optional CheckFunc that can skip the whole phase:

internal/app/machined/pkg/runtime/controller.gogo
type TaskSetupFunc func(seq Sequence, data any) (TaskExecutionFunc, string)
type TaskExecutionFunc func(context.Context, *log.Logger, Runtime) error

type Phase struct {
	Name      string
	Tasks     []TaskSetupFunc
	CheckFunc func() bool
}

The execution model is the heart of the sequencer: phases run serially, tasks run concurrently. Controller.run() iterates phases one at a time, aborting on the first failure and checking ctx.Done() at every phase boundary so a takeover cancels promptly. Within a phase, runPhase() launches every task in parallel through an errgroup:

internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_controller.go:308go
eg, ctx := errgroup.WithContext(ctx)
for number, task := range phase.Tasks {
	number++
	eg.Go(func() error {
		progress := fmt.Sprintf("%d/%d", number, len(phase.Tasks))
		if err := c.runTask(ctx, progress, task, seq, data); err != nil {
			return fmt.Errorf("task %s: failed, %w", progress, err)
		}
		return nil
	})
}
return eg.Wait()

The practical consequence: ordering is expressed only by phase boundaries. Two things that must happen in sequence go in two phases; two things that may race go in one phase as sibling tasks. Phase lists are built with a fluent PhaseList builder — .Append, .AppendWhen(cond, …), .AppendWithDeferredCheck(check, …), .AppendList — which is how the mode-specific variation (container vs. metal) is woven in without branching the whole sequence.

Sequence SequenceBoot — ordered []Phase Phase · ephemeral runs after previous phase completes MountEphemeralPartition single task Phase · env errgroup — tasks run concurrently SetUserEnvVars WaitForCARoots eg.Wait() Phase · startEverything StartAllServices PriorityLock[T] one sequence at a time Boot — running holds ctx Upgrade / Reset CanTakeOver(Boot) → cancel Boot ctx else → ErrLocked (Code_LOCKED, ignored)
A Sequence is ordered Phases; a Phase is parallel Tasks. The PriorityLock lets Upgrade/Reset cancel a running Boot.

#The priority lock & takeover

Only one sequence may run at a time. The PriorityLock[T] in runtime/v1alpha1/v1alpha1_priority_lock.go enforces that, but with a twist: a higher-priority sequence can preempt a running one by cancelling its context. Whether a takeover is allowed is decided by a small, deliberately asymmetric matrix:

internal/app/machined/pkg/runtime/sequencer.gogo
var sequenceTakeOver = map[Sequence]map[Sequence]struct{}{
	SequenceBoot: {
		SequenceReboot: {}, SequenceReset: {},
		SequenceUpgrade: {}, SequenceEmergencyVolumeCleanup: {},
	},
	SequenceReboot: {SequenceReboot: {}},
	SequenceReset:  {SequenceReboot: {}},
}

Read it as "the running sequence (outer key) can be taken over by these (inner keys)". A running Boot yields to reboot, reset, upgrade, and emergency volume cleanup — exactly the operations an operator or controller might fire while a node is still coming up. But upgrade and reset do not preempt each other, and only a reboot can preempt a running reboot. If the requested sequence CanTakeOver the running one (or the call uses WithTakeover(), the SIGTERM/ACPI escape hatch), the lock cancels the running sequence's context. Otherwise it returns runtime.ErrLocked.

Code_LOCKED is non-fatal by design.

When a sequence is rejected for the lock, the failure is published as Code_LOCKED — and the entrypoint's event watcher ignores it rather than treating it as a fatal machine error. The same goes for Code_CANCELED when a takeover cancels the loser. A denied or preempted sequence is a normal, expected outcome, not a crash. SequenceEmergencyVolumeCleanup is the subtle case: it takes over before the context cancel propagates, so the still-alive COSI runtime can react to the volume teardown.

#Runtime modes

The same sequencer code runs on bare metal, in a cloud VM, and inside a container — but each platform gets different behavior. runtime/mode.go defines four modes — ModeCloud, ModeContainer, ModeMetal, ModeMetalAgent — and a ModeCapability bitmask (Reboot, Rollback, Shutdown, Upgrade, MetaKV). The mode comes from the platform via r.State().Platform().Mode().

🖥️

ModeMetal

The only mode where RequiresInstall() is true — a physical machine must install itself to disk and reboot before it really runs.

☁️

ModeCloud

Full capability set; the image is already provisioned, so Install short-circuits and the node boots straight through.

📦

ModeContainer

Strips reboot/shutdown/upgrade/rollback/MetaKV. Initialize is tiny; udev, user-disk and ephemeral wiring are skipped; Reset/Upgrade/Install short-circuit.

🔧

ModeMetalAgent

Like metal, but suppresses the dashboard — it is the maintenance/agent variant of a bare-metal node.

The capability stripping is a single array indexed by mode:

internal/app/machined/pkg/runtime/mode.go:91go
return [...]uint64{
	all,                                                   // cloud
	all ^ uint64(Reboot|Shutdown|Upgrade|Rollback|MetaKV), // container
	all,                                                   // metal
	all,                                                   // metal-agent
}[m]

Mode is then threaded into many COSI controllers via a V1Alpha1Mode field, so the declarative side of the runtime adapts to the platform as well. That cross-over between the imperative sequencer and the controller graph is exactly where the next chapter picks up.

#Reboot is a typed error

How does a node reboot itself cleanly from inside PID 1? Not with a syscall buried in a task — with a typed error unwound to the top. The Boot tasks (and Install's final Reboot task) return a runtime.RebootError carrying the reboot command. The entrypoint's event watcher surfaces it, and the top-level handle() (main.go:101) interprets the return value of the whole machine.

  1. RebootError → handle() reverts the bootloader, kills all processes, unmounts everything, calls unix.Sync() with a 30-second budget, then issues the carried reboot command.
  2. Any other fatal error → same teardown, but on panic=0 it sleeps forever so the failure can be inspected instead of looping.
  3. Code_LOCKED / Code_CANCELED → swallowed by the watcher; the machine keeps running.

This is the Talos pattern in miniature: the lifecycle of an entire operating system expressed as ordered phases of concurrent tasks, a small preemption matrix, and an error type that means "reboot". With the imperative side mapped — two PID-1 programs, the sequencer, the priority lock, and the runtime modes — we can turn to the other half of machined: the COSI controller runtime it started in a goroutine, the ~200 controllers reconciling shared state, and the service manager that actually keeps apid, etcd and the kubelet alive. That is Chapter 02 — machined & the COSI Runtime.