Home Lab project
Proxmox Homelab
Personal

Proxmox Homelab

Dec 2023
Table of Contents

Overview

This project represents my personal homelab infrastructure, built on Proxmox VE. It is designed to be fully reproducible and manageable through code, leveraging Terraform for infrastructure provisioning and Ansible for configuration management.

The goal is to create a robust, self-healing environment that hosts various services while serving as a playground for experimenting with DevOps practices.

Here’s a high-level overview of the overall architecture: Homelab Overview

Tech Stack

TechnologyDescription
Proxmox VEHomelab hypervisor for running VMs and Containers.
TerraformInfrastructure as Code tool used to provision VMs and LXC containers.
Cloud-InitIndustry standard for early initialization of cloud instances (Ubuntu Server).
AnsibleConfiguration management tool for automating software installation and configuration.
Ubuntu ServerThe base operating system for all Virtual Machines and LXC containers.
DockerPlatform for developing, shipping, and running applications in containers.
TailscaleZero config VPN for secure private networking between devices.
OCIOracle Cloud Infrastructure. Used as a public entry point for homelab services.
CloudflareDNS management for deployed services.
TraefikThe edge router handling SSL termination and traffic routing on CloudVM.
CaddyLocal reverse proxy for internal routing and service discovery.
PangolinIdentity-aware access control and tunnel management.
CrowdSecCollaborative Intrusion Prevention System (IPS) for threat detection.

Core Principles

Immutable Infrastructure

The homelab is built on the principle of Immutable Infrastructure. This means that once a server (VM or Container) is deployed, it is never modified manually. If a configuration change is needed, the infrastructure code (Terraform/Ansible) is updated, and the resource is either re-provisioned or re-configured automatically. This eliminates “configuration drift” where servers become unique “snowflakes” over time.

Bootstrap Once Strategy

The Bootstrap Once Strategy applies to the Proxmox Hypervisor itself. By using a custom-built ISO, the physical server is fully configured during the initial installation. It sets up networking, storage, user access, and SSH keys automatically. Once the OS is installed, the only manual step required is generating a Proxmox API token to authorize Terraform. After that, the system is immediately ready for automated provisioning.

Bootstrap Once Strategy

Each Service

Pangolin Reverse Proxy Server

The secure gateway for the Public Playground. It handles all incoming public traffic, managing SSL certificates and routing requests to the appropriate services while blocking malicious actors.

For a deep dive into its architecture and security features, check out the dedicated post: Pangolin Reverse Proxy Server

Public Playground Server

A publicly accessible VM for hosting experimental projects and demos.

Services:

NewtA client service that securely connects to the Pangolin central controller.
YoroshikuHealth check application.
CaddyReverse proxy handling public traffic routed via Pangolin.

Security:

Pangolin ProtectionPublic access is strictly controlled by the Pangolin Reverse Proxy, which handles authentication and traffic filtering before it ever reaches this server.
WireguardInternal communication between Newt and Pangolin is encrypted over a Wireguard tunnel, ensuring no exposed ports on the public internet.

Private Playground Server

An internal development environment for testing services or personal projects.

Services:

YoroshikuHealth check application.
CaddyInternal reverse proxy.

Access:

Tailscale VPNThis server has NO public ingress. It is only accessible to devices authenticated on my private Tailscale network, making it invisible to the public internet.

Media Server

A dedicated VM for media streaming.

Media File Server

A lightweight LXC container serving as a NAS for media files.

Homelab Repository Directory Structure

The project is organized as a monorepo, with clear separation of concerns for each stage of the infrastructure lifecycle. From building the base OS image to deploying application containers, every component has its dedicated place.

homelab/
├── custom-iso/                 # Custom ISO builder
├── terraform/                  # Infrastructure as Code
├── ansible/                    # Configuration as Code
├── key-management/             # SSH key management
└── docker-services/            # Dockerized services

Directory Structure

Custom ISO

Purpose: Create pre-configured Proxmox VE installation media

custom-iso/
├── Makefile                    # Build automation commands
├── config/                     # Configuration templates
│   ├── installation-config.example.sh
│   └── build-config.example.sh
├── scripts/                    # Build automation
│   ├── build-custom-iso.sh     # Main ISO builder
│   ├── generate-config.sh      # Config generator
│   └── validate-answer.sh      # Configuration validator
├── templates/                  # Answer file templates
│   └── auto.toml.template      # Proxmox answer file
└── lib/                        # Shared utilities
    └── common.sh               # Common functions

Key Responsibilities:

Terraform

Purpose: Declarative infrastructure management

