Skip to content

Configuration reference

Location: /usr/local/bin/vm_autoscale/config.yaml. The path is hardcoded in autoscale.py's main().

The file is read once at startup. There is no reload signal — restart the service after editing.

This file holds credentials

ssh_password, smtp_password and app_token are stored in plain text. The file must be root-owned and mode 600. Verify with ls -l, and see the hardening guide.

Required sections

Startup fails unless all four of these are present:

scaling_thresholds
scaling_limits
proxmox_hosts
virtual_machines

Missing any of them:

CRITICAL Failed to start VM Autoscaler:
         Missing required configuration sections: scaling_limits

Note the validator only checks that the keys exist — not that their contents make sense.

scaling_thresholds

Usage percentages that trigger a scaling decision. Global; not overridable per VM.

yaml
scaling_thresholds:
  cpu:
    high: 80
    low: 20
  ram:
    high: 85
    low: 25
KeyTypeRequiredNotes
cpu.highnumberwhen cpu_scaling is usedScale up above this
cpu.lownumberwhen cpu_scaling is usedScale down below this
ram.highnumberwhen ram_scaling is usedScale up above this
ram.lownumberwhen ram_scaling is usedScale down below this

Comparisons are strict, so a value exactly on a threshold does nothing. Keep the gap between low and high wide — it is your only protection against oscillation.

scaling_limits

Hard bounds on what any VM may be scaled to. Global.

yaml
scaling_limits:
  min_cores: 1
  max_cores: 8
  min_ram_mb: 1024
  max_ram_mb: 16384
KeyTypeDefaultNotes
min_coresint1
max_coresint8
min_ram_mbint512Keep at 1024 or above — NUMA misbehaves below 1 GB
max_ram_mbint16384

These are the defaults for every VM. A VM may override any of them in its own entry, so a 2-core web server and a 16-core database can share one instance:

yaml
virtual_machines:
  - vm_id: 102
    scaling_limits:
      max_cores: 16
      max_ram_mb: 32768

Resolution order: the VM's own block, then this section, then a flat top-level key (older layout), then the built-in default.

Behaviour change

Older versions read these values under names that do not exist in this file, so the defaults above were enforced regardless of what you configured. If you are upgrading, your limits are about to take effect for the first time. Flat top-level min_cores / max_cores / min_ram / max_ram keys are still accepted as a fallback for legacy configs.

Timing

yaml
check_interval: 300
scale_cooldown: 300
KeyTypeDefaultNotes
check_intervalint (s)300Time between polling cycles
scale_cooldownint (s)300Minimum time between two changes to the same resource on the same VM. Not present in the shipped config.yaml — add it if you want a value other than the default

CPU and RAM have independent cooldown timers. Only an actual change starts one. Timers are in memory and are lost on restart. Setting scale_cooldown below check_interval has no effect.

proxmox_hosts

yaml
proxmox_hosts:
  - name: pve1
    host: 192.168.1.10
    ssh_user: root
    ssh_password: your_password_here
    ssh_key: /root/.ssh/id_rsa
    ssh_port: 22
KeyTypeRequiredNotes
namestringyesReferenced by virtual_machines[].proxmox_host; must match exactly
hoststringyesHostname or IP
ssh_userstringyesNeeds privileges to run qm set — in practice root
ssh_passwordstringone of the twoTakes precedence over ssh_key when both are set
ssh_keypathone of the twoEd25519, ECDSA, RSA and DSS all supported. The key must be unencrypted — there is no passphrase option
ssh_portintnoDefaults to 22. Earlier versions indexed this key directly, so omitting it raised KeyError and every VM on that host failed

Two footguns in the shipped example

The example config fills in both ssh_password and ssh_key with placeholders. Password wins, so a half-edited config authenticates with the literal string your_password_here. Also, host2 in the example omits ssh_port — and the code reads that key unconditionally, so the host raises a KeyError. Set ssh_port on every host.

Connections are retried five times with exponential backoff (1, 2, 4, 8, 16 s). Authentication failures are not retried.

virtual_machines

yaml
virtual_machines:
  - vm_id: 101
    proxmox_host: pve1
    scaling_enabled: true
    cpu_scaling: true
    ram_scaling: true
