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.
#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.
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.
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 — SequenceInitialize → SequenceInstall → SequenceBoot (main.go:274–292) — then blocks watching the runtime's event stream, translating SequenceEvent/RestartEvent into either a fatal error or a runtime.RebootError.
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/haltIfInstalled → config (LoadConfig).
Install does real work only if the machine is not yet installed, ending in Install → FlushMeta → TeardownVolumeLifecycle → StopAllServices → KexecPrepare → Reboot — 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:
| # | Phase | Task(s) | Notes |
|---|---|---|---|
| 1 | memorySizeCheck | MemorySizeCheck | |
| 2 | diskSizeCheck | DiskSizeCheck | |
| 3 | env | SetUserEnvVars, WaitForCARoots | |
| 4 | dbus | StartDBus | |
| 5 | sharedFilesystems | SetupSharedFilesystems | container mode only |
| 6 | ephemeral | MountEphemeralPartition | |
| 7 | udevSetup | WriteUdevRules | non-container |
| 8 | userDisks | MountUserDisks | non-container |
| 9 | userSetup | WriteUserFiles | |
| 10 | startEverything | StartAllServices | kubelet, 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:
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:
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.
#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:
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:
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.
- RebootError →
handle()reverts the bootloader, kills all processes, unmounts everything, callsunix.Sync()with a 30-second budget, then issues the carried reboot command. - Any other fatal error → same teardown, but on
panic=0it sleeps forever so the failure can be inspected instead of looping. - 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.