terraform/
├── Makefile                    # Multi-service orchestration
├── services/                   # Service-based architecture
│   ├── iam/                    # Identity & Access Management
│   │   ├── main.tf             # User/role/token resources
│   │   └── iam.auto.tfvars     # IAM configuration
│   └── compute/                # Compute workloads
│       ├── main.tf             # VM/LXC resources
│       ├── ubuntu-vms.auto.tfvars
│       └── lxc-containers.auto.tfvars
├── modules/                    # Reusable components
│   ├── vm/                     # VM resource module
│   ├── lxc/                    # Container module
│   ├── vm-cloudinit/           # Cloud-init VM module
│   └── users/                  # User management module
└── scripts/                    # Utility scripts
    ├── create-data-disk.sh
    └── extract-secrets.sh

Key Responsibilities:

Ansible

Purpose: Post-deployment configuration and management

ansible/
├── Makefile                    # Automation commands
├── playbooks/                  # Configuration playbooks
│   ├── hypervisors/            # Proxmox management
│   │   ├── configure-pve.yml   # Hypervisor config
│   │   ├── pve-post-install.yml
│   │   └── tailscale-install.yml
│   └── lxc/                    # Container setup
│       └── media-server-setup.yml
├── roles/                      # Reusable roles
│   ├── lxc_container/          # Container configuration
│   │   ├── tasks/main.yml      # Setup tasks
│   │   ├── templates/smb.conf.j2
│   │   └── files/mike-sudoers
│   └── pve_post_install/       # Hypervisor setup
├── inventory/                  # Target definitions
│   ├── hosts.yml               # Host inventory
│   └── group_vars/             # Group configurations
│       ├── all/common.yml
│       └── proxmox_hypervisors/main.yml
└── maintenance/               # Maintenance tools
    └── pve_post_install/
        └── compare-with-upstream.sh

Key Responsibilities:

Key Management

Purpose: Centralized SSH key management

key-management/
├── Makefile                    # Key management commands
├── infrastructure/             # Infrastructure keys
│   ├── id_rsa                  # Private key (gitignored)
│   └── id_rsa.pub              # Public key
├── admin/                      # Personal access keys
│   ├── id_rsa                  # Private key (gitignored)
│   └── id_rsa.pub              # Public key
├── scripts/                    # Management scripts
│   ├── setup.sh                # Key generation
│   ├── update-ansible-vault.sh
│   └── ssh-homelab.sh          # Connection helper
└── README.md                   # Documentation

Key Responsibilities:

Docker Services

Purpose: Containerized application deployment

docker-services/
├── jellyfin/                   # Media server
│   ├── compose.yml             # Docker Compose config
│   └── README.md
├── oci-reverse-proxy/          # Reverse proxy
│   └── pangolin/               # Proxy configuration
│       ├── docker-compose.yml
│       └── config/             # Traefik & Crowdsec
├── pve-110-private-playground/ # Development environment
│   ├── start-services.sh
│   ├── caddy/                  # Reverse proxy
│   └── yoroshiku/              # Health check app
└── pve-111-public-playground/  # Public services
    ├── start-services.sh
    ├── caddy/                  # Reverse proxy
    ├── newt/                   # API service
    └── yoroshiku/              # Health check app

Key Responsibilities:

Key Workflows

Automated Deployment with GitHub Deploy Keys

To enable seamless updates for the Docker services, the infrastructure uses a “Pull” model where the VM itself fetches the latest code from GitHub. This is achieved securely using GitHub Deploy Keys.

During the VM’s provisioning phase (via Cloud-Init), it generates a unique SSH key pair. It then uses a temporary Personal Access Token to upload the public key to the GitHub repository as a “Deploy Key”. This key grants read-only access specifically to that repository, allowing the VM to git clone or git pull the latest Docker Compose configuration without exposing a master credential.

Automated Deployment with GitHub Deploy Keys

This process is fully automated within the Cloud-Init user data:

