Overview
Every Tinfoil Container requires a tinfoil-config.yml file in the root of your GitHub repository. This file defines the enclave runtime, resource allocation, container configuration, and request routing.
For a pre-filled tinfoil-config.yml, follow along with the quickstart guide.
File location
The file must be named tinfoil-config.yml and placed at the root of your repository. Tinfoil fetches this file from GitHub when you deploy or validate a container.
The repository must be public. Tinfoil measures your config at each tag and publishes the measurement to the Sigstore transparency log, so clients can verify against it. The Docker image the config references can still be private — see Private images.
Top-level fields
| Field | Type | Required | Description |
|---|
cvm-version | string | Yes | Confidential VM version (e.g. 0.7.5) |
cpus | integer | Yes | Number of CPU cores |
memory | integer | Yes | RAM in megabytes |
gpus | integer | No | Number of GPUs to attach to the enclave (1 or 8). Omit for CPU-only workloads. |
models | list | No | Verified model-weight artifacts to mount in the enclave. See Model weights. |
containers | list | Yes | One or more container definitions |
shim | object | Yes | Port and path routing configuration |
Valid resource values
| Resource | Valid values |
|---|
| CPUs | 2, 4, 8, 16, 32 |
| Memory (MB) | 8192, 16384, 32768, 65536, 131072 |
| GPUs | 1 or 8 at the top level; each GPU container also needs runtime: nvidia and gpus: all |
Memory values correspond to 8 GB, 16 GB, 32 GB, 64 GB, and 128 GB respectively.
Container spec
Each entry in the containers list defines a container to run inside the enclave.
| Field | Type | Required | Description |
|---|
name | string | Yes | Container identifier |
image | string | Yes | Docker image with SHA256 digest (e.g. image:tag@sha256:...) |
command | list | No | Command arguments passed to the container |
entrypoint | list | No | Override the container’s entrypoint |
env | list | No | Environment variables (see below) |
secrets | list | No | Secret names — values are set in the dashboard |
runtime | string | No | Container runtime — set to nvidia for GPU workloads |
gpus | string/int | No | GPU allocation for this container — typically all. See GPU configuration. |
ipc | string | No | IPC mode — set to host |
volumes | list | No | Bind mounts (e.g. /mnt/ramdisk/data:/data) |
healthcheck | object | No | Docker-style healthcheck run inside the container. See Healthchecks. |
restart | string | No | Restart policy if the container process exits. One of no (default), always, on-failure, unless-stopped. See Restart policy. |
GPU configuration
GPU workloads use a two-step allocation:
- Attach GPUs to the enclave with the top-level
gpus field, set to either 1 or 8. NVIDIA confidential computing restricts enclaves to those two sizes.
- Expose GPUs to a container with
runtime: nvidia and a container-level gpus value. Use gpus: all to give the container every GPU attached to the enclave — this is the right choice for both single-GPU and most multi-GPU setups. Individual indices (e.g. gpus: "0,1") are only needed when running multiple containers in an 8-GPU enclave and splitting GPUs between them.
GPU containers typically also set ipc: host so the NVIDIA runtime can share memory with host processes.
Container images must include a SHA256 digest (e.g. image:tag@sha256:...). This pins the exact image binary and ensures it can be verified in the transparency log. To get the digest: docker pull <image> && docker inspect --format='{{index .RepoDigests 0}}' <image>
Pin digests behind an immutable tag (e.g. v1.2.3), not just :latest — on ghcr.io the digest can be garbage-collected once :latest moves.
Model weights
GPU inference containers often need large model weights in addition to the
Docker image that runs the server. Use the top-level models list to mount
verified model-weight artifacts prepared in the dashboard’s Models tab.
models:
- name: "gemma-4-31b-it"
repo: "google/gemma-4-31B-it@419b2efe421994fdfd3394e621983d4cc511cd4f"
mpk: "0900ca6b913db0036792149d3ea5862986d66a6964b010e998f56fbb7e1276ab_62578683904_59fe9787-ed93-577a-9fd9-a7804c932a11"
| Field | Type | Required | Description |
|---|
name | string | Yes | Local identifier for the model artifact |
repo | string | Yes | Hugging Face repo pinned to a commit, in owner/model@commit form |
mpk | string | Yes | Model package metadata generated by the Models tab |
The model is mounted read-only at /tinfoil/mpk/mpk-<root_hash>, where
<root_hash> is the first segment of the mpk value. For a vLLM container,
pass that path as --model.
See Model weights for the full dashboard workflow and a
complete vLLM example.
Environment variables support two formats:
env:
- PORT: "8080" # Hardcoded value (YAML map syntax)
- LOG_LEVEL: "info"
secrets:
- DATABASE_URL # Looked up from your org's secrets store
- API_KEY
Healthchecks
Add a healthcheck block to a container to have the enclave verify it’s actually ready before the deployment transitions to Running. Without one, the container is considered ready the moment Docker starts it — fine for fast-starting apps, but a problem for workloads with long startup (model loading, cache warm-up) where the process is up but isn’t serving yet.
containers:
- name: "inference"
image: "vllm/vllm-openai:v0.14.1@sha256:..."
command: ["--port", "8001"]
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:8001/health"]
interval: 30s
timeout: 5s
start_period: 30m
| Field | Type | Description |
|---|
test | list | Command to run inside the container. Prefix with CMD to exec directly, or CMD-SHELL to run through a shell. Exit code 0 = pass, non-zero = fail. |
interval | duration | How often to run the check (e.g. 30s). Docker default: 30s. |
timeout | duration | How long a single check can take before it’s counted as a failure (e.g. 5s). Docker default: 30s. |
retries | integer | Consecutive failures after start_period before the container is marked unhealthy. Docker default: 3. |
start_period | duration | Grace period after container start during which failures don’t count toward retries (e.g. 30m). Docker default: 0s. |
How it’s used during boot. The enclave’s boot process polls Docker’s health state every 5 seconds once the container starts and waits until Docker reports the container Healthy before finishing boot. If Docker reports Unhealthy (i.e. retries consecutive failures after start_period has elapsed), the deployment fails and the last healthcheck output is surfaced as the error detail.
The test command runs inside the container, so whatever you invoke (curl, wget, a language runtime) has to be available in the image. For an inference server like vLLM that already exposes /health, a curl -sf http://localhost:<port>/health check is idiomatic.
start_period is usually the most important field. If your container takes 15 minutes to load model weights, set start_period to at least 20 minutes — otherwise failing checks during the load phase will burn through retries and the deployment will fail before your app ever gets a chance to serve.
See also. The schema is taken from Docker Compose — see the Compose healthcheck reference for the full semantics (exit codes, CMD-SHELL vs CMD, disabling an inherited check with disable: true).
Restart policy
By default, a container whose process exits stays exited. Set restart to have Docker automatically restart the process if it crashes — useful for long-running servers that should stay up across transient failures.
containers:
- name: "inference"
image: "vllm/vllm-openai:v0.14.1@sha256:..."
restart: always
| Value | Behavior |
|---|
no | Don’t restart. This is the default when restart is omitted. |
always | Restart the container regardless of exit status. |
on-failure | Restart only if the process exits with a non-zero status. |
unless-stopped | Like always, but don’t restart if the container was stopped explicitly. |
Interaction with healthchecks. The restart policy fires when the container process exits — it has no effect when Docker marks the container Unhealthy (the process keeps running; only its health state changes). During boot, the enclave fails the deployment on Unhealthy regardless of restart. Once the container has been declared Healthy, restart governs what happens if the process later dies.
See also. Taken from Docker Compose — see the Compose restart reference.
Routing
The shim section controls which ports and paths your container exposes.
| Field | Type | Required | Description |
|---|
shim.upstream-port | integer | Yes | Port your container listens on |
shim.paths | list | Yes | URL paths to expose (supports * wildcards) |
Only listed paths are reachable from outside the enclave. Any request to an unlisted path is rejected with a 404 error code.
Examples
Minimal config
cvm-version: 0.7.5
cpus: 2
memory: 8192
containers:
- name: "api"
image: "ghcr.io/myorg/api-server:v1.0.0@sha256:abc123..."
command: ["--port", "8000"]
shim:
upstream-port: 8000
paths:
- /health
- /api/*
With environment variables and secrets
cvm-version: 0.7.5
cpus: 4
memory: 16384
containers:
- name: "api"
image: "ghcr.io/myorg/api-server:v2.1.0@sha256:def456..."
env:
- PORT: "8080"
- LOG_LEVEL: "info"
- NODE_ENV: "production"
secrets:
- DATABASE_URL
- STRIPE_SECRET_KEY
command: ["--port", "8080"]
shim:
upstream-port: 8080
paths:
- /health
- /api/*
- /webhooks/*
GPU inference server (vLLM)
cvm-version: 0.7.5
cpus: 16
memory: 65536
gpus: 1
models:
- name: "gemma-4-31b-it"
repo: "google/gemma-4-31B-it@419b2efe421994fdfd3394e621983d4cc511cd4f"
mpk: "0900ca6b913db0036792149d3ea5862986d66a6964b010e998f56fbb7e1276ab_62578683904_59fe9787-ed93-577a-9fd9-a7804c932a11"
containers:
- name: "inference"
image: "vllm/vllm-openai:v0.14.1@sha256:6fc52be4609fc19b09c163be2556976447cc844b8d0d817f19bc9e1f44b48d5a"
runtime: nvidia
gpus: all
ipc: host
command: [
"--model", "/tinfoil/mpk/mpk-0900ca6b913db0036792149d3ea5862986d66a6964b010e998f56fbb7e1276ab",
"--served-model-name", "gemma-4-31b-it",
"--port", "8001"
]
shim:
upstream-port: 8001
paths:
- /v1/chat/completions
- /v1/completions
- /health
- /metrics
Multi-container setup
cvm-version: 0.7.5
cpus: 8
memory: 32768
containers:
- name: "api"
image: "ghcr.io/myorg/api-server:v1.0.0@sha256:abc123..."
env:
- PORT: "8080"
secrets:
- DATABASE_URL
command: ["--port", "8080"]
- name: "worker"
image: "ghcr.io/myorg/worker:v1.0.0@sha256:def456..."
env:
- QUEUE_URL: "redis://localhost:6379"
secrets:
- API_SECRET
shim:
upstream-port: 8080
paths:
- /health
- /api/*
Validation
You can validate your config before deploying. In the dashboard, click New Container and select your repo and tag. Tinfoil fetches your tinfoil-config.yml and checks that:
- CPU, memory, and GPU values are valid
- Resource usage is within your org’s limits
- Model weight references are valid when
models is set
- Referenced secrets exist in your org
- The container image is accessible and includes a SHA256 digest
If validation fails, the dashboard shows specific error messages explaining what needs to be fixed.
Container naming constraints
| Constraint | Rule |
|---|
| Allowed characters | Lowercase letters, numbers, hyphens |
| Start/end | Cannot start or end with a hyphen |
| Max length | 64 characters |
| Uniqueness | Unique per organization (production and debug namespaces are separate) |
Examples
| Name | Valid? |
|---|
my-api | Yes |
data-processor-v2 | Yes |
MyAPI | No — uppercase not allowed |
my_api | No — underscores not allowed |
my api | No — spaces not allowed |
-my-api | No — cannot start with a hyphen |
my-api- | No — cannot end with a hyphen |
Template
The tinfoil-containers-template repo contains a ready-to-use tinfoil-config.yml with the latest cvm-version value. Create a new repo from this template to get started quickly.