How to Start a Docker Host on Microsoft Azure: Complete Deployment and Configuration Guide

How to Starting a Docker Host on Microsoft Azure

Running Docker locally is fine for development, but sooner or later you need a real remote host — something with a public IP, real compute, and enough resources to run more than a couple of containers at once. Azure is one of the three big options for that, and this guide walks through provisioning a Docker-ready VM on Azure from scratch, securing it properly, and connecting to its Docker daemon remotely.

Prerequisites

  • An Azure account with an active subscription
  • Azure CLI installed locally, or run it via container (see the companion guide on running cloud CLIs in Docker)
  • SSH key pair for authentication (ssh-keygen -t ed25519)

Step 1: Log In and Set Your Subscription

az login
az account list --output table
az account set --subscription "Pay-As-You-Go"

Step 2: Create a Resource Group

Resource groups are Azure’s logical container for related resources — keeping everything under one group makes cleanup trivial later.

az group create --name docker-rg --location eastus

Expected output:

{
  "id": "/subscriptions/xxxx/resourceGroups/docker-rg",
  "location": "eastus",
  "name": "docker-rg",
  "properties": {
    "provisioningState": "Succeeded"
  }
}

Step 3: Create a Virtual Machine

Azure has a marketplace image for Docker-ready hosts, but I generally prefer starting from plain Ubuntu and installing Docker myself via cloud-init — it keeps the setup transparent and reproducible.

az vm create \
  --resource-group docker-rg \
  --name docker-host-01 \
  --image Ubuntu2404 \
  --size Standard_B2s \
  --admin-username azureuser \
  --ssh-key-values ~/.ssh/id_ed25519.pub \
  --custom-data cloud-init-docker.yml

Where cloud-init-docker.yml looks like this:

#cloud-config
package_update: true
package_upgrade: true
runcmd:
  - curl -fsSL https://get.docker.com -o get-docker.sh
  - sh get-docker.sh
  - usermod -aG docker azureuser
  - systemctl enable docker
  - systemctl start docker

Expected output after a minute or two:

{
  "fqdns": "",
  "location": "eastus",
  "publicIpAddress": "20.185.44.112",
  "resourceGroup": "docker-rg"
}

Step 4: Open the Required Network Ports

By default Azure VMs allow SSH (22) but nothing else. Open port 2376 if you plan to expose the Docker daemon remotely (with TLS — never without), and any application ports you need.

az vm open-port --resource-group docker-rg --name docker-host-01 --port 2376 --priority 900
az vm open-port --resource-group docker-rg --name docker-host-01 --port 80 --priority 910

Step 5: SSH In and Verify Docker

ssh azureuser@20.185.44.112
docker --version
docker run hello-world

Expected output:

Docker version 27.3.1, build ce12230
...
Hello from Docker!
This message shows that your installation appears to be working correctly.

Step 6: Securing Remote Docker API Access (Optional but Important)

If you want to run docker commands against this host from your laptop (rather than SSHing in every time), you need to expose the Docker daemon over TCP — and that must be done with TLS certificates, never in plaintext, since an open Docker socket is equivalent to root access on the host.

On the VM, generate a CA and server certificates:

mkdir -p ~/docker-certs && cd ~/docker-certs
openssl genrsa -aes256 -out ca-key.pem 4096
openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem
openssl genrsa -out server-key.pem 4096
openssl req -subj "/CN=$(hostname)" -sha256 -new -key server-key.pem -out server.csr
echo subjectAltName = IP:20.185.44.112,IP:127.0.0.1 > extfile.cnf
openssl x509 -req -days 365 -sha256 -in server.csr -CA ca.pem -CAkey ca-key.pem \
  -CAcreateserial -out server-cert.pem -extfile extfile.cnf

Then configure the Docker daemon to use TLS by editing /etc/docker/daemon.json:

{
  "hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2376"],
  "tls": true,
  "tlsverify": true,
  "tlscacert": "/etc/docker/ca.pem",
  "tlscert": "/etc/docker/server-cert.pem",
  "tlskey": "/etc/docker/server-key.pem"
}

Restart Docker:

sudo systemctl restart docker

On your laptop, generate a client cert signed by the same CA and connect:

export DOCKER_HOST=tcp://20.185.44.112:2376
export DOCKER_TLS_VERIFY=1
export DOCKER_CERT_PATH=~/docker-certs/client

docker version

Step 7: Attach a Managed Disk for Persistent Storage

If your containers need persistent storage beyond the OS disk, attach and mount an Azure managed disk.

az disk create \
  --resource-group docker-rg \
  --name docker-data-disk \
  --size-gb 64 \
  --sku Premium_LRS

az vm disk attach \
  --resource-group docker-rg \
  --vm-name docker-host-01 \
  --name docker-data-disk

On the VM:

sudo lsblk
sudo mkfs.ext4 /dev/sdc
sudo mkdir /mnt/docker-data
sudo mount /dev/sdc /mnt/docker-data

Then point Docker’s data root at this disk by adding "data-root": "/mnt/docker-data/docker" to /etc/docker/daemon.json and restarting Docker.

Networking Fundamentals on This Host

Once Docker is running on the Azure VM, container networking behaves exactly as it does anywhere else — the docker0 bridge handles container-to-container traffic on the host, and published ports (-p 80:80) are what Azure’s Network Security Group rules need to allow through externally. It’s a common point of confusion: opening a port in the NSG doesn’t automatically mean a container is listening on it — you still need -p (or a Compose ports: entry) to actually bind the container’s port to the host.

Running a Real Workload

docker run -d --name nginx-demo -p 80:80 --restart unless-stopped nginx:latest