KeyTypeRequiredNotes
vm_idint or stringyesProxmox VMID. Both types work; strings are used as-is in shell commands
proxmox_hoststringyesMust match a proxmox_hosts[].name. A mismatch silently skips the VM
scaling_enabledboolnoDefaults to false — omitting it means the VM is never processed
cpu_scalingboolnoDefaults to false
ram_scalingboolnoDefaults to false
thresholdsmapnoPer-VM overrides of scaling_thresholds. Flat (cpu_high, cpu_low, ram_high, ram_low) or nested (cpu: { high, low }). Any bound you omit keeps the global value
scaling_limitsmapnoPer-VM overrides of the global scaling_limits. Same keys; any you omit keep the global value

Validation

The whole file is validated at startup: types, ranges, enumerations, cross-field consistency and referential integrity. Every problem is reported at once, one per line, and the service refuses to start:

CRITICAL Refusing to start. 3 configuration problem(s) found:
  - scaling_thresholds.cpu.high: expected a value between 0 and 100, got 150
  - virtual_machines[0].proxmox_host: 'pve-typo' does not match any
    proxmox_hosts name (pve1, pve2)
  - proxmox_hosts[1]: needs either ssh_password or ssh_key

An unknown key is a warning, not an error — rejecting outright would break configurations carrying a key from a newer version — but it is reported by path, so a typo stops being invisible:

WARNING Configuration: scaling_limits.max_ram: unknown key; it is not read by
        anything and will have no effect

This is the fix for a whole class of defect

scaling_limits, per-VM thresholds, ssh_port and per-VM limits each shipped broken in the same way: a key that was written, documented and never read, with no error anywhere. That is what validation exists to make impossible.

dry_run

yaml
dry_run: false
TypeDefaultNotes
boolfalseEvaluate everything, change nothing

No command that touches a VM is issued — hotplug auto-configuration included. Reads still happen, so the evaluation is real; the cooldown still applies, so the cadence in the log is the cadence you would get. Notifications are prefixed [DRY RUN] and billing records nothing. See operations.

metrics

yaml
metrics:
  enabled: false
  bind: 127.0.0.1
  port: 9808
  path: /metrics
KeyTypeDefaultNotes
enabledboolfalse
bindstring127.0.0.1
portint9808
pathstring/metrics

No authentication

The endpoint has none, and the series name your nodes and VMIDs. Keep it on localhost unless something in front of it authenticates. A bind failure is logged and the service continues without metrics.

Full metric list: operations.

Scaling behaviour

yaml
scale_down_after_cycles: 2
notification_dedup_seconds: 900
KeyTypeDefaultNotes
scale_down_after_cyclesint2Consecutive readings below the low threshold required before shrinking
notification_dedup_secondsint900Suppress identical notifications for this long; 0 disables

Growing and shrinking are not symmetric

Adding capacity fails safe. Reclaiming memory from a guest that is using it drives it into swap or to the OOM killer, and a vCPU unplug may simply be refused. A shrink therefore has to be sustained: any reading back inside the dead band resets the streak. Growth still acts on the first reading.

Deduplication collapses messages that differ only in their measured values, so CPU: 91.2% and CPU: 93.7% count as one event — but node identifiers are preserved, so pve1 unreachable and pve2 unreachable remain distinct. When suppression ends, the log states how many messages were withheld.

SSH host key verification

yaml
ssh_host_key_policy: accept-new
ssh_known_hosts: /etc/vm_autoscale/known_hosts
KeyTypeDefaultNotes
ssh_host_key_policystringaccept-newaccept-new, strict or auto
ssh_known_hostspath/etc/vm_autoscale/known_hostsCreated mode 600 in a 700 directory if absent
PolicyBehaviour
accept-newTrust a node the first time it is seen, record its key, and refuse to connect if that key ever changes. Equivalent to ssh -o StrictHostKeyChecking=accept-new
strictOnly connect to nodes already listed in ssh_known_hosts. Pre-populate it with ssh-keyscan
autoAccept any key, every time, and record nothing. No protection whatsoever

auto was the old behaviour

Before this was configurable the client used an auto-add policy with no known_hosts file loaded or saved, so every connection accepted whatever key it was offered. auto reproduces that and exists only as an escape hatch. A host key mismatch under the other two policies is fatal and is not retried — see the hardening guide.

host_limits

yaml
host_limits:
  max_host_cpu_percent: 90
  max_host_ram_percent: 90
KeyTypeRequiredNotes
max_host_cpu_percentnumberyesNo default; a missing key raises KeyError per VM
max_host_ram_percentnumberyesSame

