Skip to main content
Man holding a laptop while explaining a deployment decision

Docker Installation and Deployment with Cloud-Init

January 20, 2026 · Blog / Technical Guides

Cloud-init can turn a new Ubuntu VPS into a repeatable Docker host on its first boot. The safe approach is to keep the first-boot configuration small, validate it before provisioning, and deploy applications only after the Docker service passes its checks.

This guide follows the Docker Engine installation instructions and the cloud-init configuration examples. Review both sources before adapting the configuration to another distribution.

Plan the deployment

Use cloud-init for deterministic host preparation rather than for storing application secrets. The configuration below installs Docker from Docker's Ubuntu repository, enables the service, and writes a local Compose file that binds its test service to loopback only.

Before provisioning, confirm that the VPS image is Ubuntu, that cloud-init is enabled, and that you can reach the server through SSH. Keep provider-console access available until the first boot and firewall checks have passed.

Build the cloud-init configuration

Save the following as cloud-init.yaml. The first line must remain the first line of the file, and YAML indentation must use spaces.

“`yaml title="Provision Docker Engine and a loopback-only Compose service" #cloud-config package_update: true packages:

  • ca-certificates
  • curl

write_files:

  • path: /usr/local/sbin/install-docker-engine

owner: root:root permissions: '0750' content: | #!/usr/bin/env bash set -Eeuo pipefail install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc chmod a+r /etc/apt/keyrings/docker.asc . /etc/os-release printf 'deb [arch=%s signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu %s stable\n' "$(dpkg –print-architecture)" "$VERSION_CODENAME" > /etc/apt/sources.list.d/docker.list apt-get update apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin systemctl enable –now docker

  • path: /opt/hello-compose/compose.yaml

owner: root:root permissions: '0644' content: | services: web: image: nginx:alpine restart: unless-stopped ports:

  • "127.0.0.1:8080:80"

runcmd:

  • ["/usr/local/sbin/install-docker-engine"]
  • ["docker", "compose", "-f", "/opt/hello-compose/compose.yaml", "up", "-d"]

final_message: "Docker host preparation completed"


The repository key is stored in `/etc/apt/keyrings`, and the repository entry uses the Ubuntu codename reported by the image. The Compose example does not expose its test service to the internet; it is available only from the VPS itself.

Validate the file on a disposable Ubuntu system with cloud-init installed before attaching it to a VPS:

```bash title="Validate the cloud-init schema"
sudo cloud-init schema --config-file cloud-init.yaml

A schema pass checks the cloud-config structure, not the safety of every shell command. Read the generated script and verify the repository URL, distribution, and package names before provisioning.

Protect SSH and published ports

Allow SSH before enabling UFW so an active session does not become the only way back into the server:

“`bash title="Allow SSH through UFW and enable it" sudo ufw allow OpenSSH sudo ufw enable sudo ufw status verbose


Docker manages packet-filtering rules for published container ports. The [Docker firewall documentation](https://docs.docker.com/engine/network/packet-filtering-firewalls/) explains why a published port can bypass rules that appear to block it in UFW. Bind application ports to `127.0.0.1` unless they must be public, restrict public traffic at the provider firewall, and place web applications behind an authenticated TLS reverse proxy.

Do not add `docker` commands to UFW's `DOCKER-USER` chain without testing the full connection path. Keep a second SSH session or provider console open while changing host or provider firewall rules.

## Separate host and application responsibilities

Cloud-init should establish the host baseline, while each application should keep its own Compose file, environment configuration, data ownership, and recovery procedure. This separation makes it possible to rebuild the host without turning one first-boot file into a permanent application control plane.

Create one directory per application under an agreed location, restrict write access to its operator, and document which volumes contain persistent data. Do not mount the Docker socket into an application container unless its documented function requires host-level container control; possession of that socket can grant control over the VPS.

Record the DNS name, public ports, reverse-proxy route, image reference, data volumes, health check, and restore owner for each project. Before adding another service, check that it does not reuse an occupied port or depend on an undocumented network created by a different project.

Send application logs to a storage location with a defined retention limit and monitor host disk usage. A container restart policy can restart a failed process, but it cannot repair corrupt data, an invalid configuration, an expired certificate, or a full filesystem.

## Verify the first boot

Wait for cloud-init to finish, then inspect its status and service logs:

```bash title="Verify cloud-init Docker and the Compose service"
sudo cloud-init status --wait
sudo systemctl --no-pager --full status docker
sudo docker version
sudo docker compose version
sudo docker compose -f /opt/hello-compose/compose.yaml ps
sudo docker compose -f /opt/hello-compose/compose.yaml exec -T web nginx -t

A successful result shows cloud-init completed, Docker is active, the Compose service is running, and its Nginx configuration is valid. If provisioning fails, inspect /var/log/cloud-init-output.log before rerunning anything; repeated first-boot scripts can leave partial repository or package state.

Deploy an application safely

Replace the smoke-test service only after its checks pass. Use a project-specific Compose file, pin application images to a reviewed release or digest, and keep passwords, tokens, and private keys outside cloud-init and source control.

For a public web service, bind the application to loopback, terminate TLS at a reverse proxy, and open only the ports required for SSH and HTTPS. Confirm the public endpoint from another network and confirm that the application port itself is not reachable directly.

Use health checks that rely on software actually installed in the image. A health command that calls curl or wget without including that tool in the image will mark a healthy application as unavailable.

Update and recover

Before changing Docker packages or application images, create a tested application-data backup and record the running image references. Pull and restart one project at a time, then check service health and logs before continuing.

Avoid broad cleanup commands such as system-wide prune operations on a host with unknown workloads. Review stopped containers, unused images, networks, and volumes separately; remove only named resources whose owners and recovery path are known.

If a deployment fails, restore the previous Compose file and image references, restore application data when required, and start the project again. Cloud-init is a provisioning record, not a substitute for application-aware backup and restore testing.

Next steps

Keep the cloud-config in version control without secrets, test changes on a disposable VPS, and record the validation output with each revision.

Planning a Docker deployment?

Choose a Virtarix Cloud VPS for the operating system, containers, application data, and recovery workspace your deployment needs.

VPS S

For small sites, dev servers and Docker

$ 5 .50 /month
  • 3 cores
  • 6 GB
  • 50 GB NVMe
  • Unlimited
Get It Now
BEST SELLER

VPS M

For growing apps, websites and staging

$ 11 .40 /month
  • 6 cores
  • 16 GB
  • 100 GB NVMe
  • Unlimited
Get It Now
Peter French
About the Author Peter Frenchis the Managing Director at Virtarix, with over 17 years in the tech industry. He has co-founded a cloud storage business, led strategy at a global cloud computing leader, and driven market growth in cybersecurity and data protection.