What I Built
- 1 Linux VM on Azure (Ubuntu 24.04 LTS, Standard D2lds v6)
- A cloud-init script that installed Docker automatically on first boot
- A Dockerized static website served by Nginx
- A live URL accessible from anywhere in the world
Step 1: Provisioning the Azure VM
I created the VM through the Azure Portal with these settings:
| Field | Value |
|---|---|
| Resource Group | docker-project-rg |
| VM Name | docker-vm |
| Region | UK South |
| Image | Ubuntu 24.04 LTS |
| Size | Standard D2lds v6 |
| Authentication | SSH public key |
Under the Networking tab, I opened port 22 for SSH and port 80 for HTTP traffic by adding inbound rules to the Network Security Group.
Step 2: The Cloud-Init Script (This Is the Magic Part)
Before launching the VM, I pasted this script under the Advanced tab in the Custom Data field:
#cloud-config
package_update: true
package_upgrade: true
packages:
- apt-transport-https
- ca-certificates
- curl
- gnupg
- lsb-release
runcmd:
- apt-get update -y
- apt-get install -y docker.io
- systemctl enable docker
- systemctl start docker
- usermod -aG docker azureuser
This script runs automatically the moment the VM boots. Docker installs itself, starts itself, and adds my user to the docker group. I had not even SSH'd in yet.
When I later ran:
sudo cat /var/log/cloud-init-output.log | grep -i docker
The log confirmed everything. Docker was installed by the startup script, not by me. That is infrastructure automation doing exactly what it is supposed to do.
Step 3: SSH Into the VM
Once deployment completed, I grabbed my public IP from the Azure Portal and connected:
chmod 400 docker-vm_key.pem
ssh -i docker-vm_key.pem azureuser@4.234.163.212
Then I verified Docker was running:
docker --version
# Docker version 29.1.3
docker ps
# Empty, no containers yet. But Docker is alive.
Step 4: Clone the Static Website
git clone https://github.com/pravinmishraaws/Azure-Static-Website.git
cd Azure-Static-Website
ls
# README.md index.html
Simple. Just an index.html. That is all we need.
Step 5: Write the Dockerfile
FROM nginx:alpine
RUN rm -rf /usr/share/nginx/html/*
COPY . /usr/share/nginx/html
EXPOSE 80
Let me break this down:
FROM nginx:alpine — we are using a lightweight version of Nginx as our base image. Alpine Linux is tiny, which keeps our container small and fast.
RUN rm -rf /usr/share/nginx/html/* — wipes out the default Nginx welcome page so our site shows instead.
COPY . /usr/share/nginx/html — copies everything in our current folder (including index.html) into the Nginx web root.
EXPOSE 80 — tells Docker this container will accept traffic on port 80.
Step 6: Build the Image
docker build -t static-site:latest .
Docker pulls nginx:alpine, runs each step in the Dockerfile, and produces an image called static-site:latest. The whole process takes about a minute.
After building, I checked the image sizes:
| Image | Size |
|---|---|
| nginx:alpine | 93.5 MB |
| static-site:latest | 92.9 MB |
Our image is actually slightly smaller than the base because we replaced Nginx's default content with our own lighter files.
Step 7: Run the Container
docker run -d --name static-site \
-p 80:80 \
--restart unless-stopped \
static-site:latest
Breaking down the flags:
-
-druns the container in the background -
--name static-sitegives it a friendly name -
-p 80:80maps port 80 on the VM to port 80 inside the container -
--restart unless-stoppedmeans it comes back automatically after a VM reboot
Step 8: Verify and Open in Browser
docker ps
Output:
CONTAINER ID IMAGE PORTS NAMES
83cb16a6cb44 static-site:latest 0.0.0.0:80->80/tcp static-site
Then I opened http://4.234.163.212 in my browser.
The site loaded.
I cannot fully explain how good that felt. Three months into learning DevOps, and I just served a live website from inside a Docker container running on a cloud VM I spun up myself.
What I Learned
Cloud-init is a game changer. The idea that a VM can configure itself on boot, without any human touching it, is what separates manual setups from real infrastructure automation.
Containers are not complicated. A Dockerfile is just a recipe. Build the recipe, get an image. Run the image, get a container. That is it.
nginx:alpine is perfect for static sites. Small, fast, and it just works.
The --restart flag matters. In production, you want your containers to survive reboots. Always add --restart unless-stopped.
Full Project
GitHub: https://github.com/vivianokose/cloud-vm-docker-deploy
See you in the next one.
Top comments (0)