Bootstrap Configuration

python3 -m scripts.bootstrap reads config/bootstrap.yaml and orchestrates the supported first-time deployment. The tracked config-example/bootstrap.yaml is the canonical starting point.

Configuration

version: 1

# Optional. If omitted, use the current Git origin URL and branch. The current
# commit must be the selected remote branch tip so the VM can clone it exactly.
repository:
  url: <infrastructure-repository-url>
  branch: bootstrap

deployment:
  # Version 1 supports a separate Ansible administrative host only.
  admin_model: separate

  # Exact service set. Version 1 supports this core pair only.
  services:
    - security/traefik
    - dashboard/homepage

proxmox:
  # SSH destination used for read-only checks and VM provisioning.
  ssh_target: <user@host-or-ssh-alias>

vm:
  # Use a numeric VM ID or "auto" for Proxmox allocation.
  id: <preferred-id-or-auto>
  name: <hostname>
  ubuntu_version: "26.04"
  username: admin
  cpu_cores: 4
  memory_max_mib: 4096
  memory_min_mib: 1024
  disk_size: 256G
  storage: local-lvm
  bridge: vmbr0

  # Optional. A supplied MAC must be valid, unicast, and unused. If omitted,
  # bootstrap derives a stable local MAC from the Proxmox target and VM name.
  mac: <mac-address>

  # Public key installed in the VM and later managed by Ansible.
  ssh_public_key: ~/.ssh/id_ed25519.pub

network:
  # DHCP is the only supported guest-addressing mode. Create a reservation for
  # the planned MAC at this address before applying. For a test-only deployment
  # without a reservation, use "auto" and complete the later operator checkpoint.
  expected_ipv4: <reserved-address-or-auto>

  # Base domain for the wildcard certificate and service routes.
  domain: <public-base-domain>

  dns:
    # DNS remains user-managed. Bootstrap verifies the base name, selected
    # service names, and a wildcard probe against expected_ipv4.
    mode: external
    verify: true

host:
  docker_volumes: /srv/docker-volumes
  timezone: Etc/UTC

tls:
  provider: cloudflare
  acme_email: <registration-email>

  # Secret value. Bootstrap redacts this from plans, errors, diagnostics, and
  # subprocess arguments.
  token: <cloudflare-dns-api-token>

Secret Handling

The actual file may contain secrets and must meet all of these requirements:

  • Store it under the ignored config/ directory.
  • Set mode 0600 before adding a token.
  • Commit only placeholder values in config-example/bootstrap.yaml.
  • Do not pass secrets through command arguments or environment variables.

Bootstrap refuses a group- or world-accessible configuration. Generated secret files are written atomically with mode 0600; plans display <redacted> rather than the token.

Address Discovery

The recommended value for network.expected_ipv4 is the reserved DHCP address. It lets bootstrap:plan verify address and DNS before the VM is created.

auto is a test-only alternative when the address cannot be reserved first. Bootstrap creates or resumes the VM, finds the single usable IPv4 address on the guest-agent interface matching the planned MAC, and stops for the operator to map the required names. Add the reported base domain, service names, and bootstrap-check name to the administrative host’s /etc/hosts, then rerun the same apply command. Bootstrap does not add mappings automatically or modify resolver configuration, and refuses ambiguous guest addresses. To add the documented temporary block explicitly, run:

task bootstrap:add-hosts NAME=docker-host ADDRESS=192.0.2.10 DOMAIN=example.com

For test-only manual mappings, use one exact named block so it can be removed later without touching unrelated entries:

# BEGIN infra docker-host temporary DNS
192.0.2.10 example.com home.example.com traefik.example.com bootstrap-check.example.com
# END infra docker-host temporary DNS

Replace docker-host with the lowercase VM hostname label. Cleanup is always explicit and runs with sudo; bootstrap never invokes it automatically:

task bootstrap:cleanup-hosts NAME=docker-host