############ GitHub Setup ############
# Automated GitHub deploy key setup and repository cloning
if [ -n "${GITHUB_REPO_URL}" ]; then
  echo "Setting up GitHub repository access..."

  # 1. Create .ssh directory
  mkdir -p /home/${USER}/.ssh

  # 2. Generate SSH key on the VM (if it doesn't exist)
  if [ ! -f /home/${USER}/.ssh/github_deploy_key ]; then
    echo "Generating SSH key for GitHub..."
    ssh-keygen -t ed25519 -f /home/${USER}/.ssh/github_deploy_key -N "" -C "deploy-key"
    chmod 600 /home/${USER}/.ssh/github_deploy_key
    chmod 644 /home/${USER}/.ssh/github_deploy_key.pub
  fi

  # ... (SSH Config Setup) ...

  # 4. Create deploy key via GitHub API (if token is provided)
  if [ -n "${GITHUB_PERSONAL_TOKEN}" ] && [ -n "${GITHUB_REPO_OWNER}" ] && [ -n "${GITHUB_REPO_NAME}" ]; then
    echo "Creating GitHub deploy key via API..."
    PUB_KEY=$(cat /home/${USER}/.ssh/github_deploy_key.pub)

    # Try to create the deploy key (ignore if it already exists)
    curl -X POST \
      -H "Authorization: token ${GITHUB_PERSONAL_TOKEN}" \
      -H "Accept: application/vnd.github+json" \
      -H "X-GitHub-Api-Version: 2022-11-28" \
      "https://api.github.com/repos/${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}/keys" \
      -d "{\"title\":\"deploy-key-$(date +%Y%m%d)\",\"key\":\"$PUB_KEY\",\"read_only\":false}" \
      || echo "Deploy key might already exist, continuing..."
  fi
fiterraform/.../public-playground-server.yaml

SSH Key Lifecycle

SSH Key Lifecycle

The SSH key management is designed to be secure and automated. We separate “Infrastructure Keys” (used by Terraform/Ansible) from “Admin Keys” (used by humans). These keys are injected into the VMs via Terraform variables.

variable "vm_provisioning_secrets" {
  description = "Per-VM secrets to merge with cloud_vms configuration (set in secrets.auto.tfvars)"
  type = map(object({
    cloudinit_password = optional(string, "")
    cloudinit_ssh_keys = optional(list(string), [])
    # ... other secrets
  }))
  sensitive = true
  default   = {}
}terraform/services/compute/variables.tf

These secrets are populated in a secrets.auto.tfvars file which is gitignored to prevent accidental exposure:

vm_provisioning_secrets = {
  "public-playground" = {
    cloudinit_password = "secure-password-here"
    cloudinit_ssh_keys = [
      "ssh-ed25519 AAAAC3NzaC... user@laptop",
      "ssh-ed25519 AAAAC3NzaC... ansible@controller"
    ]
  }
}terraform/services/compute/secrets.auto.tfvars

The VM module logic ensures that every VM gets the correct set of keys. It concatenates the specific keys defined for that VM (from vm_provisioning_secrets) with the global “Infrastructure” and “Admin” keys. This guarantees that both the automation tools and the administrator always have access, regardless of the specific VM configuration.

# SSH keys are injected during VM creation
dynamic "initialization" {
  for_each = var.cloudinit_ssh_keys != null ? [true] : []
  content {
    user_account {
      # Combine multiple SSH keys
      keys = concat(
        var.cloudinit_ssh_keys,
        var.infrastructure_ssh_key != "" ? [var.infrastructure_ssh_key] : [],
        var.admin_ssh_key != "" ? [var.admin_ssh_key] : []
      )
    }
  }
}terraform/modules/vm/main.tf

For more advanced use cases where we use custom Cloud-Init scripts (like the Playground servers), we inject these keys directly into the user-data template. This allows the bash scripts inside the template to reference ${INFRASTRUCTURE_PUBLIC_KEY} and ${ADMIN_PUBLIC_KEY} as variables, keeping the scripts clean and generic.

# SSH keys embedded in cloud-init user data
resource "proxmox_virtual_environment_file" "cloud_init_user_data" {
  content_type = "snippets"
  source_raw {
    data = templatefile("${path.module}/${each.value.user_data_file}", {
      # SSH keys from key-management system
      INFRASTRUCTURE_PUBLIC_KEY = local.infrastructure_public_key
      ADMIN_PUBLIC_KEY          = local.admin_public_key
    })
  }
}terraform/services/compute/main.tf

Finally, the Cloud-Init template applies these keys to the default user, disabling password authentication for enhanced security.

# User configuration
users:
  - name: <username>
    sudo: "ALL=(ALL) NOPASSWD:ALL"
    groups: sudo, users, admin, docker
    shell: /bin/bash
    lock_passwd: true # Disable password login - use SSH keys only
    ssh_authorized_keys:
      - "${INFRASTRUCTURE_PUBLIC_KEY}"
      - "${ADMIN_PUBLIC_KEY}"

# SSH hardening
ssh_pwauth: false # Disable password authentication
ssh_deletekeys: true # Remove default cloud keysterraform/.../public-playground-server.yaml

Related Projects

    Mike 3.0

    Send a message to start the chat!

    You can ask the bot anything about me and it will help to find the relevant information!

    Try asking: