# Tutorial: Deploy a production-ready container

> Tutorial: Deploy a production-ready container - Deploy a production Workload with a locked artifact,
> importance, sharing, and monitoring using cURL, the CLI, or Pulumi.

This Markdown file sits beside the HTML page at the same path (with a `.md` suffix). It summarizes the topic and lists links for tools and LLM context.

Companion generated at `2026-08-14T12:37:00.288088+00:00` (UTC).

## Primary page

- [Tutorial: Deploy a production-ready container](https://docs.datarobot.com/ja/docs/workload-api/create-workloads/tutorial-production-ready-container.html.md): Full documentation for this topic (Markdown sidecar).

## Sections on this page

- [Locked-artifact Workloads at a glance](https://docs.datarobot.com/ja/docs/workload-api/create-workloads/tutorial-production-ready-container.html.md#locked-artifact-workloads-at-a-glance): In-page section heading.
- [前提条件](https://docs.datarobot.com/ja/docs/workload-api/create-workloads/tutorial-production-ready-container.html.md#prerequisites): In-page section heading.
- [Create the Workload](https://docs.datarobot.com/ja/docs/workload-api/create-workloads/tutorial-production-ready-container.html.md#create-the-workload): In-page section heading.
- [Lock the artifact](https://docs.datarobot.com/ja/docs/workload-api/create-workloads/tutorial-production-ready-container.html.md#lock-the-artifact): In-page section heading.
- [Wait for running](https://docs.datarobot.com/ja/docs/workload-api/create-workloads/tutorial-production-ready-container.html.md#wait-for-running): In-page section heading.
- [Invoke the service](https://docs.datarobot.com/ja/docs/workload-api/create-workloads/tutorial-production-ready-container.html.md#invoke-the-service): In-page section heading.
- [Govern the Workload](https://docs.datarobot.com/ja/docs/workload-api/create-workloads/tutorial-production-ready-container.html.md#govern-the-workload): In-page section heading.
- [Observe](https://docs.datarobot.com/ja/docs/workload-api/create-workloads/tutorial-production-ready-container.html.md#observe): In-page section heading.
- [サマリー](https://docs.datarobot.com/ja/docs/workload-api/create-workloads/tutorial-production-ready-container.html.md#summary): In-page section heading.
- [次のステップ](https://docs.datarobot.com/ja/docs/workload-api/create-workloads/tutorial-production-ready-container.html.md#next-steps): In-page section heading.

## Documentation content

> [!NOTE] プレミアム機能
> Workload APIはプレミアム機能です。 この機能を有効にする方法については、DataRobotの担当者または管理者にお問い合わせください。

Deploy a containerized AI service with full governance: a locked artifact, `importance`, sharing, and monitoring. Unlike a draft Workload (see [Hello, Workload](https://docs.datarobot.com/ja/docs/workload-api/get-started-workloads/tutorial-hello-world.html.md)), this one is long-lived and production-grade.

This tutorial deploys a FastAPI-based agent service ( [otkachnlp/fastapi-server-example](https://hub.docker.com/r/otkachnlp/fastapi-server-example), a publicly available third-party image, not published by DataRobot) that exposes:

- OpenAI-compatible /chat/completions , connected to the DataRobot LLM Gateway—no separate LLM deployment required.
- A LangGraph /agent endpoint: a ReAct agent with ArXiv search.
- /healthz , /readyz , and /health for liveness, readiness, and detailed status.

Pick the tab that matches how you want to work: cURL calls the REST API directly, the CLI wraps the same calls in `dr workload` / `dr artifact` commands (falling back to `curl` where no subcommand exists), and Pulumi declares the locked artifact and Workload as code. Prefer a runnable notebook instead:

[View as a runnable notebook](https://docs.datarobot.com/ja/docs/workload-api/create-workloads/nb-tutorial-production-ready-container.html.md)

## Locked-artifact Workloads at a glance

| プロパティ | 値 |
| --- | --- |
| Lifetime | Indefinite. Persists until explicitly stopped or deleted. |
| Artifact mutability | Immutable once locked. |
| importance | Optional; defaults to low. Set explicitly for production (critical, high, moderate, or low). |
| Workloads per artifact | Unlimited. One locked artifact can back many Workloads. |
| 置換 | Supported. Replace locked with locked only. |

See [Workload concepts](https://docs.datarobot.com/ja/docs/workload-api/create-workloads/workload-concepts.html.md#choose-draft-vs-locked) for the full draft-vs-locked comparison.

## 前提条件

**cURL:**
A terminal with
curl
and
jq
.
Your DataRobot API endpoint and token:

```
export DATAROBOT_ENDPOINT="https://app.datarobot.com/api/v2"
export DATAROBOT_API_TOKEN="<your-api-token>" 
```

**CLI:**
The
DataRobot CLI
(
dr
), authenticated, with Workload commands enabled (
export DATAROBOT_CLI_FEATURE_WORKLOAD=true
).
curl
and
jq
for the calls that have no CLI equivalent—sharing and metadata updates.

**Pulumi:**
Pulumi CLI
and the
DataRobot Pulumi provider
installed, with a stack configured (
datarobot:endpoint
,
datarobot:apikey
).
curl
for sharing, which Pulumi doesn't model—see
When to use Pulumi
.


Then set the values that vary per run:

```
export MODEL="azure/gpt-5-nano-2025-08-07"
export RECIPIENT_USER_ID=""  # user, group, or organization ID to share with; leave blank to skip 
```

## Create the Workload

Artifacts are always created as `draft`, so with cURL and the CLI you create the Workload first with `importance` set, then lock the artifact in the next step. Locking flips its backing Workload into the locked (production) lifecycle: indefinite lifetime, immutable spec, eligible for locked-to-locked replace.

The artifact's `spec` defines container topology (image, port, entrypoint, environment variables, probes)—anything that travels with the artifact across deployments. Replica count, CPU/memory, and autoscaling are deployment-time concerns and live in `runtime.containerGroups[]`; entries are matched to the artifact by group and container `name`. The container authenticates against the DataRobot LLM Gateway using a per-Workload API token the platform resolves and injects automatically via `{"source": "api-key"}` —no value is set in the spec, so the token never appears in `GET /artifacts/{id}` responses, and `DATAROBOT_ENDPOINT` doesn't need to be set either since it's [platform-managed](https://docs.datarobot.com/ja/docs/workload-api/build-artifacts/artifacts-concepts.html.md#platform-managed-env-vars). See [Environment variable types](https://docs.datarobot.com/ja/docs/workload-api/build-artifacts/artifacts-concepts.html.md#environment-variable-types) for the full list.

`readinessProbe.path` gates `running` —the platform polls that path and only transitions the Workload to `running` once it returns 2xx. This tutorial points the probe at `/healthz`, which returns 2xx as soon as the FastAPI process is up. The container also exposes a deeper `/readyz` endpoint that exercises the LLM connection, but keep the readiness probe pointed at `/healthz`: gating `running` on an external dependency makes the Workload's status flap whenever that dependency has issues. Keep deep checks reachable as explicit endpoints for monitoring and runbooks instead of blocking startup on them.

**cURL:**
```
curl -s -X POST "${DATAROBOT_ENDPOINT}/workloads" \
  -H "Authorization: Bearer ${DATAROBOT_API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d "$(jq -n \
    --arg model "$MODEL" \
    '{
      "name": "agent-service",
      "importance": "high",
      "artifact": {
        "name": "agent-service-artifact",
        "type": "service",
        "spec": {
          "containerGroups": [{
            "name": "default",
            "containers": [{
              "name": "agent",
              "imageUri": "otkachnlp/fastapi-server-example:latest",
              "port": 8080,
              "primary": true,
              "entrypoint": ["python", "server.py"],
              "environmentVars": [
                {"name": "MODEL", "value": $model},
                {"source": "api-key"}
              ],
              "readinessProbe": {"path": "/healthz", "port": 8080}
            }]
          }]
        }
      },
      "runtime": {
        "containerGroups": [{
          "name": "default",
          "replicaCount": 1,
          "containers": [{
            "name": "agent",
            "resourceAllocation": {"cpu": 1, "memory": "512MB"}
          }]
        }]
      }
    }'
  )" | tee /tmp/workload.json

export WORKLOAD_ID=$(jq -r '.id' /tmp/workload.json)
export ARTIFACT_ID=$(jq -r '.artifactId' /tmp/workload.json) 
```

**CLI:**
Save the spec to a file, substituting `$MODEL`:

```
cat > workload.yaml <<EOF
name: agent-service
importance: high
artifact:
  name: agent-service-artifact
  type: service
  spec:
    containerGroups:
      - name: default
        containers:
          - name: agent
            imageUri: otkachnlp/fastapi-server-example:latest
            port: 8080
            primary: true
            entrypoint: ["python", "server.py"]
            environmentVars:
              - name: MODEL
                value: $MODEL
              - source: api-key
            readinessProbe:
              path: "/healthz"
              port: 8080
runtime:
  containerGroups:
    - name: default
      replicaCount: 1
      containers:
        - name: agent
          resourceAllocation:
            cpu: 1
            memory: "512MB"
EOF

dr workload create --spec-file workload.yaml --output-format json | tee /tmp/workload.json
export WORKLOAD_ID=$(jq -r '.id' /tmp/workload.json)
export ARTIFACT_ID=$(jq -r '.artifactId' /tmp/workload.json) 
```

**Pulumi:**
A `datarobot.Artifact` resource always resolves to `locked` by the end of `pulumi up` (see [When to use Pulumi](https://docs.datarobot.com/ja/docs/workload-api/workload-interfaces/workload-pulumi/index.html.md#when-to-use-pulumi)), so there's no separate lock step in this flow— `pulumi up` creates the artifact already locked.

```
import pulumi
import pulumi_datarobot as datarobot

artifact = datarobot.Artifact(
    "agent-service-artifact",
    name="agent-service-artifact",
    type="service",
    spec={
        "container_groups": [{
            "name": "default",
            "containers": [{
                "name": "agent",
                "image_uri": "otkachnlp/fastapi-server-example:latest",
                "port": 8080,
                "primary": True,
                "entrypoint": ["python", "server.py"],
                "environment_vars": [
                    {"name": "MODEL", "value": pulumi.Config().require("model")},
                    {"source": "api-key"},
                ],
                "readiness_probe": {"path": "/healthz", "port": 8080},
            }],
        }],
    },
)

workload = datarobot.Workload(
    "agent-service",
    name="agent-service",
    importance="high",
    artifact_id=artifact.artifact_id,
    runtime={
        "container_groups": [{
            "name": "default",
            "replica_count": 1,
            "containers": [{
                "name": "agent",
                "resource_allocation": {"cpu": 1, "memory": "512MB"},
            }],
        }],
    },
    opts=pulumi.ResourceOptions(replace_on_changes=["artifact_id"]),
)

pulumi.export("artifactId", artifact.artifact_id)
pulumi.export("workloadId", workload.id)
pulumi.export("endpoint", workload.endpoint) 
```

```
pulumi config set model "azure/gpt-5-nano-2025-08-07"
pulumi up 
```

`pulumi up` blocks until the Workload is `running`, so once it returns, skip ahead to [Invoke the service](https://docs.datarobot.com/ja/docs/workload-api/create-workloads/tutorial-production-ready-container.html.md#invoke-the-service).


## Lock the artifact

Transition the artifact from `draft` to `locked`. Because this Workload is the only one backing the draft artifact, the Workload's lifecycle transitions to locked alongside it. Locking is one-way: locked artifacts cannot return to draft.

**cURL:**
```
curl -X PATCH "${DATAROBOT_ENDPOINT}/artifacts/${ARTIFACT_ID}" \
  -H "Authorization: Bearer ${DATAROBOT_API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"status": "locked"}' 
```

`PATCH /artifacts/{artifact_id}` also accepts `name`, `description`, and `spec` for other updates while an artifact is still in `draft`.

**CLI:**
```
dr artifact lock "$ARTIFACT_ID" 
```

**Pulumi:**
Nothing to do here—the artifact was created already locked in the previous step.


## Wait for running

Poll the Workload's status until it reaches `running`. Expected happy-path progression: `submitted` → `provisioning` → `launching` → `running`.`errored` is a terminal state, not a transient blip to poll through—if you see it, stop and inspect ( `dr workload get "$WORKLOAD_ID"` or the events endpoint) rather than continuing to wait.

**cURL:**
```
curl -s "${DATAROBOT_ENDPOINT}/workloads/${WORKLOAD_ID}" \
  -H "Authorization: Bearer ${DATAROBOT_API_TOKEN}" | jq -r '.status' 
```

**CLI:**
```
dr workload status "$WORKLOAD_ID" 
```

**Pulumi:**
Nothing to do here— `pulumi up` already blocked until `running`.


## Invoke the service

Read the invoke URL from the Workload, then call the application routes against it.

**cURL:**
```
ENDPOINT=$(curl -s "${DATAROBOT_ENDPOINT}/workloads/${WORKLOAD_ID}" \
  -H "Authorization: Bearer ${DATAROBOT_API_TOKEN}" | jq -r '.endpoint')

curl -X POST "${ENDPOINT}/chat/completions" \
  -H "Authorization: Bearer ${DATAROBOT_API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"model": "'"${MODEL}"'", "messages": [{"role": "user", "content": "Hello!"}]}' 
```

**CLI:**
```
ENDPOINT=$(dr workload endpoint "$WORKLOAD_ID")
curl -X POST "${ENDPOINT}/chat/completions" \
  -H "Authorization: Bearer ${DATAROBOT_API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"model": "'"${MODEL}"'", "messages": [{"role": "user", "content": "Hello!"}]}' 
```

**Pulumi:**
```
ENDPOINT=$(pulumi stack output endpoint)
curl -X POST "${ENDPOINT}/chat/completions" \
  -H "Authorization: Bearer ${DATAROBOT_API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"model": "'"${MODEL}"'", "messages": [{"role": "user", "content": "Hello!"}]}' 
```


The server also has an agent with a tool call to ArXiv. To test it, call the same way against `${ENDPOINT}/agent` with a `{"query": "..."}` body.

## Govern the Workload

Now that it's a production Workload, wire up importance and sharing.

**cURL:**
```
# Raise importance to critical
curl -X PATCH "${DATAROBOT_ENDPOINT}/workloads/${WORKLOAD_ID}" \
  -H "Authorization: Bearer ${DATAROBOT_API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"importance": "critical"}'

# Share with another user, group, or organization (skip if RECIPIENT_USER_ID is blank)
if [ -n "$RECIPIENT_USER_ID" ]; then
  curl -X PATCH "${DATAROBOT_ENDPOINT}/workloads/${WORKLOAD_ID}/sharedRoles" \
    -H "Authorization: Bearer ${DATAROBOT_API_TOKEN}" \
    -H "Content-Type: application/json" \
    -d '{
      "operation": "updateRoles",
      "roles": [{"id": "'"${RECIPIENT_USER_ID}"'", "role": "USER", "shareRecipientType": "user"}]
    }'
fi 
```

**CLI:**
Importance and sharing updates go through the REST API directly— `dr workload` covers create, read, lifecycle, and logs, not metadata or sharing updates:

```
# Raise importance to critical
curl -X PATCH "${DATAROBOT_ENDPOINT}/workloads/${WORKLOAD_ID}" \
  -H "Authorization: Bearer ${DATAROBOT_API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"importance": "critical"}'

# Share with another user, group, or organization (skip if RECIPIENT_USER_ID is blank)
if [ -n "$RECIPIENT_USER_ID" ]; then
  curl -X PATCH "${DATAROBOT_ENDPOINT}/workloads/${WORKLOAD_ID}/sharedRoles" \
    -H "Authorization: Bearer ${DATAROBOT_API_TOKEN}" \
    -H "Content-Type: application/json" \
    -d '{
      "operation": "updateRoles",
      "roles": [{"id": "'"${RECIPIENT_USER_ID}"'", "role": "USER", "shareRecipientType": "user"}]
    }'
fi 
```

**Pulumi:**
`importance` is declarative—change it in code and reapply:

```
workload = datarobot.Workload(
    "agent-service",
    name="agent-service",
    importance="critical",  # was "high"
    artifact_id=artifact.artifact_id,
    runtime={ ... },
    opts=pulumi.ResourceOptions(replace_on_changes=["artifact_id"]),
) 
```

```
pulumi up 
```

Sharing isn't modeled by the provider—manage `/sharedRoles` via REST or the Console (see [When to use Pulumi](https://docs.datarobot.com/ja/docs/workload-api/workload-interfaces/workload-pulumi/index.html.md#when-to-use-pulumi)):

```
if [ -n "$RECIPIENT_USER_ID" ]; then
  curl -X PATCH "${DATAROBOT_ENDPOINT}/workloads/${WORKLOAD_ID}/sharedRoles" \
    -H "Authorization: Bearer ${DATAROBOT_API_TOKEN}" \
    -H "Content-Type: application/json" \
    -d '{
      "operation": "updateRoles",
      "roles": [{"id": "'"${RECIPIENT_USER_ID}"'", "role": "USER", "shareRecipientType": "user"}]
    }'
fi 
```


> [!NOTE] PATCH scope
> `PATCH /workloads/{id}` accepts `name`, `description`, and `importance`. For runtime changes (replicas, resources) use `PATCH /workloads/{id}/settings`, which triggers a rolling replacement—see [Runtime settings](https://docs.datarobot.com/ja/docs/workload-api/operate-workloads/runtime-settings.html.md).

## Observe

Locked Workloads expose the full monitoring surface with the organization's configured telemetry retention—see [Monitoring concepts: Retention summary](https://docs.datarobot.com/ja/docs/workload-api/monitor-workloads/monitoring-concepts.html.md#retention-summary).

| 機能 | エンドポイント |
| --- | --- |
| Service health, latency, error rate | GET /workloads/{id} (computed fields on the Workload) |
| Lifecycle events (audit trail) | GET /workloads/{id}/events |
| Aggregate request statistics | GET /workloads/{id}/stats |
| Per-metric time series | GET /workloads/{id}/stats/{metric_name} |
| Per-replica status | GET /workloads/{id}/protons/{proton_id}/statusDetails |

These all read through the REST API directly, regardless of which interface you used to create the Workload— `dr workload logs` covers container stdout/stderr, a separate signal from the ones in this table.

```
curl -s "${DATAROBOT_ENDPOINT}/workloads/${WORKLOAD_ID}/events" \
  -H "Authorization: Bearer ${DATAROBOT_API_TOKEN}"

curl -s "${DATAROBOT_ENDPOINT}/workloads/${WORKLOAD_ID}/stats" \
  -H "Authorization: Bearer ${DATAROBOT_API_TOKEN}" 
```

## サマリー

In this tutorial you:

- Created a Workload with importance set, backed by a draft artifact, then locked the artifact to move it onto the production (indefinite-lifetime) path.
- Deployed a FastAPI agent service that calls the DataRobot LLM Gateway using an automatically injected, per-Workload API token—no credential ever appeared in the artifact spec.
- Invoked both the OpenAI-compatible /chat/completions route and the LangGraph /agent route.
- Raised importance and shared the Workload with another user, group, or organization.
- Read events and aggregate statistics from the monitoring surface.

## 次のステップ

| リソース | 説明 |
| --- | --- |
| Workload concepts | The object model, lifecycle states, and the draft vs. locked decision in depth. |
| Tutorial: Replace the artifact behind a running Workload | Ship a new container version without dropping the endpoint. |
| Deploy an open-weight model from Hugging Face | Size a GPU compute bundle and serve a Hugging Face Hub model. |
| Instrument a Workload with OpenTelemetry (Python) | Add traces, metrics, and logs inside each request. |
