---
title: "Deploy on a Managed Container Platform"
url: "https://docs.caracal.run/v1.0/operations/managed-container-platforms/"
markdown_url: "https://docs.caracal.run/markdown/v1.0/operations/managed-container-platforms.md"
description: "Run the published Caracal images on Azure Container Apps without a Kubernetes cluster, and know which production guarantees you trade away."
page_type: "workflow"
concepts: []
requires: []
---

# Deploy on a Managed Container Platform

Canonical URL: https://docs.caracal.run/v1.0/operations/managed-container-platforms/
Markdown URL: https://docs.caracal.run/markdown/v1.0/operations/managed-container-platforms.md
Description: Run the published Caracal images on Azure Container Apps without a Kubernetes cluster, and know which production guarantees you trade away.
Page type: workflow
Concepts: none
Requires: none

---

The published images are role-agnostic: one Go image carries the STS, Gateway, and Audit binaries, one Node image carries API and Coordinator, and the container start command selects the role. Any platform that runs an OCI image with a custom start command, file-projected secrets, and HTTP probes can therefore run Caracal from released artifacts with no build step of your own.

`infra/containerPlatform` renders that deployment. `topology.yaml` describes the stack once, in platform-neutral terms; a target module maps it onto a provider. Azure Container Apps ships today.

## Use Criteria

Choose this path when you want a managed runtime, per-service scaling, and revision-based rollout without operating a cluster, and when the traded guarantees below are acceptable. Choose [Deploy with Helm](/v1.0/operations/kubernetes-helm/) when any of them are not.

## What You Trade Away

The Helm chart enforces controls that container platforms do not expose. None of them are optional in a hardened deployment; on this path you replace them or accept their absence.

| Control | Helm chart | Container platform | Consequence |
| --- | --- | --- | --- |
| East-west network policy | Default-deny NetworkPolicy per service | No pod-level policy; subnet NSGs only | Any workload in the environment can reach Audit and Coordinator |
| Container hardening | `readOnlyRootFilesystem`, dropped capabilities, `runAsNonRoot`, seccomp | Not configurable | The image's own non-root user is the only remaining boundary |
| Audit spill durability | Per-replica persistent volume | Replica-local ephemeral storage by default | A replaced replica loses audit evidence it had not drained to Redis |
| Disruption budgets | PodDisruptionBudget per service | None | Platform maintenance can take replicas below your intended floor |
| Failure-domain spread | Topology spread constraints and anti-affinity | Platform-chosen placement | Replica distribution across zones is not something you assert |
| Scheduling priority | PriorityClass keeps the control plane above best-effort work | None | No eviction ordering under pressure |
| Migration ordering | `pre-install`/`pre-upgrade` hook, automatic | Explicit job run before the rollout | Ordering depends on the deploy flow, not the platform |
| Vertical headroom | Node-sized | 4 vCPU and 8 GiB per replica | Scale out rather than up; STS and Gateway hit this first |
| Metrics collection | ServiceMonitor and PrometheusRule | Not consumed by the platform | Alerting must be rebuilt on the provider's monitor |

Kubernetes remains the recommended path for production because those controls are the deployment's security and availability posture, not decoration. This path is appropriate for evaluation, staging, lower-traffic production, and teams with no cluster to run.

What does not change: horizontal autoscaling, replica floors and ceilings, health-gated rolling updates, TLS ingress, and per-service resource limits are all expressed natively. Revision traffic splitting is a stronger canary primitive than the chart's rolling update.

## Prerequisites

Operate managed Postgres reachable with migration privileges, managed Redis with Streams and `noeviction`, a secret manager holding the runtime credentials, a registry, and a workload identity with pull and secret-read access. Review [Choose a Cloud Profile](/v1.0/operations/cloud-native-profiles/) for the dependency contract, which is identical here.

## Procedure

1. Import the released images into your registry so deployments pull from a private, in-region source. No rebuild is involved, and the digest and its provenance are preserved:

   ```bash
   for image in caracal-go caracal-node caracal-web caracal-postgres; do
     az acr import --name <registry> \
       --source ghcr.io/garudex-labs/${image}:v0.2.1 \
       --image ${image}:v0.2.1
   done
   ```

