Deploy to Azure Container Instances
Why this matters
Everything so far has run on your own machine. Azure Container Instances (ACI) is the fastest path from "I have an image in a registry" to "it's running on the public internet" — no cluster to provision, no VM to manage, just a single command that starts a container and gives it a public address. It's the right tool for a quick deployment, a demo, or a batch job; it's also the simplest way to prove your ACR setup from module 08 actually works end to end before tackling App Service (module 11) or Kubernetes (the next track).
This module uses your real Azure subscription. Follow the cleanup subsection at the end — ACI bills while the container group exists.
Concepts
A container group is ACI's unit of deployment
ACI's core object is a container group — one or more containers that
are always scheduled together on the same host, sharing a network
namespace and, optionally, volumes. For a single-container deployment
(everything in this module), a container group behaves like one
docker run, but hosted on Azure-managed infrastructure with a public IP
instead of your machine's.
your machine Azure
┌───────────┐ push ┌──────────────────────────────────────────┐
│ az acr │────────►│ ACR myacr.azurecr.io/webapp:v1 │
│ build │ └───────────────┬──────────────────────────┘
└───────────┘ │ pull (registry creds)
▼
┌─────────── container group "webapp-aci" ─────┐
│ ┌──────────────┐ public FQDN: │
browser ────────────►│ webapp :8000 │ <label>.<region> │
:8000 │ └──────────────┘ .azurecontainer.io │
└───────────────────────────────────────────────┘
ACI needs to authenticate to your registry to pull the image
Since your image lives in a private ACR instance (module 08), ACI needs
credentials to pull it — the same problem docker login solves locally,
solved here with registry credentials passed to az container create.
The simplest approach for learning purposes is ACR's admin account: a
single built-in username/password pair for the whole registry, which you
enable once and pass to ACI at container-creation time. (Production
setups typically use a managed identity instead, avoiding a shared
password entirely — worth knowing exists, not required for this
exercise.)
DNS name label gives you a stable public hostname
A container group can be assigned a --dns-name-label, producing a
fully-qualified domain name of the form
<label>.<region>.azurecontainer.io. This label must be unique within
that Azure region (similar in spirit to ACR's globally-unique name
requirement from module 08, but scoped to the region rather than all of
Azure) — if it's taken, az container create fails with a clear error
telling you so.
Logs work the same way conceptually as docker logs
az container logs reads the same stdout/stderr stream docker logs
reads locally — the daemon-buffers-output convention from module 02
holds here too, just surfaced through Azure's API instead of the local
Docker engine.
In Docker Desktop: none of ACI's resources show up in Docker Desktop — ACI containers run entirely on Azure's infrastructure, not your local Docker engine. Docker Desktop's GUI is local-engine-only; from here on, the Azure Portal (portal.azure.com) is the GUI equivalent for anything you deploy to Azure. Worth opening the Portal alongside PowerShell in this module's exercises to see the resource group, registry, and container group appear as you create them.
Command reference
| Command | What it does | Example |
|---|---|---|
az login |
Authenticates the Azure CLI to your account | az login |
az group create |
Creates a resource group (a logical container for related Azure resources) | az group create --name learn-docker-rg --location eastus |
az acr update --admin-enabled true |
Enables the built-in admin username/password for an ACR instance | az acr update --name myacr --admin-enabled true |
az acr credential show |
Retrieves the admin username/password for an ACR instance | az acr credential show --name myacr |
az container create |
Creates and starts a container group in ACI | see below |
az container show |
Shows a container group's status, including its public FQDN | az container show --resource-group learn-docker-rg --name webapp-aci --query "{fqdn:ipAddress.fqdn,state:instanceView.state}" --output table |
az container logs |
Prints a container group's stdout/stderr | az container logs --resource-group learn-docker-rg --name webapp-aci |
az container attach |
Streams live logs and events (similar to docker logs -f) |
az container attach --resource-group learn-docker-rg --name webapp-aci |
az container exec |
Runs a command inside a running container in the group | az container exec --resource-group learn-docker-rg --name webapp-aci --exec-command "/bin/sh" |
az container restart |
Restarts a container group | az container restart --resource-group learn-docker-rg --name webapp-aci |
az container delete |
Deletes a single container group | az container delete --resource-group learn-docker-rg --name webapp-aci --yes |
az group delete |
Deletes a resource group and everything in it | az group delete --name learn-docker-rg --yes --no-wait |
Flag breakdown for the full az container create command:
az container create `
--resource-group learn-docker-rg `
--name webapp-aci `
--image myacr.azurecr.io/webapp:v1 `
--registry-login-server myacr.azurecr.io `
--registry-username <acr-admin-username> `
--registry-password <acr-admin-password> `
--dns-name-label webapp-aci-<yourinitials>2026 `
--ports 8000 `
--cpu 1 `
--memory 1.5
--resource-group learn-docker-rg— which resource group this container group belongs to.--name webapp-aci— the container group's name (used in lateraz container ...commands to refer to it).--image myacr.azurecr.io/webapp:v1— the fully-qualified image reference to pull, exactly like the tag you pushed in module 08.--registry-login-server,--registry-username,--registry-password— how ACI authenticates to pull a private image from ACR, equivalent to whatdocker logindoes locally.--dns-name-label webapp-aci-<yourinitials>2026— the subdomain portion of the public FQDN; must be unique within the chosen region.--ports 8000— which container port(s) to expose publicly (the container-side port, matching what your app actually listens on — same concept as the container-side number indocker run -p).--cpu 1 --memory 1.5— resource allocation in vCPUs and GiB of memory; billing scales with these values, so keep them modest for a learning exercise.
Hands-on exercises
(PowerShell) Log in and confirm your subscription:
az login az account show --output tableExpect a browser window to open for sign-in, then a table showing your subscription name and ID.
(PowerShell) Reuse or create the resource group and ACR instance from module 08 (skip the
az group create/az acr createlines if you already have them from that module):az group create --name learn-docker-rg --location eastus az acr create --resource-group learn-docker-rg --name <yourinitialsacr2026> --sku Basic(PowerShell) Build and push a small example image directly with
az acr build(no local Docker build needed — see module 08 if you want the local-build alternative):mkdir ~/learn-docker/aci-lab cd ~/learn-docker/aci-labThen create the same three files from earlier modules (
app.py,requirements.txt,Dockerfilewith a Flask app exposing/healthon port 8000 — copy the pattern from module 08, exercise 1), and run:az acr build --registry <yourinitialsacr2026> --image webapp:v1 .Expect a build log streamed from Azure, ending in a successful push.
(PowerShell) Enable the ACR admin account and fetch its credentials:
az acr update --name <yourinitialsacr2026> --admin-enabled true az acr credential show --name <yourinitialsacr2026>Expect JSON containing a
usernameand twopasswords— note the username and the first password value for the next step.(PowerShell) Create the container group:
az container create ` --resource-group learn-docker-rg ` --name webapp-aci ` --image "<yourinitialsacr2026>.azurecr.io/webapp:v1" ` --registry-login-server "<yourinitialsacr2026>.azurecr.io" ` --registry-username "<acr-admin-username>" ` --registry-password "<acr-admin-password>" ` --dns-name-label "webapp-aci-<yourinitials>2026" ` --ports 8000 ` --cpu 1 ` --memory 1.5Expect a JSON response with
"provisioningState": "Succeeded"after a short wait (ACI provisioning typically takes under a minute).(PowerShell) Get the public FQDN and confirm the app is reachable:
az container show --resource-group learn-docker-rg --name webapp-aci --query "{fqdn:ipAddress.fqdn,state:instanceView.state}" --output tableTake the
fqdnvalue and open, in a browser or withcurl:curl "http://<fqdn>:8000/health"Expect
{"status":"ok"}— this request left your machine entirely and hit a container running on Azure.(Azure Portal GUI) Open portal.azure.com, navigate to your
learn-docker-rgresource group, and click into thewebapp-acicontainer instance. Its Overview page shows the same FQDN, state, and CPU/memory allocation you just saw via CLI; its Containers → Logs tab shows the same outputaz container logswould print — confirm both match.(PowerShell) View logs and stream them live:
az container logs --resource-group learn-docker-rg --name webapp-aciThen, in a separate terminal, generate some traffic and watch logs stream:
az container attach --resource-group learn-docker-rg --name webapp-aci(Ctrl+C to stop attaching — this does not stop the container, only your local log stream.) Expect Flask's request log lines to appear as you hit the
/healthendpoint again from another window.Diagnose and fix: container group stuck in "Waiting" with a pull failure. Deliberately break the registry credential:
az container delete --resource-group learn-docker-rg --name webapp-aci --yes az container create ` --resource-group learn-docker-rg ` --name webapp-aci-broken ` --image "<yourinitialsacr2026>.azurecr.io/webapp:v1" ` --registry-login-server "<yourinitialsacr2026>.azurecr.io" ` --registry-username "<acr-admin-username>" ` --registry-password "wrong-password" ` --dns-name-label "webapp-aci-broken-<yourinitials>2026" ` --ports 8000 az container show --resource-group learn-docker-rg --name webapp-aci-broken --query "instanceView.state" --output tsvExpect the state to stay stuck (not
Running), and:az container show --resource-group learn-docker-rg --name webapp-aci-broken --query "containers[0].instanceView.events" --output tableto show a pull failure event referencing authentication. Fix it by deleting and recreating with the correct password from exercise 4:
az container delete --resource-group learn-docker-rg --name webapp-aci-broken --yes(There's no in-place credential update for a running container group — ACI container groups are immutable once created; fixing a bad config means delete-and-recreate, which is why getting the credentials right from
az acr credential showmatters.)Cleaning up (avoid surprise charges). ACI bills for CPU/memory allocated to a running container group for as long as it exists — stopping is not free the way
docker stopis; you must delete the container group (or the whole resource group) to stop being billed for it. When you're done experimenting:az container delete --resource-group learn-docker-rg --name webapp-aci --yesOr, to remove everything from this module and module 08 at once (resource group, ACR instance, and any container groups inside it) in one step:
az group delete --name learn-docker-rg --yes --no-wait--yesskips the confirmation prompt;--no-waitreturns immediately instead of blocking until deletion finishes (deletion itself continues in the background). Confirm it's gone a few minutes later:az group show --name learn-docker-rgExpect a
ResourceGroupNotFounderror once deletion completes — that error is what you want to see.
Independent challenge
No commands given here — figure it out yourself using what you know from this module and earlier ones.
Task: Using real az commands, get one of your own images from a private ACR instance (module 08) running on ACI and reachable over the public internet, then deliberately provoke and diagnose a failure before cleaning everything up. First deploy the image so a curl against its public FQDN returns your app's response — you'll have to get the registry authentication and the exposed container port both right for that to work. Then create a second container group with something wrong (a bad registry credential, or an exposed port that doesn't match what the app listens on), observe that it never reaches a healthy running state, and find the specific reason using the platform's own diagnostics rather than guessing. When you are finished, delete the container groups and the resource group so you stop being billed — ACI charges for allocated CPU/memory for as long as a group exists, and there is no free "stopped" state. Confirm the resource group is actually gone afterward.
Stuck? One hint
Get the pull working by passing the ACR login server plus admin username/password to az container create and setting --ports to the container's own port; when it won't start, read the container group's instanceView.events for the pull/authentication error, and tear down with az group delete.
Common mistakes & troubleshooting
- Forgetting
az container deleteoraz group deleteafter finishing. Unlike a localdocker run, an ACI container group keeps billing until you explicitly delete it — there's no equivalent of just closing your laptop to stop the meter. - Picking a
--dns-name-labelthat's already taken in that region. Fails with a clear "already in use" error — the fix is simply to pick a more unique label (initials + numbers works reliably). - Mismatched
--portsand what the app inside actually listens on. If your Flask app listens on 8000 but you pass--ports 80, the health check/curl will time out even though the container is "Running" — double-check the container-side port matches the app, the same container-vs-host-port confusion from module 05, just without a host-side number to get right here (ACI publishes the container port directly). - Using the wrong ACR admin password (or the account not being
enabled) and getting a stuck pull. As shown in exercise 9, the
container group provisions but never reaches
Running— checkinstanceView.eventsfor the specific pull error rather than guessing. - Expecting
az container createto update an existing container group in place. It doesn't — running it again with the same--nameagainst an existing group errors; you must delete first (as in the diagnose-and-fix exercise) for any config change. - Confusing ACI's per-second CPU/memory billing model with a flat
monthly cost. Cost scales directly with
--cpu/--memoryand how long the group exists — keep both modest for learning exercises, and don't leave a container group running between study sessions.
Checkpoint quiz
Write down your answer to each question before expanding it — checking without attempting first is the single easiest way to fool yourself into thinking you've learned this.
Show questions
- What is a "container group" in ACI, and how does it relate to what
docker rundoes locally? - Why does
az container createneed--registry-login-server,--registry-username, and--registry-passwordfor an image stored in ACR, when a localdocker runfor the same image needs none of that (once you're logged in)? - What determines the public hostname of a deployed container group, and what constraint does that value have to satisfy?
- If a container group's provisioning state never reaches
Running, what's the first thing to check, and which command shows it? - Can you update an existing container group's image or credentials in
place with another
az container create? What do you have to do instead? - Why does deleting a container group (or the whole resource group) matter for cost, in a way that stopping a local container never required?
Show answers
- A container group is ACI's unit of deployment — one or more
containers scheduled together, sharing a network namespace. For a
single container, it's conceptually equivalent to one
docker run, except hosted on Azure infrastructure with a public IP/FQDN instead of your local machine. - ACI has no persisted login session the way your local Docker CLI does
after
docker login/az acr login— each container group creation needs to authenticate to the registry itself, so the credentials must be supplied explicitly at creation time. - The
--dns-name-labelvalue, combined with the region, forms the FQDN (<label>.<region>.azurecontainer.io); the label must be unique within that Azure region. - Check
instanceView.eventsviaaz container show ... --query "containers[0].instanceView.events"— it surfaces specific failure reasons like image pull authentication errors. - No — container groups are immutable once created; changing the image, credentials, ports, or resources requires deleting the existing group and creating a new one with the updated configuration.
- ACI bills for allocated CPU/memory for as long as the container group exists, regardless of whether it's actively serving traffic — unlike a local container, there's no free "just leave it stopped" state; you must delete the group (or resource group) to stop being charged.
Further reading & sources
- Azure: What is Azure Container Instances? - the official overview of container groups and the ACI service.
- Azure: Quickstart — deploy a container to ACI with the CLI - the walkthrough this module's exercises mirror.
- Azure: Deploy to ACI from Azure Container Registry - how ACI authenticates and pulls a private ACR image.
- az container CLI reference - the full reference for
create,show,logs, anddelete. - Azure: Container Instances pricing - the per-second CPU/memory billing model behind this module's cleanup warnings.
Next
Continue to 11-deploy-to-azure-app-service to deploy the same kind of image to a managed web-hosting platform with built-in scaling, custom domains, and deployment slots.