Azure Deployment Journey: OCaml Web Application with Dream Framework

A comprehensive guide to deploying OCaml applications on Azure App Service using Azure Container Registry

Project: Chaufr – Personal drivers, on demand, in your ownΒ vehicle
Tech Stack: OCaml, Dream Framework, PostgreSQL, Azure App Service, GitHub Actions

What started as a simple deployment task evolved into a comprehensive exploration of Azure's container ecosystem, authentication mechanisms, and optimization strategies. This chronicles our journey deploying an OCaml web application to Azure App Service and the valuable lessons learned along the way.

The Challenge: OCaml on Azure

Azure App Service has excellent built-in support for popular languages like .NET, Python, Node.js, and Java. However, OCaml is not natively supported, presenting several challenges:

Why We Chose Azure Container Registry (ACR)

Given the lack of native OCaml support, we decided on a containerized approach using Azure Container Registry:

Benefits:

Our Solution Architecture

High-Level Architecture

GitHub Repository β†’ GitHub Actions β†’ Azure Container Registry β†’ Azure App Service
        ↓                ↓                    ↓                      ↓
   OCaml Source       Docker Build        Container Image       Running App
        ↓                ↓                    ↓                      ↓
   Dream Framework    Multi-stage Build   Optimized Image    PostgreSQL Connection

Components Used

Major Challenges and Solutions

1. πŸ” Entra ID Permission Hurdle

Challenge: GitHub Actions couldn't authenticate with Azure services using OIDC (OpenID Connect).

Error Messages:

ERROR: The user, group or application does not have permissions to read the subscription

Root Cause:

Solution:

# GitHub Actions workflow configuration
- name: Azure Login (OIDC)
  uses: azure/login@v1
  with:
    client-id: ${{ secrets.AZURE_CLIENT_ID }}
    tenant-id: ${{ secrets.AZURE_TENANT_ID }}
    subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

Steps to Fix:

  1. Created service principal: gh-chaufr-deploy
  2. Assigned Contributor role to resource group
  3. Configured federated identity credentials for GitHub repository
  4. Updated GitHub secrets with correct client ID

2. 🐳 Docker File Drama

Challenge: Multiple Docker-related issues that prevented successful container builds.

Issue 2.1: Missing System Dependencies

Error:

ERROR while compiling conf-libcurl.2
"curl-config": command not found.
The packages you requested declare the following system dependencies:
    libcurl4-gnutls-dev libgmp-dev

Solution - Multi-stage Dockerfile:

# Build stage - All development dependencies
FROM ocaml/opam:debian-12-ocaml-5.3 AS builder
WORKDIR /app