The add helper validates the hostname, IPv4 address, and domain and refuses an existing or malformed named block. Cleanup refuses to modify /etc/hosts when the named block is absent, duplicated, incomplete, or malformed.

Implementation Map

ConfigurationGenerated target or checkExisting consumer
repositoryLocal revision validation and remote checkoutgit ls-remote, git clone
proxmox.ssh_targetPROXMOX_HOSTscripts/bootstrap/Taskfile.bootstrap.yaml
vm.idVMIDqm, vm/lib-common.sh
vm.nameVMNAME and ignored inventory hostnameCloud-init, Ansible, Docker host config
vm.ubuntu_versionUBUNTU_VERSIONvm/proxmox/create-ubuntu-cloud-vm.sh
VM compute/storage fieldsconfig/vm/proxmox/ubuntu-cloud.envvm/proxmox/create-ubuntu-cloud-vm.sh
vm.macVM_MAC and Proxmox net0qm create in the cloud-image script
vm.ssh_public_keyIgnored copied public key and inventory key pathsCloud-init and debian_base
network.expected_ipv4EXPECTED_IPV4 and ansible_hostCloud-init wait and ansible/apply-homelab.sh; auto uses the guest agent after provisioning
network.domainHost MYDOMAINTraefik and Homepage Compose files
network.dnsBase, service, and wildcard-probe lookupsPython preflight and HTTPS verification
host.docker_volumesAnsible host variable and host DOCKER_VOLUMESdebian_docker_host and Compose
host.timezoneHost TIMEZONETraefik and Homepage Compose files
tls.acme_emailHost ADMIN_EMAILTraefik ACME configuration
tls.tokenHost .env.traefik, mode 0600docker/security/traefik/traefik.yaml
deployment.servicesHost services.yamlscripts/labctl.py config apply

Generated local files:

config/
├── bootstrap.yaml
├── ansible/inventory/inventory.yaml
├── vm/proxmox/
│   ├── bootstrap-authorized-key.pub
│   └── ubuntu-cloud.env
└── docker/<hostname>/
    ├── .env
    ├── .env.traefik
    └── services.yaml

Process And Entry Points

The Python module keeps orchestration phases explicit and delegates existing operations rather than reimplementing them:

PhasePython functionExisting entry point
Load and validateload_config()PyYAML plus local validation
Resolve repository, ID, and MACresolve_plan()Git and read-only Proxmox commands
Validate target and DNSpreflight()SSH, Proxmox storage/bridge, system resolver
Render ignored filesrender_config()Existing env and inventory contracts
Provision or resume VMprovision_vm()task bootstrap:vm-preflight, vm-provision, vm-wait
Establish SSH trustestablish_ssh_trust()Proxmox guest agent and ssh-keyscan
Configure hostconfigure_host()ansible/apply-homelab.sh --limit <host>
Prepare servicesprepare_remote_repository()Git, task bootstrap:init-local-config, rsync
Deploydeploy_services()task docker:apply
Accept deploymentverify_services()Docker inspection, HTTPS checks, repeat apply

Public commands:

task bootstrap:init
task bootstrap:plan
task bootstrap:apply
task bootstrap:apply YES=1

bootstrap:vm-init, bootstrap:vm-preflight, bootstrap:vm-provision, bootstrap:vm-wait, and bootstrap:init-local-config are advanced/manual commands for recovery and debugging. Normal users should use bootstrap:init, then bootstrap:plan, then bootstrap:apply.

The same operations are directly available as:

python3 -m scripts.bootstrap --config config/bootstrap.yaml --plan
python3 -m scripts.bootstrap --config config/bootstrap.yaml --apply
python3 -m scripts.bootstrap --config config/bootstrap.yaml --apply --yes

Rerunning apply is the recovery mechanism. The renderer and Docker apply are idempotent. Provisioning resumes an existing VM only when its ID, name, MAC, disks, guest agent, and VM-specific cloud-init snippets match. It never deletes or replaces a conflicting or partially created resource.