Azure Functions & Container Apps for Go
Microsoft Azure hosts Go through two main paths: Azure Functions with a custom handler process, and Azure Container Apps (ACA) for OCI images with scale rules.
Both accept the same static Go binary patterns you use on AWS and GCP - the differences are hosting contracts and identity wiring.
Summary
Functions custom handlers let Go own an HTTP server on a platform-assigned port while the Functions host routes trigger events to it.
Container Apps run your image like Cloud Run with optional scale-to-zero and KEDA triggers (HTTP, queues, cron).
Use managed identity and Key Vault references instead of connection strings in app settings.
Recipe
Quick-reference recipe card - copy-paste ready.
// cmd/functions/main.go - custom handler skeleton
http.HandleFunc("/api/HttpExample", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"message":"hello from go"}`))
})
port := os.Getenv("FUNCTIONS_CUSTOMHANDLER_PORT")
log.Fatal(http.ListenAndServe(":"+port, nil))# Container Apps deploy sketch
az containerapp up --name go-api --source . --ingress external --target-port 8080When to reach for this:
- Azure-first orgs needing event triggers (Storage, Service Bus) with Go business logic.
- HTTP microservices on ACA with scale-to-zero and internal ingress options.
- Hybrid teams running Go on Azure alongside existing .NET infrastructure.
Working Example
package main
import (
"encoding/json"
"log/slog"
"net/http"
"os"
)
type response struct {
Message string `json:"message"`
}
func main() {
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil)))
mux := http.NewServeMux()
mux.HandleFunc("POST /api/process", func(w http.ResponseWriter, r *http.Request) {
var payload map[string]any
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
http.Error(w, "bad json", http.StatusBadRequest)
return
}
slog.Info("event", "keys", len(payload))
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(response{Message: "processed"})
})
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
})
port := os.Getenv("FUNCTIONS_CUSTOMHANDLER_PORT")
if port == "" {
port = os.Getenv("PORT")
}
if port == "" {
port = "8080"
}
slog.Info("listening", "port", port)
if err := http.ListenAndServe(":"+port, mux); err != nil {
slog.Error("listen failed", "err", err)
os.Exit(1)
}
}What this demonstrates:
- Dual port env support:
FUNCTIONS_CUSTOMHANDLER_PORTfor Functions,PORTfor Container Apps. - JSON handler suitable for HTTP-triggered Functions or ACA ingress.
- Structured logging for Application Insights ingestion.
Deep Dive
How It Works
- Custom handler Functions spawn your Go process; the Functions host forwards HTTP trigger requests to routes you register.
- Non-HTTP triggers (Queue, Timer) still arrive as HTTP calls to your handler endpoints per the Functions custom handler contract.
- Container Apps place your container in a managed environment with ingress, secrets, and replicas controlled by scale rules.
Functions vs Container Apps
| Dimension | Azure Functions (custom handler) | Azure Container Apps |
|---|---|---|
| Deploy unit | Binary + function.json host config | Container image |
| Triggers | Built-in bindings catalog | KEDA scalers (HTTP, SB, cron) |
| Scale | Consumption or Premium plans | Min/max replicas, scale to zero |
| Go style | net/http on custom port | Same as Cloud Run patterns |
| Identity | System-assigned managed identity | Managed identity on app |
host.json and function.json Essentials
{
"bindings": [{
"authLevel": "function",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": ["post"],
"route": "process"
}, {
"type": "http",
"direction": "out",
"name": "res"
}]
}Pair each trigger with a route your Go mux implements; mismatched routes return 404 from your process while the host still bills the invocation.
Container Apps Ingress
- External ingress exposes a public HTTPS endpoint (similar to Cloud Run public services).
- Internal ingress limits traffic to the ACA environment VNet.
- Set target port to the port your Go binary listens on inside the container.
Gotchas
- Missing
FUNCTIONS_CUSTOMHANDLER_PORT- Functions host assigns the port; hardcoding 8080 fails locally underfunc start. Fix: Always bindos.Getenv("FUNCTIONS_CUSTOMHANDLER_PORT"). - Connection strings in app settings - Keys rotate and leak via portal exports. Fix: Managed identity + Key Vault secret references.
- Cold start on Consumption plan - Custom handler still pays process start cost. Fix: Premium plan or move steady HTTP to Container Apps.
- Ignoring ACA SIGTERM - Same graceful shutdown requirements as Cloud Run. Fix:
signal.Notify+http.Server.Shutdown. - Mixing Functions bindings with heavy HTTP middleware - Functions host expects quick responses; long work belongs in queue-triggered flows. Fix: Offload to Service Bus and a worker ACA app.
- Dockerfile not listening on 0.0.0.0 - Binding
127.0.0.1breaks ingress health probes. Fix: Listen on0.0.0.0/:+ port.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Azure App Service | Simple Windows/Linux web apps | You need KEDA queue scaling |
| AKS | Kubernetes operators and CRDs | Single Go API with minimal ops |
| AWS Lambda / Cloud Run | Multi-cloud standard | Azure identity and networking are fixed |
| Azure API Management only | Gateway policies without compute | You still need a Go backend somewhere |
FAQs
Is Go a supported in-process Functions language?
Azure's first-class in-process languages differ by runtime generation.
Production Go on Functions typically uses the custom handler model with your own HTTP server.
Can Container Apps scale to zero?
Yes with HTTP or event-driven KEDA scalers configured to allow zero replicas.
Cold starts resemble Cloud Run - keep images lean.
How do I wire Application Insights?
Enable OpenTelemetry or use the Application Insights Go SDK; emit JSON logs to stdout for automatic correlation when configured in the host.
Should I use Functions or ACA for a REST API?
ACA is usually simpler for always-available HTTP with standard middleware.
Functions excel when you need the bindings catalog for Timer/Queue/Blob triggers alongside small HTTP endpoints.
Can I reuse my Cloud Run Dockerfile?
Often yes - change the deploy command to az containerapp up or pipeline tasks and verify port/health check settings.
Related
- Cloud Deploy Basics - shared module layout and Docker patterns
- Go in the Cloud: Lambda, Cloud Run, and Beyond - cross-cloud comparison
- GCP Cloud Run & AWS Fargate Patterns - container health and shutdown
- Secrets, IAM & Workload Identity - managed identity parallels
- Serverless Go Architecture Decision Guide - pick the right Azure shape
- Cloud & Serverless Best Practices - observability on Azure
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).