Estimated reading time: 12 minutes
Table of contents
The Craftsman’s Dilemma
My server fortress is complete. After a series of meticulous projects, it stands as a testament to careful planning and execution – secure, fast, and resilient. But then, the professional’s dilemma strikes. What if the underlying Proxmox host suffered a catastrophic hardware failure? Or, what if I needed to spin up a second, identical staging server to test a major change before deploying it to production?
The thought of manually re-doing all those precise steps is not just daunting; it’s inefficient and dangerously prone to human error. A master craftsman can build one perfect item, but a master engineer builds a factory that can produce thousands of them flawlessly. For this reason, I knew it was time to evolve my setup. It was time to embrace the professional discipline of Infrastructure as Code (IaC) for my homelab.
My Checklist for an Infrastructure as Code Workflow
As is my custom, I defined a clear set of requirements for this new automated workflow. This wasn’t just about scripting a few commands; it was about adopting a professional pipeline for managing my entire server environment.
- Declarative: Most importantly, the tools must allow me to define the desired end state of my server, not just the sequence of commands to get there.
- Idempotent: Running the code a second, third, or hundredth time must result in the exact same state, without causing errors or unwanted changes.
- Version Controlled: The entire infrastructure definition – from the container’s hardware specs to the software configuration – must live in a Git repository.
- Separation of Concerns: The workflow should use the right tool for the right job: one tool to provision the “hardware” (the LXC container) and another to configure the “software” inside it.
The Solution: Terraform for Provisioning, Ansible for Configuration
After some consideration, I landed on the industry-standard combination for this task, which we – coinsidently 😉 – also use at my work. The roles are distinct and perfectly complementary. I like to think of it with a simple analogy: Terraform builds the house, and Ansible furnishes it.
- Terraform: This acts as the architect’s blueprint. It defines the LXC container itself – its OS template, CPU, RAM, and network settings – and tells Proxmox how to build it.
- Ansible: This acts as the interior decorator and installation crew. Once the house is built, Ansible goes inside to install the software, copy over my perfected configuration files, and enable the services.
This combination of tools provides a robust and professional approach to proxmox infrastructure as code.
A Note on Practice vs. Purpose:
While enterprise best practices often lean toward building pre-baked container images, I’ve intentionally taken a different path here. This project is a homelab showcase designed for exploration and experimentation. I’ve incorporated a wide range of Ansible capabilities – static files, dynamic templates, and inline configurations – specifically to demonstrate the breadth of what is possible within a single playbook. My goal is to keep things simple and comprehensible, treating each LXC as a freshly installed VM rather than setting up a complex image pipeline. It’s about learning the full potential of the tools in an accessible, “KISS” way for anyone following along.

