Containeriser des agents IA (pour qu’ils ne meurent pas au déploiement)

Containeriser et shipper des agents proprement : config runtime, secrets, timeouts, health checks et rollouts. Exemples Python + JS.
Sur cette page
  1. Problème (ça marchait… jusqu’au déploiement)
  2. Pourquoi ça casse en prod
  3. Diagramme : ce que tu déploies vraiment
  4. Code réel : entrypoint friendly container (Python + JS)
  5. Dockerfile (multi-stage, sans secrets)
  6. Panne réelle (avec chiffres)
  7. Compromis
  8. Quand NE PAS containeriser
  9. Checklist copy-paste
  10. Config safe par défaut (YAML)
  11. Implémenter dans OnceOnly (optionnel)
  12. FAQ (3–5)
  13. Pages liées (3–6 liens)

Problème (ça marchait… jusqu’au déploiement)

Agent dans un notebook : OK.

Agent déployé : là où ça fait mal :

  • pas le réseau que tu pensais avoir
  • OOMKill parce que quelqu’un a activé “full trace logging”
  • storms de retries
  • secrets dans l’image (non)

Containeriser, ce n’est pas du théâtre Dockerfile. C’est forcer l’agent à se comporter comme un vrai service.

Pourquoi ça casse en prod

Les agents sont des workloads bizarres :

  • bursty (spikes de trafic = spikes de tokens)
  • I/O (tools) + blocages sur timeouts
  • longues queues (p95 ok, p99 chaos)

Si ton container n’enforce pas budgets/timeout, la prod le fera. Mais via 504s, OOMKills et factures.

Diagramme : ce que tu déploies vraiment

Code réel : entrypoint friendly container (Python + JS)

On garde ça boring :

  • config via env
  • budgets/timeout enforced
  • health endpoint
PYTHON
import os
import time
from dataclasses import dataclass
from typing import Any, Dict


@dataclass(frozen=True)
class Budgets:
  max_steps: int
  max_tool_calls: int
  max_seconds: int


def load_budgets() -> Budgets:
  return Budgets(
      max_steps=int(os.getenv("AGENT_MAX_STEPS", "25")),
      max_tool_calls=int(os.getenv("AGENT_MAX_TOOL_CALLS", "12")),
      max_seconds=int(os.getenv("AGENT_MAX_SECONDS", "60")),
  )


def run_request(task: str, *, budgets: Budgets) -> Dict[str, Any]:
  t0 = time.time()
  steps = 0
  tool_calls = 0

  while True:
      steps += 1
      if steps > budgets.max_steps:
          return {"output": "", "stop_reason": "max_steps"}
      if tool_calls > budgets.max_tool_calls:
          return {"output": "", "stop_reason": "max_tool_calls"}
      if time.time() - t0 > budgets.max_seconds:
          return {"output": "", "stop_reason": "max_seconds"}

      return {"output": "ok", "stop_reason": "finish"}


def health() -> Dict[str, str]:
  return {"ok": "true"}
JAVASCRIPT
export function loadBudgets() {
return {
  maxSteps: Number(process.env.AGENT_MAX_STEPS ?? 25),
  maxToolCalls: Number(process.env.AGENT_MAX_TOOL_CALLS ?? 12),
  maxSeconds: Number(process.env.AGENT_MAX_SECONDS ?? 60),
};
}

export function runRequest(task, { budgets }) {
const t0 = Date.now();
let steps = 0;
let toolCalls = 0;

while (true) {
  steps += 1;
  if (steps > budgets.maxSteps) return { output: "", stop_reason: "max_steps" };
  if (toolCalls > budgets.maxToolCalls) return { output: "", stop_reason: "max_tool_calls" };
  if ((Date.now() - t0) / 1000 > budgets.maxSeconds) return { output: "", stop_reason: "max_seconds" };

  return { output: "ok", stop_reason: "finish" };
}
}

export function health() {
return { ok: true };
}

Dockerfile (multi-stage, sans secrets)

DOCKERFILE
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY . .
EXPOSE 3000
CMD ["npm","run","start"]

Panne réelle (avec chiffres)

On a déployé avec “debug logging” activé par défaut. Résultats tools complets loggés à chaque appel.

