Back to Blog
AzureContainer AppsDockerNode.jsDevOps

How to Deploy a Node.js App on Azure Container Apps

9 min read
How to Deploy a Node.js App on Azure Container Apps

If you've outgrown simple VMs and want to skip the overhead of managing Kubernetes yourself, Azure Container Apps (ACA) is the right move in 2026. It abstracts the server and cluster management while giving you full control over your Docker environment. No OS patching, no cluster nodes to babysit — just deploy a container and define your scaling rules.

This guide walks through the full pipeline: building your image in the cloud, pushing it to a private registry, and running it as a Container App — with your MongoDB connection wired in correctly so your app doesn't die on startup.


The Architecture: What You're Building

Before touching the portal, understand the separation of concerns:

ComponentRole
Azure Container Registry (ACR)Your private image storage — the "pantry" where Docker images live
Container App EnvironmentThe secure network boundary (VNet) and logging workspace (Log Analytics)
Container AppThe running instance of your image — scales up and down automatically
RevisionsEvery config change (env vars, image tag) creates a new revision — enables zero-downtime deploys

Step 1 — Create an Azure Container Registry

In the Azure Portal, search for Container registries and click Create.

Fill in the basics:

  • Registry name — must be globally unique (e.g., myappregistry)
  • Location — pick the region closest to your users
  • SKUBasic is fine to start; upgrade to Standard or Premium if you need geo-replication or private endpoints

Once created, navigate to your registry and copy the Login server value (e.g., myappregistry.azurecr.io). You'll need this throughout the guide.

Azure Container Registry overview page


Step 2 — Build Your Image in the Cloud (The Right Way)

In 2026, building images locally is a trap. If you develop on a Mac with Apple Silicon (M1/M2/M3), your local Docker image is built for the arm64 architecture. Azure runs amd64 Linux. The result: a container that builds fine locally and silently fails in production.

The fix is ACR Tasks — you push your source code to the registry and let Azure build the image in the cloud on the correct architecture.

Authenticate with Azure CLI

Before running any az command, you need to be logged in. Run:

az login

This opens a browser window to complete authentication. Once done, verify you're targeting the correct subscription — this matters if your account has access to multiple Azure subscriptions:

# List all subscriptions your account has access to
az account list --output table

# Switch to the correct one if needed
az account set --subscription "My Subscription Name"

# Confirm the active subscription
az account show --output table

Always confirm the active subscription before provisioning resources. Accidentally deploying to the wrong subscription is a common and annoying mistake to undo.

Add a Dockerfile to Your App

ACR Tasks builds your image from a Dockerfile at the root of your project. If you don't have one yet, here's a production-ready example for a Node.js/Express backend:

# Use a specific Node.js LTS version on Alpine for a small image
FROM node:20-alpine

# Set the working directory inside the container
WORKDIR /app

# Copy package files first — lets Docker cache the install layer
COPY package*.json ./

# Install only production dependencies
RUN npm install --omit=dev

# Copy the rest of your source code
COPY . .

# Expose the port your Express app listens on
EXPOSE 5000

# Start the server
CMD ["node", "server.js"]

A few things worth noting:

  • node:20-alpine — Alpine Linux keeps the image small (under 200MB vs ~1GB for the full Debian image). Use a pinned version, not latest, so your builds are reproducible.
  • Copy package*.json before COPY . . — Docker caches each layer. If your source code changes but your dependencies don't, Docker reuses the cached npm install layer instead of re-running it on every build.
  • --omit=dev — skips devDependencies. Your production image doesn't need testing libraries or build tools.
  • EXPOSE 5000 — documents the port. Make sure this matches the PORT in your Express app and the Ingress Target Port you'll set in Step 3.

Run the Cloud Build

With your Dockerfile in place, run:

# Build and push in one step — no local Docker required
az acr build --registry myappregistry --image my-node-app:v1 .

This command:

  1. Zips your local source directory and uploads it to ACR
  2. Builds the image on Azure's infrastructure (amd64 Linux)
  3. Pushes the final image to your registry automatically

No docker build, no docker push, no architecture mismatch.


Step 3 — Create the Container App

In the Azure Portal, search for Container Apps and click Create.

Basics Tab

  • App name — e.g., my-node-app
  • Region — match the region of your ACR
  • Container Apps Environment — create a new one if this is your first app. Give it a name and let Azure create the Log Analytics workspace automatically.

Container Tab

Select Azure Container Registry as the image source.

Selecting ACR as the image source in the Container App creation form