TL;DR
In this guide, I show you how to achieve true proxmox infrastructure as code. I combine the power of Terraform to provision a new LXC container and Ansible to automatically furnish it, transforming my entire server fortress into a reproducible blueprint that deploys with a single command. For those who want to jump straight to the code, you can find all the files and scripts in my proxmox-iac GitHub repo:
https://github.com/ramonvanraaij/proxmox-iac
Please note that the GitHub repository contains the most up-to-date code (which I aim to maintain regularly); see the commits for changes I’ve made since the time of writing this blog post. The Terraform and Ansible code within this post may be outdated.
This is the project tree, excluding the files and templates:
.
|-- LICENSE.md
|-- README.md
|-- example_proxmox_api_secrets
|-- lemp
| |-- create_tfvars.sh
| |-- files/
| |-- get_next_id.sh
| |-- get_templates.sh
| |-- main.tf
| |-- outputs.tf
| |-- playbook-gitea.yml
| |-- playbook-github.yml
| |-- playbook.yml -> playbook-github.yml
| |-- templates/
| `-- variables.tf
`-- validate-pct-exec.sh
Step 1: Preparing the Control Node
Before I could write any code, I needed a dedicated, clean environment from which to run my automation tools. This “control node” is where Terraform and Ansible will live. For the operating system, I decided to go with Debian. I’ve spent a lot of time in Alpine Linux for my LEMP server and my SSH Jump Box, I use Arch as my daily driver, and at work, our environment is RHEL-based. With the recent release of Debian 13 “Trixie,” I saw a perfect opportunity to start using this new Debian release. It offers a different, incredibly stable ecosystem, and more importantly, it provides up-to-date, officially supported packages for both Terraform and Ansible right out of the box. For that reason, it was the perfect, pragmatic choice for the job.
I created a new, minimal Debian LXC container on my Proxmox host (created a sudo user and setup the firewall) and then ran the following steps inside it.
1. Install Core Packages
First, I updated the system and installed the prerequisites. Note that I’m also installing jq for our helper scripts, lsb-release to ensure the repository commands work reliably, gpg as I will need to add the HashiCorp GPG key and git as I will eventually push this to a repo.
# Update package lists and upgrade the system
sudo apt update && sudo apt upgrade -y
# Install prerequisites
sudo apt install -y wget gpg jq lsb-release git
With the base system ready, I could now install Terraform and Ansible.
# Add the HashiCorp GPG key
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
# Add the official HashiCorp repository for Terraform
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
# Update package lists again to include the new repository
sudo apt update
# Install Terraform and Ansible from the official repositories
sudo apt install -y terraform ansible
2. Setting Up SSH Key Pair and Nano Syntax Highlighting
This workflow relies heavily on SSH for communication. First, I needed to ensure my control node had its own SSH key pair.
# Generate a new ed25519 SSH key pair
ssh-keygen -t ed25519 -C "control-node@proxmox"
I accepted the default file location and opted for an empty passphrase for this key, as it will be used in an automated context.
Next, as a quality-of-life improvement, I set up syntax highlighting in my preferred text editor, nano. A clean, “Debian-native” approach is to use the excellent syntax files that come pre-packaged with nano on Debian. This already provides highlighting for Ansible’s YAML files and many others. However, I noticed that the default package doesn’t include syntax highlighting for Terraform. To fix this, I pulled the syntax file directly from Anthony Scopatz’ nanorc repository. This is the most reliable way to ensure I have the correct and most up-to-date highlighting rules.
# Create a directory for custom nano syntax files
mkdir -p ~/.nano/syntax/
# Download the hcl nanorc file
wget -P ~/.nano/syntax/ https://raw.githubusercontent.com/scopatz/nanorc/master/hcl.nanorc
Finally, I created a ~/.nanorc file to tell nano to include both the system-wide syntax files and my new, custom Terraform file.
# Create the main .nanorc file with includes
tee ~/.nanorc > /dev/null <<'EOF'
## Include the standard syntax definitions
include "/usr/share/nano/*.nanorc"
## Include my custom Terraform syntax
include "~/.nano/syntax/*.nanorc"
EOF
With these steps, I now have a dedicated control node ready to manage my proxmox infrastructure as code.
Step 2: Preparing Proxmox for Automation
Next, Terraform needs a way to securely authenticate with my Proxmox host. The best practice is to create a dedicated user with a specific role and a dedicated API token for it. I also need to create a ci user to install the openssh service on the newly created container as Alpine Linux does not have it installed by default and we will need it for Ansible.
1. Create a Dedicated User, Role and API Token
For those who prefer a fully scripted approach, all of these steps can be accomplished from the Proxmox host’s command line using the pveum tool.
# Run these commands on the Proxmox host as root
# Create the TerraformProv role with the correct permissions for Proxmox 9
pveum role add TerraformProv -privs "Pool.Audit Datastore.AllocateSpace Datastore.AllocateTemplate Datastore.Audit Pool.Allocate Sys.Audit Sys.Console Sys.Modify VM.Allocate VM.Audit VM.Clone VM.Config.CDROM VM.Config.Cloudinit VM.Config.CPU VM.Config.Disk VM.Config.HWType VM.Config.Memory VM.Config.Network VM.Config.Options VM.Migrate VM.PowerMgmt SDN.Use"
# Create the user and assign the role
pveum user add terraform-prov@pve
pveum acl modify / -user terraform-prov@pve -role TerraformProv
# Create the API token
pveum user token add terraform-prov@pve terraform-token --privsep 0
This sequence of commands achieves the exact same secure setup as the web interface, but in a scriptable, reproducible way.
2. Create a Dedicated CI User for Installing and Enabling the OpenSSH service
First, I’m creating the ci user and setup the .ssh directory and authorized_keys file with the correct ownership and give it sudo privileges to execute the pct exec command without giving a password.
# Run these commands on the Proxmox host as root
# Create the user with a password, we'll need it later to copy the SSH key.
adduser ci
# Create the .ssh directory and authorized_keys file
mkdir -p /home/ci/.ssh
touch /home/ci/.ssh/authorized_keys
# Set the correct ownership and permissions
chown -R ci:ci /home/ci/.ssh
chmod 700 /home/ci/.ssh
chmod 600 /home/ci/.ssh/authorized_keys
# Give the ci user sudo privileges
tee /etc/sudoers.d/ci > /dev/null <<'EOF'
# Allow the ci user to run pct without a password
ci ALL=(ALL) NOPASSWD: /usr/sbin/pct
EOF
Next, I’m making sure that the ci user can only execute sudo pct exec and the cat >> ~/.ssh/authorized_keys command when logging in via SSH, the last one is needed to add the SSH key.
# Run these commands on the Proxmox host as root
# Create the validate-pct-exec.sh script
tee /usr/local/bin/validate-pct-exec.sh > /dev/null <<'EOF'
#!/bin/sh
#
# This script is forced by sshd_config for the 'ci' user.
# It validates and executes the 'sudo pct exec'
# and 'cat >> ~/.ssh/authorized_keys' commands.
case "$SSH_ORIGINAL_COMMAND" in
"sudo pct exec "*)
# Using "eval exec" correctly processes the command string
# with its nested quotes, which is required for 'sh -c "..."'.
eval exec "$SSH_ORIGINAL_COMMAND"
;;
"cat >> ~/.ssh/authorized_keys"*)
# The original command is a signal. The real action is to
# read from this script's standard input and append to the file.
# The tilde (~) automatically expands to the correct user's home directory.
cat >> ~/.ssh/authorized_keys
;;
*)
# All other commands are rejected.
echo "Rejected" >&2
exit 1
;;
esac
EOF
# Make the script executable
chmod +x /usr/local/bin/validate-pct-exec.sh
Now I’m making sure that only SSH login is allowed for the ci user from my control node IP (192.168.0.111) on the Proxmox host and that the above script is executed when the user logs in via SSH.
# Run these commands on the Proxmox host as root
# Append "Match User" patterns to /etc/ssh/sshd_config
tee -a /etc/ssh/sshd_config <<EOF
# Deny the ci user to login when it's not logging in from IP 192.168.0.111
Match User ci Address *,!192.168.0.111
ForceCommand /bin/false
AllowTcpForwarding no
X11Forwarding no
# Restrict the ci user to execute the 'sudo pct exec' command
Match User ci Address 192.168.0.111
ForceCommand /usr/local/bin/validate-pct-exec.sh
EOF
Finally, I’m checking the sshd config and restarting the sshd service.
# Run these commands on the Proxmox host as root
# Check the sshd config
sshd -t
# Restart the sshd service
sudo systemctl restart sshd
With the ci user fully configured on my Proxmox host, the final step is to authorize my control node. A standard tool like ssh-copy-id will not work here, and that’s a good thing! It would be rejected by the ForceCommand I just put in place, confirming that my security is working as intended. The correct way to authorize the key, therefore, is to manually append it. I ran the following single command from my control node to securely add my public key to the ci user’s authorized_keys file on the Proxmox host.
# Run this from the control node to authorize its key on the Proxmox host
cat ~/.ssh/id_ed25519.pub | ssh ci@pve 'cat >> ~/.ssh/authorized_keys'
This crucial link now ensures Terraform can connect securely as the ci user to bootstrap the new container.
Step 3: Building the House – A Flexible Terraform Blueprint
A professional blueprint needs to be flexible. For that reason, I structured my code into separate files for variables and logic. This is a best practice that makes the code much easier to manage and reuse.
First, I created a new, nested directory structure on my control node. This allows me to keep the files for this specific LEMP server project separate from any other future projects I might automate.
# Create and enter a new project directory for the LEMP server
mkdir -p ~/proxmox-iac/lemp && cd ~/proxmox-iac/lemp
1. Defining the Inputs (variables.tf)
Next, using my favorite text editor, I created a new file named variables.tf. This file defines all the inputs for my module, including a new variable for the container’s root disk storage.
# Create the file with for example the nano editor
nano variables.tf
# variables.tf - Input variables for our Proxmox LXC module
variable "proxmox_host_ip" {
type = string
description = "The IP address of the Proxmox host for the CI provisioner."
}
variable "rootfs_storage" {
type = string
description = "The storage pool for the container's root disk (e.g., local-lvm)."
default = "local-lvm"
}
variable "root_password" {
type = string
description = "The root password for the LXC container. Set to null for a random password."
default = "a_very_secret_password!"
sensitive = true
}
variable "container_id" {
type = number
description = "The ID for the LXC container. If null, Proxmox will assign the next available ID."
default = "110"
validation {
condition = var.ip_prefix == null || (var.ip_prefix != null && var.container_id != null)
error_message = "If you specify an 'ip_prefix', you must also specify a 'container_id'."
}
}
variable "hostname" {
type = string
description = "The hostname for the new LXC container."
default = "lemp-iac"
}
variable "ip_prefix" {
type = string
description = "The IP prefix for a static IP (e.g., '192.168.0'), setting a container_id is required for this. If null, DHCP will be used."
default = "192.168.0"
}
variable "cidr_suffix" {
type = string
description = "The CIDR suffix for the network (e.g., '24' for /24)."
default = "24"
}
variable "gateway" {
type = string
description = "The network gateway IP address. Required for static IP configuration."
default = "192.168.0.1"
}
variable "target_node" {
type = string
description = "The name of the target Proxmox node."
}
variable "ostemplate" {
type = string
description = "The name of the LXC template to use (e.g., 'local:vztmpl/alpine-3.22-default_20250617_amd64.tar.xz')."
default = "local:vztmpl/alpine-3.22-default_20250617_amd64.tar.xz"
}
variable "ssh_public_key_path" {
type = string
description = "The path to the public SSH key to install in the container."
default = "~/.ssh/id_ed25519.pub"
}
A quick note on the default values in this file: for this guide, I’ve set them to my common defaults to ensure the blueprint works out of the box on the first run. However, in a true production workflow, a more robust and safer practice is to remove the default line entirely for variables like container_id and ip_prefix, or set their default to null. This makes these variables required. Consequently, if I ever forget to run my create_tfvars.sh wizard (see the Bonus section later on), Terraform will immediately stop and prompt me for the missing values, preventing an accidental deployment with the wrong configuration. It’s a simple change that adds another layer of foolproof safety to the process.
2. Writing the Main Blueprint (main.tf)
With the variables defined, I created another file, main.tf, to hold my main logic. You’ll notice in the code below that the provider "proxmox" {} block is now empty. This is a deliberate, clean approach. The Terraform provider is designed to automatically detect and use any environment variables that start with PM_ (like PM_API_URL and PM_API_TOKEN_SECRET), which we will set up in a later step. This keeps my secrets out of my code, which is a critical security best practice. I’ve updated it to include a crucial new block that explicitly defines where to place the container’s root filesystem. This is a common “gotcha” in Proxmox, as not all storage types can hold container disks.
# Create the file with for example the nano editor
nano main.tf
# main.tf - Main logic for creating the Proxmox LXC
terraform {
required_providers {
proxmox = {
source = "telmate/proxmox"
version = "3.0.2-rc07" # Using the required version
}
}
}
# Provider is configured using environment variables
provider "proxmox" {}
# Define the Alpine Linux LXC container
resource "proxmox_lxc" "lemp_iac" {
target_node = var.target_node
hostname = var.hostname
ostemplate = var.ostemplate
vmid = var.container_id
unprivileged = true
password = var.root_password
start = true
onboot = true
features {
nesting = true
}
rootfs {
storage = var.rootfs_storage
size = "8G"
}
cores = 2
memory = 2048
swap = 512
network {
name = "eth0"
bridge = "vmbr0"
ip = var.ip_prefix != null ? "${var.ip_prefix}.${var.container_id}/${var.cidr_suffix}" : "dhcp"
gw = var.gateway
}
ssh_public_keys = file(var.ssh_public_key_path)
# Step 1: Install OpenSSH inside the container using Proxmox's pct exec command.
provisioner "local-exec" {
command = <<-EOT
sleep 15
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ci@${var.proxmox_host_ip} \
'sudo pct exec ${self.vmid} -- sh -c "apk update && apk upgrade --available && apk add openssh && rc-update add sshd default && service sshd start"'
EOT
}
# Step 2: After SSH is installed, run Ansible from the local machine.
provisioner "local-exec" {
# Use the split() function here to provide a clean IP to Ansible
# ANSIBLE_HOST_KEY_CHECKING=False to bypass the SSH security prompt
command = "ANSIBLE_HOST_KEY_CHECKING=False ansible-playbook --inventory ${split("/", self.network[0].ip)[0]}, --user root playbook.yml"
}
}
3. Defining the Outputs (outputs.tf)
To make automation easier, I created an outputs.tf file.
# Create the file with for example the nano editor
nano outputs.tf
# outputs.tf - Define the output values
output "proxmox_host_ip" {
description = "The IP address of the Proxmox host for the CI provisioner."
value = var.proxmox_host_ip
}
output "container_ip" {
description = "The IP address of the LEMP LXC container."
value = split("/", proxmox_lxc.lemp_iac.network[0].ip)[0]
}
output "container_root_password" {
description = "The root password set for the new LXC container."
value = var.root_password
sensitive = true
}
After a successful terraform apply, I can now run, as an example, terraform output -raw container_root_password to securely retrieve the password that was set, which is a very convenient feature.
4. Creating My Configuration (terraform.tfvars)
Finally, I created a terraform.tfvars file. This is the “control panel” where I set the values for a specific build.
# Create the file with for example the nano editor
nano terraform.tfvars
# terraform.tfvars - Core configuration for the build
target_node = "pve"
# Note: ostemplate and other values can be set by the create_tfvars.sh helper script.
Step 4: Furnishing the Rooms – A Comprehensive Ansible Playbook
With the house built, it’s time for Ansible to furnish it. The playbook.yml file, which I’ll show in a moment, is the master instruction manual for configuring the entire server. However, it’s not a standalone script; it relies on a local collection of configuration files and templates that I’ve perfected throughout my previous blog posts.
Preparing the Local Ansible Files
Before I could run the playbook, I needed to have these local files ready. For this guide, I assume you are in the ~/proxmox-iac/lemp directory and have cloned my proxmox-iac GitHub repository. This will provide two crucial subdirectories:
files/: This directory contains static files that I want to copy to the server as-is.templates/: This is where the real magic happens. This directory holds.j2(Jinja2) templates, which are dynamic blueprints that Ansible populates with variables before placing them on the server.
I’ve put a lot of my “secret sauce” into these templates, as they are the codified blueprints of the entire fortress. A few of the most interesting ones are:
nftables.nft.j2: This isn’t just a static firewall. It’s a dynamic ruleset that programmatically includes the CrowdSec blocklists and will even open the Monit port (2812) only if I’ve set amonit_enabledvariable totrue.lemp.monit.j2: Similarly, this template automatically builds a complete Monit configuration file. It’s smart enough to add monitoring checks for Nginx, PHP-FPM, MariaDB, and CrowdSec, but only for the services that are actually enabled in my setup.proxy-ips.conf.j2: This template perfectly codifies the complex logic I worked out for handling reverse proxies. It intelligently adds the correctset_real_ip_fromandreal_ip_headerdirectives based on whether I’m using just Nginx Proxy Manager or the full Cloudflare-NPM chain.
With these powerful templates in place (by downloading them from the repository), the playbook itself becomes a clean and simple set of instructions that brings the entire fortress to life.
The Ansible Playbook (playbook.yml)
I created my playbook.yml file in the same ~/proxmox-iac/lemp directory. This playbook codifies all the manual steps from my previous guides into a single, automated run. As the playbook ended up beeing 810 lines long, I’m not going to show it in the this blog post, it can be downloaded here.
# Download the playbook
curl -o playbook.yml https://github.com/ramonvanraaij/proxmox-iac/blob/main/lemp/playbook-github.yml
The variables at the top of the playbook can/should be changed.
Step 5: Securely Authenticate with the Proxmox API
First, I need a way to securely authenticate with the Proxmox API. The best practice is to store these credentials in a separate, secure file and never commit them to a Git repository.
I created a file in my home directory for this purpose:
# Create the file with for example the nano editor
nano ~/.proxmox_api_secrets
readonly export PM_API_URL="https://192.168.0.99:8006/api2/json"
readonly export PM_API_TOKEN_ID="terraform-prov@pve!terraform-token"
readonly export PM_API_TOKEN_SECRET="MY_VERY_SECRET_TOKEN"
readonly export PM_TLS_INSECURE="true"
And making it read/write for only the user:
chmod 600 ~/.proxmox_api_secrets
A Critical Security Note: The ~/.proxmox_api_secrets file now contains the keys to my kingdom. It is absolutely essential that this file is never committed to a public (or even private) Git repository. To prevent this, I immediately added ~/.proxmox_api_secrets to my global .gitignore file. This is a non-negotiable step for maintaining a secure workflow.
Step 6: The Payoff: The One-Command Deployment
With the provisioner block in my main.tf file, Terraform will automatically trigger my Ansible playbook after the container is created. Now, the entire workflow is unified.
# Load the variables
source ~/.proxmox_api_secrets
# Initialize Terraform and upgrade the provider if needed
terraform init -upgrade
# Apply the plan to build and provision the container
terraform apply
This sequence now orchestrates the entire deployment, resulting in a fully hardened, ready-to-use server in under 4 minutes. Only thing I would need to do now is either setup a new website or restore my website data from my restic backup to have My Ultimate LEMP Server up and running (again).