RUN sudo apt-get update && sudo apt-get install -y --no-install-recommends \
    libpq-dev \
    pkg-config \
    ca-certificates \
    git \
    libcurl4-openssl-dev \
    libgmp-dev \
    libev-dev && sudo rm -rf /var/lib/apt/lists/*

# Runtime stage - Minimal production image
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
    libpq5 \
    ca-certificates \
    libcurl4 \
    libgmp10 \
    libev4 && rm -rf /var/lib/apt/lists/*

Issue 2.2: Package Name Differences Between Ubuntu and Debian

Challenge: Local development used Ubuntu 24.04, but Docker used Debian.

Package Mapping Between Ubuntu 24.04 and Debian Bookworm

Component Ubuntu 24.04 Debian Bookworm Notes
libcurl (dev) libcurl4-gnutls-dev libcurl4-openssl-dev
libcurl (runtime) libcurl4-gnutls libcurl4
PostgreSQL libpq5 libpq5 βœ… Same name
GMP libgmp10 libgmp10 βœ… Same name

3. πŸ“¦ Opam Environment and Dune Build Issues

Challenge: OCaml build tools weren't available in the container environment.

Error:

/bin/sh: 1: dune: not found

Root Cause: The opam environment wasn't activated before running dune build.

Solution:

# Activate opam environment before building
RUN eval $(opam env) && dune build --profile=release ./bin/main.exe

4. πŸ”— ACR Pull Permission Challenge

Challenge: Azure App Service couldn't pull container images from Azure Container Registry.

Error:

WARNING: No credential was provided to access Azure Container Registry. Trying to look up...
WARNING: Retrieving credentials failed

Solutions Explored:

Option 1: ACR Admin Access (Initial Solution)

- name: Enable ACR admin access and get credentials
  run: |
    az acr update -n ${{ secrets.ACR_NAME }} --admin-enabled true
    ACR_USERNAME=$(az acr credential show -n ${{ secrets.ACR_NAME }} --query username -o tsv)
    ACR_PASSWORD=$(az acr credential show -n ${{ secrets.ACR_NAME }} --query passwords[0].value -o tsv)
- name: Configure ACR access via Managed Identity
  run: |
    PRINCIPAL_ID=$(az webapp identity show -g ${{ secrets.RESOURCE_GROUP }} -n $WEBAPP_NAME --query principalId -o tsv)
    az role assignment create \
      --assignee $PRINCIPAL_ID \
      --scope /subscriptions/${{ secrets.AZURE_SUBSCRIPTION_ID }}/resourceGroups/${{ secrets.RESOURCE_GROUP }}/providers/Microsoft.ContainerRegistry/registries/${{ secrets.ACR_NAME }} \
      --role "AcrPull"

5. 🌐 The Interface Discovery: 0.0.0.0 vs 127.0.0.1

Challenge: OCaml application was accessible locally but returned HTTP 503 errors on Azure.

Root Cause: Network interface binding issue.

Local Development (Working):

Dream.run ~interface:"127.0.0.1" ~port:8080 app

Azure App Service (Failing):

The Critical Discovery:

let get_interface () =
  match Sys.getenv_opt "INTERFACE" with
  | Some interface -> interface
  | None -> (
      match Sys.getenv_opt "WEBSITE_SITE_NAME" with
      | Some _ -> "0.0.0.0" (* Azure App Service - allows external connections *)
      | None -> "127.0.0.1" (* Local development - secure default *))

Key Insight:

6. πŸ—οΈ Pinned Package Management

Challenge: Using a pinned OCaml package (simple_dotenv) that wasn't available in standard opam repository.

Solution:

# Pin to specific commit hash for caching stability
RUN opam update && \
    opam pin add -y simple_dotenv.1.0.0 git+https://github.com/Lomig/simple_dotenv.git#05ef4a35eff29784abc3f454ee36163f3ae48747

Benefits:

Key Learnings

1. Azure App Service Container Networking

Discovery: Azure App Service uses a reverse proxy architecture that requires containers to bind to 0.0.0.0 rather than 127.0.0.1.

Implementation:

(* Dynamic interface selection based on environment *)
let get_interface () =
  match Sys.getenv_opt "WEBSITE_SITE_NAME" with
  | Some _ -> "0.0.0.0" (* Azure automatically sets this *)
  | None -> "127.0.0.1" (* Local development *)

2. GitHub Secrets Management Strategy

What We Used:

Security Best Practices:

3. Multi-Stage Docker Build Optimization

Build Stage (Large):

FROM ocaml/opam:debian-12-ocaml-5.3 AS builder
# ~500MB with all development tools

Runtime Stage (Small):

FROM debian:bookworm-slim
# ~150MB with only runtime dependencies

Result: 70% reduction in final image size.

4. OCaml-Specific Deployment Patterns

Dependency Management:

# Copy opam metadata first for better caching
COPY dune-project chaufr.opam ./
RUN opam install -y --deps-only .

# Copy source code last
COPY . .

Environment Activation:

RUN eval $(opam env) && dune build --profile=release ./bin/main.exe

Cost Optimization Strategies

1. GitHub Actions Credits Conservation

Problem: Limited GitHub Actions credits on Pro account.

Docker Build Caching Strategy:

- name: Build and push with cache
  uses: docker/build-push-action@v4
  with:
    cache-from: type=gha
    cache-to: type=gha,mode=max

Pinned Dependencies for Cache Stability:

Expected Savings:

2. Azure Container Registry Pricing

ACR Standard Tier Benefits:

Our Usage Pattern:

3. Azure App Service B1 Instance

Why B1 Basic Plan:

Performance Characteristics:

Final Architecture

Production Deployment Flow

1. Code Push to GitHub

git push origin main

2. GitHub Actions Triggers

3. Azure App Service

Health Check Endpoints

GET /health              -> "ok" (basic liveness)
GET /health/detailed     -> Database connection status
GET /diagnostic          -> Environment variables (masked)
GET /status              -> Container runtime information

Environment Variables Configuration

Azure App Service automatically provides:

GitHub Actions configures:

Security Implementation

Credential Masking:

let mask_database_url url =
  try
    (* Replace password with *** in connection strings *)
    let protocol_end = String.index url '@' in
    let colon_pos = String.rindex_from url protocol_end ':' in
    let masked_part = String.sub url 0 (colon_pos + 1) ^ "***@" in
    let remaining = String.sub url (protocol_end + 1) (String.length url - protocol_end - 1) in
    masked_part ^ remaining
  with
  | Not_found -> "postgresql://***:***@***"

Lessons for Future Developers

1. OCaml on Azure: Containerization is the Way

Don't try to:

Do instead:

2. Authentication: OIDC Over Service Principals

Modern approach:

permissions:
  id-token: write
  contents: read

Benefits:

3. Environment Detection Pattern

Robust environment detection:

let detect_environment () =
  match Sys.getenv_opt "WEBSITE_SITE_NAME" with
  | Some site_name -> `Azure_App_Service site_name
  | None -> (
      match Sys.getenv_opt "GITHUB_ACTIONS" with
      | Some _ -> `GitHub_Actions
      | None -> `Local_Development)

4. Cost Management Strategies

For GitHub Actions:

For Azure:

5. Debugging Techniques

Container startup issues:

- name: Tail logs for debugging
  run: |
    az webapp log tail -n $WEBAPP_NAME -g ${{ secrets.RESOURCE_GROUP }} --timeout 60 || true

Health check debugging:

- name: Enhanced Health Check
  run: |
    # Test multiple endpoints with detailed error reporting
    for endpoint in "/health" "/diagnostic" "/status" "/"; do
      echo "Testing $endpoint..."
      HTTP_STATUS=$(curl -s -o response.txt -w "%{http_code}" "$APP_URL$endpoint" || true)
      echo "Status: $HTTP_STATUS"
      cat response.txt
    done

Performance Metrics

Build Times

Deployment Times

Runtime Performance

Future Improvements

1. Enhanced Monitoring

2. Security Hardening

3. Scalability Enhancements

Conclusion

This journey taught us that deploying OCaml applications to Azure App Service is entirely feasible with the right approach. The key insights:

βœ… Containerization enables OCaml on Azure
βœ… Network interface binding (0.0.0.0) is critical for Azure App Service
βœ… OIDC authentication is more secure than traditional service principals
βœ… Docker caching strategies significantly reduce CI/CD costs
βœ… Multi-stage builds optimize both build time and image size

πŸ’‘ The labor of love was worth it - we now have a robust, secure, and cost-effective deployment pipeline for OCaml applications on Azure.

For developers embarking on similar journeys: embrace the complexity, document your discoveries, and remember that the modern cloud platforms can support virtually any technology stack with the right architectural decisions.


This documentation represents our collective learning from deploying the Chaufr platform to Azure. We hope it serves as a valuable resource for the OCaml and Azure communities.

Live Application: Will be shared soon, be on the look out.
Technology Stack: OCaml 5.3, Dream Framework, TyXML, PostgreSQL, Azure App Service, GitHub Actions.

Hey, this site is part of ring.muhokama.fun!