Both gates block scale-down as well as scale-up. See host safety limits.

auto_configure_hotplug

yaml
auto_configure_hotplug: true
TypeDefaultNotes
booltrueWhen on, the service issues qm set -hotplug cpu,memory,network,disk,usb -numa 1 on guests missing them

Runs once per VM per service lifetime. NUMA changes need a guest reboot. See hotplug and NUMA.

logging

yaml
logging:
  level: INFO
  log_file: /var/log/vm_autoscale.log
KeyTypeDefaultNotes
levelstringINFO
log_filepath/var/log/vm_autoscale.log

logging_config.json overrides this

If /usr/local/bin/vm_autoscale/logging_config.json exists it is loaded via dictConfig and this whole section is ignored. Since the installer ships that file, the logging block in config.yaml is inert in a default installation. Edit the JSON instead — see operations.

gotify

yaml
gotify:
  enabled: false
  server_url: https://gotify.example.com
  app_token: your_gotify_app_token_here
  priority: 5
KeyTypeRequiredNotes
enabledboolyes
server_urlurlwhen enabledTrailing slash stripped automatically
app_tokenstringwhen enabledApplication token
priorityintnoDefault 5; per-event priorities override it

Validated at startup. See notifications.

alerts (SMTP)

yaml
alerts:
  email_enabled: false
  email_recipient: admin@example.com
  smtp_server: smtp.example.com
  smtp_port: 587
  smtp_user: your_smtp_user
  smtp_password: your_smtp_password
KeyTypeRequiredNotes
email_enabledboolyes
smtp_serverstringwhen enabled
smtp_userstringwhen enabledAlso the From address
email_recipientstring or listwhen enabled
smtp_portintnoDefault 587
smtp_passwordstringnoEmpty string skips login()

starttls() is always called. See notifications.

billing

yaml
billing:
  enabled: false
  billing_period_days: 30
  cost_per_cpu_core_per_hour: 0.01
  cost_per_gb_ram_per_hour: 0.005
  csv_output_dir: /var/log/vm_autoscale/billing/
  webhook_script: ""
  webhook_url: ""
KeyTypeDefaultNotes
enabledboolfalseOnly spec recording is automatic
billing_period_daysint30
cost_per_cpu_core_per_hourfloat0.01
cost_per_gb_ram_per_hourfloat0.005
csv_output_dirpath/var/log/vm_autoscale/billing/Created at startup; holds billing_data.json
webhook_scriptpath""Only fires from report generation
webhook_urlurl""Only fires from report generation

See billing tracking for what is and is not automatic.

Complete example

yaml
scaling_thresholds:
  cpu: { high: 80, low: 20 }
  ram: { high: 85, low: 25 }

scaling_limits:
  min_cores: 1
  max_cores: 8
  min_ram_mb: 1024
  max_ram_mb: 16384

check_interval: 300
scale_cooldown: 600
auto_configure_hotplug: false

proxmox_hosts:
  - name: pve1
    host: 10.0.0.11
    ssh_user: root
    ssh_key: /root/.ssh/vm_autoscale_rsa
    ssh_port: 22
  - name: pve2
    host: 10.0.0.12
    ssh_user: root
    ssh_key: /root/.ssh/vm_autoscale_rsa
    ssh_port: 22

virtual_machines:
  - { vm_id: 101, proxmox_host: pve1, scaling_enabled: true,  cpu_scaling: true,  ram_scaling: true  }
  - { vm_id: 102, proxmox_host: pve1, scaling_enabled: true,  cpu_scaling: true,  ram_scaling: false }
  - { vm_id: 201, proxmox_host: pve2, scaling_enabled: false, cpu_scaling: true,  ram_scaling: true  }

host_limits:
  max_host_cpu_percent: 85
  max_host_ram_percent: 90

logging:
  level: INFO
  log_file: /var/log/vm_autoscale.log

gotify:
  enabled: true
  server_url: https://gotify.internal.example
  app_token: REPLACE_ME
  priority: 5

alerts:
  email_enabled: false

billing:
  enabled: false

Validating before restart

bash
python3 -c "import yaml; yaml.safe_load(open('/usr/local/bin/vm_autoscale/config.yaml'))" \
  && echo "YAML OK"

This catches syntax errors only. Semantic problems — a proxmox_host that matches nothing, a missing ssh_port — surface at runtime.