Visit http://20.185.44.112 in a browser — you should see the default Nginx welcome page.

Monitoring the Host

docker stats --no-stream

Expected output:

CONTAINER ID   NAME          CPU %   MEM USAGE / LIMIT   MEM %   NET I/O
a1b2c3d4e5f6   nginx-demo    0.00%   2.4MiB / 1.91GiB    0.12%   1.2kB / 0B

For anything beyond ad-hoc checks, install cadvisor or point Azure Monitor’s VM insights at the host for CPU, memory, and disk metrics over time.

Deploying with Docker Compose on the Azure VM

Most real workloads involve more than a single container, and Compose is usually the fastest way to define them. Install the Compose plugin (already included if you used the get-docker.sh convenience script above) and define a stack:

# docker-compose.yml
services:
  web:
    image: nginx:latest
    ports:
      - "80:80"
    restart: unless-stopped
    depends_on:
      - api

  api:
    image: myregistry.example.com/api:1.0.0
    environment:
      - DATABASE_URL=postgresql://appuser:apppass@db:5432/appdb
    restart: unless-stopped

  db:
    image: postgres:16
    volumes:
      - db-data:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=appdb
      - POSTGRES_USER=appuser
      - POSTGRES_PASSWORD=apppass
    restart: unless-stopped

volumes:
  db-data:
docker compose up -d

Expected output:

[+] Running 4/4
 ✔ Network azureuser_default   Created
 ✔ Container azureuser-db-1    Started
 ✔ Container azureuser-api-1   Started
 ✔ Container azureuser-web-1   Started

Using an Azure Load Balancer for High Availability

A single VM is a single point of failure. For anything beyond a dev/test environment, put an Azure Load Balancer or Application Gateway in front of two or more identically configured Docker hosts, using a Virtual Machine Scale Set (VMSS) so Azure can add or remove instances automatically based on load:

az vmss create \
  --resource-group docker-rg \
  --name docker-vmss \
  --image Ubuntu2404 \
  --custom-data cloud-init-docker.yml \
  --admin-username azureuser \
  --ssh-key-values ~/.ssh/id_ed25519.pub \
  --instance-count 2 \
  --lb-sku Standard \
  --public-ip-per-vm false

Each instance in the scale set boots with the same cloud-init script, so Docker and your container startup logic come up identically on every node.

Understanding Azure’s Networking Layers

It helps to keep Azure’s network layers distinct in your head, since they each control a different thing:

  • Virtual Network (VNet) and Subnets — define the private IP address space the VM lives in.
  • Network Security Group (NSG) — the stateful firewall controlling inbound/outbound traffic to the VM’s network interface; this is what az vm open-port modifies.
  • Docker’s own bridge network — entirely internal to the VM, invisible to Azure; controls how containers talk to each other and to the host.
  • Published ports (-p) — the bridge between the two; a container port isn’t reachable from the VNet or the internet until it’s published to the host, and the host port isn’t reachable externally until the NSG allows it.

Missing any one of these three layers is the most common reason a “working” container is unreachable from outside the VM.

Best Practices

  1. Never expose the Docker API without TLS. An unauthenticated Docker socket over the network is a full remote-root vulnerability.
  2. Use Azure Network Security Groups as a second layer on top of TLS — restrict port 2376 to known IP ranges rather than 0.0.0.0/0.
  3. Enable automatic OS patching via Azure’s VM guest patching settings, or handle it yourself with unattended-upgrades.
  4. Use a managed disk for /var/lib/docker if you expect meaningful image/container storage growth — resizing the OS disk later is more disruptive.
  5. Tag resources (--tags env=prod team=platform) from day one; cleanup and cost attribution get painful otherwise.
  6. Consider Azure Container Instances or AKS instead of a raw VM if you don’t specifically need daemon-level control — a VM is the right choice when you need custom daemon config, privileged containers, or GPU passthrough.

Troubleshooting

  • VM creation fails with quota errors — check az vm list-usage --location eastus --output table for your subscription’s vCPU quota.
  • Can’t SSH in — confirm the NSG allows port 22 from your IP, and that you’re using the correct username (azureuser by default in the example above).
  • docker run hello-world hangs — check systemctl status docker on the VM; cloud-init logs at /var/log/cloud-init-output.log will show if the install script failed.
  • Remote TLS connection refused from laptop — confirm port 2376 is open in the NSG and that daemon.json was loaded correctly (sudo systemctl status docker will show a parse error if the JSON is malformed).

Summary

Standing up a Docker host on Azure is a matter of provisioning a VM (via cloud-init for a repeatable setup), opening the right ports, and — if remote access is needed — securing the Docker API with TLS certificates rather than leaving it open. From there, container networking, storage, and monitoring on Azure behave exactly like they would on any Linux Docker host; the cloud-specific work is really just the provisioning and network security layer around it.

References

  • Azure CLI documentation: https://learn.microsoft.com/en-us/cli/azure/
  • Docker Engine installation guide: https://docs.docker.com/engine/install/ubuntu/
  • Docker Engine API security (TLS): https://docs.docker.com/engine/security/protect-access/
  • Azure Virtual Machines documentation: https://learn.microsoft.com/en-us/azure/virtual-machines/
Total
2
Shares

Leave a Reply

Previous Post
How to Start a Docker Host on Google GCE

How to Start a Docker Host on Google GCE: Complete Setup and Configuration Guide

Next Post
How to Run a Cloud Provider CLI in a Docker Container

How to Run a Cloud Provider CLI in a Docker Container: AWS, Azure, and GCP Setup Guide

Related Posts