
Deploying Containers to Azure App Services
I’ve been running a project on an Azure App Service for a while now, and I’ve landed on a setup I’m pretty happy with: a single Docker container that bundles the Angular frontend, NestJS API, and Caddy as the web server. The whole thing builds in GitHub Actions, pushes to Azure Container Registry (ACR), and deploys to App Service with zero-downtime swaps.
I wanted to share what I’ve learned in case it helps someone else avoid the same rabbit holes I fell into.
Why I Went with App Service + Containers
I evaluated a few options before settling on this approach. VMs felt like overkill for what I needed, and I didn’t want to manage OS patches and SSH hardening. Kubernetes was tempting but honestly too much infrastructure for a project of this size.
App Service with containers hit the sweet spot:
- Managed hosting – Azure handles SSL, scaling, health probes, and platform patching. One less thing for me to worry about.
- Immutable deploys – The exact image built in CI is what runs in prod. No “it worked on my machine” issues.
- Smaller attack surface – No public SSH to worry about. When I need a shell, Azure’s Web SSH connects through an internal port.
- Easy rollbacks – If something goes wrong, I can swap back to any previous image tag.
The Architecture
At runtime, I’m running a single container with three pieces:
- Caddy on port 80 – Serves the Angular static bundle and reverse-proxies
/api/*to the backend. Caddy is ridiculously easy to configure compared to nginx. - NestJS API on port 3000 – Launched with
foreverso it restarts automatically if it crashes. - Web SSH on 2222 – Only used by Azure’s portal/CLI for debugging. Not exposed to the internet.
How Deploys Work
The flow is pretty straightforward:
1. I push to git, GitHub Actions kicks off
2. The workflow builds the image (frontend + backend together), tags it, and pushes to ACR
3. Then it tells App Service to pull that image and start it up
4. Caddy serves the frontend and proxies API calls to NestJS
That’s it. No fancy orchestration, no deployment scripts to maintain.
The Dockerfile Pattern
I use a multi-stage build to keep the final image small:
- Stage 1 (backend) – Install deps, build NestJS to
dist/ - Stage 2 (frontend) – Install deps, build the Angular bundle
- Final stage – Install Node + Caddy, copy the built assets, and set up the startup script
Here’s the pattern (simplified):
The GitHub Actions Workflow
Here’s the minimal CI I use. It assumes you’ve already set up ACR and your Web App for Containers, and added AZURE_CREDENTIALS, ACR_NAME, and APP_NAME to your repository secrets.
A Few Things I Learned the Hard Way
Secrets and Environment Config
I keep Azure credentials as AZURE_CREDENTIALS (the Service Principal JSON), the registry name as ACR_NAME, and app names per environment (`QA_APP_NAME`, PROD_APP_NAME). Database settings live in App Service Configuration, not in the image – I use Azure Managed Identity for passwordless SQL auth.
Caddy Routing Matters
Make sure your Caddyfile handles /api/* before the catch-all for static files. I spent an embarrassing amount of time debugging why my API calls were returning HTML.
Questions or thoughts? I’d love to hear what’s working (or not working) for your deployments. Hit me up on X @chadmichel – always happy to chat about this stuff.