2. Store the runtime credentials in the secret manager. `render.mjs` writes `secrets.txt` listing every required name in the platform's naming form; the key material itself is the same set the Helm chart consumes, described in [Rotate Keys and Secrets](/v1.0/operations/key-management/).

3. Download the deployment tooling for the release you are deploying. It is a published, checksummed, attested artifact, so no source checkout is involved:

   ```bash
   tag=v0.2.1
   gh release download "$tag" --repo Garudex-Labs/caracal \
     --pattern "caracal-deploy-${tag}.tar.gz" --pattern SHA256SUMS
   grep " caracal-deploy-${tag}.tar.gz$" SHA256SUMS | sha256sum --check --strict -
   gh attestation verify "caracal-deploy-${tag}.tar.gz" --repo Garudex-Labs/caracal
   tar -xzf "caracal-deploy-${tag}.tar.gz"
   npm --prefix caracal-deploy install --omit=dev
   ```

4. Copy `caracal-deploy/containerPlatform/examples/azure.yaml`, set your endpoints, public origins, operator admission, and provider wiring, and render:

   ```bash
   node caracal-deploy/containerPlatform/render.mjs --config deployment.yaml --out ./rendered
   ```

   The `console` block carries operator sign-in: `operatorEmails` lists the addresses or `@domain` suffixes admitted to the console, and `console.auth` names at least one sign-in method - a Google or GitHub client id with the name of its client secret in the secret manager, or an SMTP transport. Rendering fails without a method, on a plaintext public origin, debug logging in `stable` mode, a missing identity, or an unresolvable value. Review the manifests before applying them.

5. Apply in order. Schema migrations must finish before any service revision rolls, because a release's migrations are expand-only against the version still serving:

   ```bash
   CARACAL_RESOURCE_GROUP=<group> bash caracal-deploy/containerPlatform/scripts/deployAzure.sh ./rendered
   ```

6. Restore what the platform does not provide: subnet-level network restrictions, provider-native metrics and alert rules equivalent to the chart's, and backup and restore for Postgres and Redis.

## Durable Audit Spill

STS, Gateway, and Audit write audit evidence to disk when Redis is unreachable and drain it on the next start. With ephemeral storage that evidence does not survive replica replacement.

To keep it, attach an NFS file share and set `azure.stateStorageName`. Use NFS rather than SMB: the spill path fsyncs each file and its parent directory, an SMB share does not reliably honor that, and a failed fsync is accounted as lost evidence and fails readiness.

## Verify

Confirm secrets resolve into the container, migrations completed, and every service answers `/ready`. Run `caracal-deploy/smokeTest.sh` against the ingress hosts, then a canary token exchange and Gateway request, and locate the resulting audit evidence. Confirm internal-only services are not externally reachable.

## Rollback or Recovery

Roll back by activating the previous revision; it is immediate and does not rebuild. Migrations are never reversed by a revision rollback, which is why they stay expand-only. Keep the previous rendered manifests alongside the release they describe.

## Teardown

Which teardown is correct depends on whether the environment holds anything you need to keep.

For a **long-lived environment**, `caracal-deploy/containerPlatform/scripts/destroyAzure.sh` removes only the container apps and jobs it created. The database, cache, vault, registry, and the environment itself survive, so audit evidence and credentials are not destroyed along with the compute.

For a **temporary environment**, put every resource in one resource group and delete the group:

```bash
az group delete --name <group> --yes
```

This is deliberately manual and deliberately not automated. It is atomic and complete, whereas removing only the applications leaves the database, cache, registry, and environment billing indefinitely. Confirm the result in Cost Analysis rather than trusting the command's exit code: a teardown is proven by the spend reaching zero.

## Next Step

Use [Monitor Health and Metrics](/v1.0/operations/observability/) to rebuild the signals the chart's ServiceMonitor and alert rules would have provided.