Bonus: Automating the Blueprint’s Inputs with a Setup Wizard
To make the process even smoother, I wrote a small collection of helper scripts to fully automate the configuration. For simplicity, I’ll create these inside my lemp project directory, but in a larger setup, you might move them to a shared location.
Before running my helper scripts, I just need the variables into my shell session with the command:
source ~/.proxmox_api_secrets.
Next, I created three scripts to find the next available container ID and to list all available LXC templates. I then wrote a final “wizard” script that uses them to intelligently suggest defaults and guide me through creating a perfect terraform.tfvars file, now including a prompt for the essential root disk storage location.
# Download the ID finder script
curl -o get_next_id.sh https://raw.githubusercontent.com/ramonvanraaij/proxmox-iac/main/lemp/get_next_id.sh
# Make it executable
chmod +x get_next_id.sh
# Download the template lister script
curl -o get_templates.sh https://raw.githubusercontent.com/ramonvanraaij/proxmox-iac/main/lemp/get_next_id.sh
# Make it executable
chmod +x get_templates.sh
# Download the interactive setup script
curl -o create_tfvars.sh https://raw.githubusercontent.com/ramonvanraaij/proxmox-iac/main/lemp/create_tfvars.sh
# Make it executable
chmod +x create_tfvars.sh
Now, before every deployment, I can simply run source ~/.proxmox_api_secrets followed by ./create_tfvars.sh, answer a few questions, and have a perfect configuration file ready for Terraform.
Final Thoughts: From a Fortress to a Blueprint
This project achieves a significant milestone in my server journey. The fortress is no longer just a hand-crafted machine; it is now a version-controlled blueprint that can be summoned, destroyed, and perfectly replicated at will. This, for me, is the ultimate expression of the pragmatic architect’s philosophy. It transforms a manual craft into a reproducible science, ensuring my perfect server can be rebuilt at any time, on any hardware, with a single command. While this completes the foundational architecture of the server itself, it also opens the door to a whole new world of possibilities. With a reproducible fortress at my command, the real fun can begin: deploying and managing the services that will live inside it.
Buy me a coffee 🙂
If you found this post helpful, informative, or if it saved or made you some money, consider buying me a coffee. Your support means a lot and motivates me to keep writing.
You can do so via bunq.me (bunq, iDeal, Bankcontact and Credit- or Debit cards) or PayPal (PayPal and Credit- or Debit cards). Thank you!
Disclaimer
The blog posts, guides, and scripts provided on this website are for informational and educational purposes only. They are provided “as is” and without any warranty of any kind, either express or implied, including, but not limited to, the implied warranties of merchantability, fitness for a particular purpose, or non-infringement.
By using the information or scripts from this blog, you agree that I am not liable for any direct, indirect, incidental, consequential, or any other damages or losses arising from the use of or inability to use the information, scripts, or instructions contained herein. You assume full responsibility for any and all risks associated with the use of this content.
The blog posts, guides, and pages may contain referral/affiliate links. If you make a purchase through these links, I may receive a commission at no additional cost to you.
At the last commands for step 2, did you mean to have the control node’s ssh public key get added to its own authorized_keys list? I suspect you meant to have ssh ci@ so it adds its public key to the Proxmox host.
Good catch! I see that I wrongfully typed the control node’s IP in the last command of step 2 in this post. It should be the IP of the Proxmox host. So, yes, the public key of the control node is to be added to the ci user on the Proxmox host, I’ll change that in the blog post.