Fill in:

  • Registry — select the ACR you just created
  • Imagemy-node-app
  • Tagv1

Set the CPU and memory to the smallest option that fits your app. For a typical Node.js backend, 0.5 CPU / 1Gi is a reasonable starting point.

Container App basics configuration page

Notice what you're pointing Azure to here: a container image (my-node-app:v1) stored in ACR — not your source code, not a zip file, not a Git repo. This is an important mental shift. The image is a self-contained, immutable snapshot of your app and everything it needs to run (Node.js runtime, dependencies, your code). Azure doesn't know or care what language it's written in — it just pulls the image and runs it. This is what makes containers portable: the same image that runs locally is the exact same artifact running in production, with no "but it worked on my machine" surprises.

Ingress Tab

Enable Ingress so your app is reachable from the internet:

  • Ingress traffic — Accepting traffic from anywhere
  • Target port — this must match the PORT your Express server listens on (e.g., 5000)

Ingress tab showing external traffic enabled and target port set

Common mistake: If your Express app listens on port 5000 but you set the target port to 80 here, every request will time out. The portal won't warn you — the container will run, but no traffic will reach it.


Step 4 — Wire in Your Environment Variables

Your Node.js backend needs a MONGO_URI to connect to the database. Never hardcode this in your image.

After the app is created, go to Containers in the left menu and click Edit and Deploy.

In the container configuration panel, scroll down to Environment variables and add your secrets:

NameValue
MONGO_URImongodb+srv://user:pass@cluster.mongodb.net/mydb
PORT5000
NODE_ENVproduction

Environment variables panel in the Container App edit form

Click Save — this creates a new Revision with the updated config. By default, Container Apps runs in Single revision mode, which means the old revision is deactivated as soon as the new one activates. For true zero-downtime deploys, switch to Multiple revision mode under the Revisions menu — this keeps the old revision live until the new one is fully healthy.

Why this matters: Injecting variables here means you can swap databases, rotate credentials, or change configuration without rebuilding or re-pushing your Docker image. The image is immutable; the config is not.


Step 5 — Check Logs and Debug

Once deployed, open the Log stream from the left menu. This is your first stop when something isn't working.

Container App log stream showing startup logs


Troubleshooting Checklist

When your app isn't loading, check these before assuming the code is wrong. The most common failures are configuration issues, not code bugs.

SymptomProbable CauseFix
Blank screen / timeoutPort mismatchEnsure Ingress Target Port matches your app's PORT (e.g., 5000)
JSON "Route not found"Code logicThe container is running — check your Express.js route definitions
App crashes on startupMissing env varCheck the Log stream — your app will log which variable is undefined. Add it in Containers > Edit and Deploy
DB connection timeout on bootFirewall blockingYour Container App has dynamic outbound IPs — add them to your MongoDB Atlas or Cosmos DB firewall rules

Database Firewall: The Most Common Silent Failure

Your Container App in, say, francecentral has a dynamic set of outbound IP addresses. If your MongoDB Atlas cluster or Cosmos DB instance has IP-based firewall rules, these IPs must be whitelisted — or your app will silently time out when it tries to connect on startup.

To find your outbound IPs, go to Networking in your Container App and look for the Outbound IP addresses list. Add each of these to your database firewall's IP allowlist.

Alternatively, if security requirements allow it, you can set the MongoDB Atlas firewall to allow access from anywhere (0.0.0.0/0) during development and lock it down before going to production.


The App, Running

Once ingress is configured, environment variables are injected, and the database firewall is open to your outbound IPs — the app loads.

The deployed Node.js app running successfully in the browser


Summary

Azure Container Apps is the right default choice for containerized apps in 2026. Here's the full picture in one place:

  • ACR Tasks (az acr build) — build in the cloud, avoid architecture mismatches
  • Container App Environment — the secure boundary for your apps and logs
  • Ingress Target Port — must match your app's actual PORT, no exceptions
  • Environment Variables — inject via Edit and Deploy, never hardcode in the image
  • Outbound IPs — add to your database firewall, or connections will time out silently
  • Revisions — every config change is a new revision; zero-downtime by default

The pattern is clean once you understand the separation of concerns: your image is immutable, your config is injectable, and the platform handles the rest.

When you push a new version of your app, run az acr build again with a new tag (e.g., v2), then go to Containers > Edit and Deploy and update the image tag. That's the entire update cycle.

AJ

Aziz Jarrar

Full Stack Engineer

Share this article