Impact :

  • mémoire monte → OOMKill
  • retries amplifient la charge
  • ~12% de failure rate
  • astreinte : ~3 heures (logs énormes et pourtant pas utiles)

Fix :

  1. logging échantillonné + redaction (/fr/observability-monitoring/agent-logging)
  2. budgets enforced au runtime
  3. kill switch pour désactiver des tools coûteux en incident

Compromis

  • Timeouts serrés réduisent la queue et peuvent réduire la qualité.
  • Plus de logs aide le debug et fait mal en coût/privacy.
  • Un container par agent = simple et cher. Mutualiser = moins cher et plus dur.

Quand NE PAS containeriser

Si tu n’opères pas ça comme un service, n’overbuild pas. Mais dès qu’un user peut déclencher le run, tu opères un service. Voilà.

Checklist copy-paste

  • [ ] Budgets via env + enforced au runtime
  • [ ] Tool gateway : timeouts/retries/allowlists
  • [ ] Health + readiness checks
  • [ ] Secrets via plateforme (pas baked)
  • [ ] Kill switch config
  • [ ] Logs structurés + sampling; redaction PII par défaut

Config safe par défaut (YAML)

YAML
runtime:
  env:
    AGENT_MAX_STEPS: 25
    AGENT_MAX_TOOL_CALLS: 12
    AGENT_MAX_SECONDS: 60
tools:
  allowlist: ["search.read", "http.get"]
  timeouts_ms: { default: 8000 }
  retries: { max: 2, backoff_ms: [200, 800] }
observability:
  sampled_tool_results: true
  result_sample_rate: 0.01
rollout:
  canary_percent: 10
  rollback_on_error_rate: 0.05

Implémenter dans OnceOnly (optionnel)

Implémenter dans OnceOnly
Budgets + defaults tool gateway qui survivent au déploiement.
Utiliser dans OnceOnly
# onceonly-python: tool allowlist + governed tool call
import os
from onceonly import OnceOnly

client = OnceOnly(
    api_key=os.environ["ONCEONLY_API_KEY"],
    timeout=5.0,
    max_retries_429=2,
)

agent_id = "billing-agent"

client.gov.upsert_policy({
    "agent_id": agent_id,
    "allowed_tools": ["search.read", "http.get"],
    "max_actions_per_hour": 200,
    "max_spend_usd_per_day": 10.0,
})

res = client.ai.run_tool(
    agent_id=agent_id,
    tool="http.get",
    args={"url": "https://example.com/health"},
    spend_usd=0.001,
)
if not res.allowed:
    raise RuntimeError(res.policy_reason)

FAQ (3–5)

Je dois mettre prompts/modèles dans l’image ?
Code oui, secrets non. Prompts versionnés dans le repo. Modèles = config. Secrets = runtime only.
Le fail le plus courant au déploiement ?
Timeouts + retries qui se renforcent. 504s puis storm. Retries à un seul endroit + budgets.
Kubernetes est obligatoire ?
Non. Budgets, observabilité, rollback. Ça peut marcher sans K8s.
Rollback safe ?
Kill switch + image/prompt version précédente ready. Rollback sur error‑rate + spikes de spend.

Pages liées (3–6 liens)

Pas sur que ce soit votre cas ?

Concevez votre agent ->
⏱️ 5 min de lectureMis à jour Mars, 2026Difficulté: ★★★
Intégré : contrôle en productionOnceOnly
Ajoutez des garde-fous aux agents tool-calling
Livrez ce pattern avec de la gouvernance :
  • Budgets (steps / plafonds de coût)
  • Permissions outils (allowlist / blocklist)
  • Kill switch & arrêt incident
  • Idempotence & déduplication
  • Audit logs & traçabilité
Mention intégrée : OnceOnly est une couche de contrôle pour des systèmes d’agents en prod.
Auteur

Cette documentation est organisée et maintenue par des ingénieurs qui déploient des agents IA en production.

Le contenu est assisté par l’IA, avec une responsabilité éditoriale humaine quant à l’exactitude, la clarté et la pertinence en production.

Les patterns et recommandations s’appuient sur des post-mortems, des modes de défaillance et des incidents opérationnels dans des systèmes déployés, notamment lors du développement et de l’exploitation d’une infrastructure de gouvernance pour les agents chez OnceOnly.