Back to articles
Technology Insight

Automating Minimal OS Packaging with Docker, Packer, and Cloud-init for Multi-Cloud VPS Deployment

June 3, 2026

Introduction: The Cost of Manual VPS Provisioning

In modern cloud engineering, speed, consistency, and security are the cornerstones of infrastructure management. Yet, many development teams still rely on manual configurations when provisioning Virtual Private Servers (VPS). Spinning up a generic Ubuntu or Debian instance, manually installing Docker, configuring firewall rules, and setting up user permissions is not only time-consuming but also introduces the risk of configuration drift.

When environments are configured by hand, reproducing the exact same state across multiple cloud providers becomes an uphill battle. To solve this, DevOps practices emphasize the use of immutable infrastructure. Instead of configuring servers after they are created, we bake the entire configuration directly into the operating system image itself. This comprehensive guide walks you through automating the packaging of a minimal, Docker-optimized OS image using HashiCorp Packer and Cloud-init, designed to deploy seamlessly across any VPS provider.

The Core Architectural Component

To build a fully automated, cross-platform pipeline, we leverage a powerful combination of three industry-standard tools:

  • Packer: An open-source tool by HashiCorp used to create identical machine images for multiple platforms from a single source configuration.
  • Cloud-init: The industry-standard multi-distribution method for cross-platform cloud instance initialization. It handles early-boot configurations such as networking, SSH keys, and package installations.
  • Docker: The containerization engine that serves as the runtime environment for applications, eliminating the need for heavy dependencies on the host OS.

By coupling Packer's build automation with Cloud-init's initialization capabilities, we can strip away unnecessary bloated packages from standard OS distributions, leaving behind a highly secure, ultra-lightweight template prepared exclusively for running containerized workloads.

Prerequisites and Environment Setup

Before diving into the configuration files, ensure you have the following prerequisites installed on your local machine or continuous integration (CI) runner:

  1. Packer CLI: Version 1.9.0 or higher.
  2. A VPS Provider Account: API access keys for your chosen provider (e.g., DigitalOcean, AWS, Linode, or Vultr).
  3. OpenSSH: For generating cryptographic key pairs used during the build phase.
Note: While this guide uses the DigitalOcean builder as a primary example, Packer’s HCL (HashiCorp Configuration Language) syntax makes it trivial to port the exact same configuration to AWS AMI, OpenStack, or Proxmox.

Step 1: Structuring the Packer HCL File

We begin by defining our Packer template using HCL. Create a file named docker-minimal.pkr.hcl. This file tells Packer which base image to pull, what size instance to spin up temporarily for the build, and how to save the final snapshot.

packer {
  required_plugins {
    digitalocean = {
      version = ">= 1.0.0"
      source  = "[github.com/digitalocean/digitalocean](https://github.com/digitalocean/digitalocean)"
    }
  }
}

variable "do_token" {
  type      = string
  default   = env("DIGITALOCEAN_TOKEN")
  sensitive = true
}

source "digitalocean" "ubuntu-minimal" {
  api_token     = var.do_token
  image         = "ubuntu-24-04-x64"
  region        = "nyc3"
  size          = "s-1vcpu-1gb"
  ssh_username  = "root"
}

This block sets up the baseline connection. It fetches a clean Ubuntu image and provisions a minimal temporary droplet to execute our optimization and installation scripts.

Step 2: Designing the Cloud-init Initialization Blueprint

Instead of running multiple sequential bash scripts over SSH, which can fail silently, we use Cloud-init to handle the configuration natively during the boot process. Create a file named cloud-config.yaml. This script executes system updates, purges unnecessary packages, adjusts SSH configurations for security, and installs the Docker engine cleanly.

#cloud-config
package_update: true
package_upgrade: true

packages:
  - apt-transport-https
  - ca-certificates
  - curl
  - gnupg
  - lsb-release
  - ufw

runcmd:
  - install -m 0755 -d /etc/apt/keyrings
  - curl -fsSL [https://download.docker.com/linux/ubuntu/gpg](https://download.docker.com/linux/ubuntu/gpg) | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
  - chmod a+r /etc/apt/keyrings/docker.gpg
  - echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] [https://download.docker.com/linux/ubuntu](https://download.docker.com/linux/ubuntu) $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
  - apt-get update -y
  - apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
  - systemctl enable docker
  - systemctl start docker
  - ufw default deny incoming
  - ufw default allow outgoing
  - ufw allow ssh
  - ufw allow 80/tcp
  - ufw allow 443/tcp
  - ufw --force enable
  - apt-get autoremove -y && apt-get clean

This declarative configuration ensures that the Uncomplicated Firewall (UFW) is immediately locked down, blocking all unnecessary ports, while Docker is enabled to start automatically on system boot. Crucially, the apt-get autoremove command strips out residual dependencies, minimizing the final image footprint and reducing the overall attack surface.

Step 3: Connecting Packer to the Provisioner

Now, we append the build instructions to our docker-minimal.pkr.hcl file to execute our Cloud-init scripts and prepare the image for snapshotting.

build {
  sources = ["source.digitalocean.ubuntu-minimal"]

  provisioner "file" {
    source      = "cloud-config.yaml"
    destination = "/var/lib/cloud/seed/nocloud/user-data"
  }

  provisioner "shell" {
    inline = [
      "cloud-init clean --logs --reboot",
      "sleep 10"
    ]
  }
}

In this block, Packer moves our local cloud-config.yaml to the seed directory of the instance. The shell provisioner triggers cloud-init clean, clearing old logs and machine identifiers so that when a new VPS is initialized from this image, Cloud-init will re-run uniquely with fresh machine metadata and SSH keys.

Step 4: Executing the Automated Build

With both configurations validated, you are ready to compile the image. Execute the following commands in your terminal:

# Initialize Packer and download necessary provider plugins
packer init docker-minimal.pkr.hcl

# Validate the syntax of your configuration files
packer validate docker-minimal.pkr.hcl

# Execute the build pipeline
packer build docker-minimal.pkr.hcl

Packer will launch a temporary VPS instance, upload the configuration, execute the OS stripping and Docker installation, shut down the instance, register a custom reusable snapshot in your cloud account, and cleanly destroy the temporary VPS. The entire process takes less than five minutes.

Benefits of This Multi-Cloud Workflow

Transitioning from manual configuration to an automated image packaging system yields massive operational advantages:

  • Speed to Deployment: Spinning up a new production-ready node takes seconds rather than minutes because Docker and security hardening are pre-baked into the image layer.
  • Absolute Consistency: Every VPS instance created from this snapshot is mathematically identical, eliminating the classic "it works on my staging server" dilemma.
  • Enhanced Security: Stripping out unused compilers, development libraries, and system utilities fundamentally shrinks the OS attack vector. Furthermore, firewalls are active before the server ever faces the open web.
  • Cost Efficiency: By avoiding post-boot configuration tools like Ansible or manual SSH scripts during deployment, you reduce the CPU and network overhead required when scaling instances horizontally.

Conclusion: Next Steps in Infrastructure Evolution

Automating your operating system packaging is a profound step forward in mature infrastructure management. By treating your OS images as code, you can version-control your server images alongside your application codebase. To extend this architecture further, integrate this Packer pipeline into a CI/CD tool like GitHub Actions or GitLab CI. Trigger a new image build whenever a new version of Docker is released, ensuring your infrastructure templates remain secure, patched, and perpetually modern.

Automating Minimal OS Packaging with Docker, Packer, and Cloud-init for Multi-Cloud VPS Deployment | DPTCloud