---
title: How to test a container image in Vercel Sandbox before deploying
description: Validate a container image before deploying by booting it as a custom Sandbox image from Vercel Container Registry (VCR) or running Docker inside a Vercel Sandbox.
url: /kb/guide/test-container-image-vercel-sandbox
canonical_url: "https://vercel.com/kb/guide/test-container-image-vercel-sandbox"
last_updated: 2026-08-01
authors: Ben Sabic
related:
  - /docs/cli
  - /docs/sandbox/concepts/authentication
  - /docs/sandbox/sdk-reference
  - /docs/container-registry
  - /docs/vercel-sandbox/cli-reference
  - /docs/deployments
  - /docs/sandbox
  - /docs/sandbox/concepts/images
  - /kb/guide/docker
  - /docs/sandbox/pricing
install_vercel_plugin: npx plugins add vercel/vercel-plugin
---

Container images that build cleanly can still fail at runtime.

The server doesn't start, the port never binds, or a migration errors against the image's real filesystem. Vercel Sandbox lets you catch these failures before deploying by running your checks inside an isolated Firecracker microVM, so nothing touches your host machine or production systems.

There are two ways to test an image in a sandbox. You can boot it as a custom Sandbox image from Vercel Container Registry (VCR), or install Docker inside a sandbox and run the image exactly as Docker would. This guide helps you pick the right path, run pass/fail checks against your image, and wire the validation into CI or an agent workflow.

## Overview

In this guide, you'll learn how to:

- Choose between booting an image as a custom Sandbox image and running Docker inside a sandbox
  
- Run install, start, and HTTP smoke checks against the image
  
- Verify the container's network behavior with sandbox network policies
  
- Automate the validation in CI or an agent workflow
  
- Read the results: what a passing sandbox run does and doesn't guarantee
  

## Prerequisites

Before you begin, make sure you have:

- A [Vercel account](https://vercel.com/signup) and the [Vercel CLI](https://vercel.com/docs/cli) installed
  
- A linked project with an OIDC token available locally. Run `vercel link`, then `vercel env pull` to download `VERCEL_OIDC_TOKEN`. In external CI systems, use an [access token](https://vercel.com/docs/sandbox/concepts/authentication#access-tokens) instead.
  
- The [`@vercel/sandbox`](https://vercel.com/docs/sandbox/sdk-reference) SDK installed: `npm i @vercel/sandbox`
  
- A container image for testing.
  

## Choose your validation path

Vercel Sandbox works with container images in two ways, and each validates something different:

|                       | Boot the image as the sandbox                                                                     | Run Docker inside a sandbox                                                                              |
| --------------------- | ------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| **Mechanism**         | `Sandbox.create()` with a custom image pulled from VCR                                            | Install Docker in a default sandbox, start the daemon with `sudo`, then `docker run` the image           |
| **What it validates** | The image's filesystem, toolchain, and dependencies, plus any command you run with `runCommand()` | The image exactly as Docker executes it, including `ENTRYPOINT` and `CMD`                                |
| **Key caveat**        | Sandbox does not run Docker `ENTRYPOINT` or `CMD`; you start processes yourself                   | Containers don't inherit the sandbox's proxy CA certificate; daemon startup needs `sudo`                 |
| **Prerequisites**     | Image pushed to VCR with a `linux/amd64` manifest                                                 | Any image source reachable from the sandbox                                                              |
| **Speed**             | Fast: boots from a precompiled snapshot                                                           | Slower first run (Docker install plus image pull); persistent sandboxes carry both over between sessions |
| **Best for**          | Repeated smoke tests against a known image, and agent toolchains                                  | End-to-end validation, testing the start command itself, and images not yet in VCR                       |

The decision rule: if you need to validate the container's own start behavior (`ENTRYPOINT`, `CMD`, and signal handling), use Path B. If you need a fast, repeatable environment built from the image's contents, use Path A.

## Boot the image as a custom Sandbox image (Path A)

### 1\. Push the image to VCR

Sandbox pulls custom images from VCR, a project-scoped registry hosted at `vcr.vercel.com`. VCR only serves an image to Sandbox once it has prepared an optimized `linux/amd64` build, so build for that platform and use zstd compression:

`docker buildx build \ --platform linux/amd64 \ --output "type=image,name=vcr.vercel.com/team-slug/project-slug/my-repository:latest,push=true,oci-mediatypes=true,compression=zstd,compression-level=3,force-compression=true" \ .`

To authenticate locally before pushing, run `vercel vcr login`, which mints a short-lived, project-scoped OIDC credential valid for 12 hours. After the push, check the [repository details page](https://vercel.com/d?to=%2F%5Bteam%5D%2F%5Bproject%5D%2Fsettings%2Fgit) in your project dashboard for the readiness state.

For repository setup and other authentication options, see the [Vercel Container Registry documentation](https://vercel.com/docs/container-registry).

### 2\. Create the sandbox from the image

Pass the image reference to `Sandbox.create()`, along with the port:

`import { Sandbox } from '@vercel/sandbox'; const sandbox = await Sandbox.create({ image: 'my-repository:latest', ports: [3000], timeout: 10 * 60 * 1000, // 10 minutes env: { DATABASE_URL: 'your_database_url_here' }, });` The `image` parameter accepts a repository name, tag, digest, or fully qualified VCR URL. The `env` parameter sets default environment variables for every command run in the sandbox, which is how you supply configuration the image expects. Vercel Sandbox does not run Docker `ENTRYPOINT` or `CMD` for custom images. The sandbox boots with your image's filesystem in place, but you start processes yourself with `sandbox.runCommand()`. If the Dockerfile defines `WORKDIR`, commands start in that directory. ### 3\. Run your checks with `runCommand()` Verify the toolchain and dependencies the image claims to ship, then start the server explicitly. Use `detached: true` for the server process: `// Check the toolchain baked into the image const version = await sandbox.runCommand('node', ['--version']); console.log(await version.stdout()); // Start the server yourself, since CMD did not run const server = await sandbox.runCommand({ cmd: 'node', args: ['server.js'], detached: true, });` Any nonzero `exitCode` on a blocking command is a failed check. For the detached server, stream `server.logs()` if you need to watch for a readiness line. ### 4\. Smoke-test over the exposed port `sandbox.domain(port)` returns a public URL for any port you declared in `ports`. Send a request and assert on the response: ``const url = sandbox.domain(3000); const response = await fetch(url); console.log(response.status); // 200 if (!response.ok) { throw new Error(`Smoke test failed with status ${response.status}`); } await sandbox.stop();`` A `200` response confirms the image's dependencies and your start command produce a working server. If the request hangs, the server likely never started. ## Run the image with Docker inside a sandbox (Path B) ### 1\. Create a sandbox and install Docker Each sandbox is a Firecracker microVM with its own kernel, running Amazon Linux 2023 with `sudo` access, which is what lets you start the Docker daemon inside it. Install Docker, start the daemon detached, and wait for it to report ready: `import { Sandbox } from '@vercel/sandbox'; const sandbox = await Sandbox.create({ name: 'image-validation', ports: [3000], timeout: 15 * 60 * 1000, // 15 minutes }); await sandbox.runCommand({ sudo: true, cmd: 'dnf', args: ['install', '-y', 'docker'], }); // Start the Docker daemon and wait for it to be ready await sandbox.runCommand({ sudo: true, cmd: 'dockerd', detached: true }); await sandbox.runCommand({ cmd: 'sh', args: ['-lc', 'until sudo docker info >/dev/null 2>&1; do sleep 1; done'], });` Sandboxes are persistent by default, so the Docker installation and any pulled images carry over between sessions through filesystem snapshots. Running processes don't survive a stop, though, so restart `dockerd` on each resume. The `onResume` hook in `Sandbox.getOrCreate()` handles this for you (by default it fires when the session actually resumes on your first SDK call; pass `resume: true` to resume immediately). ### 2\. Authenticate and pull the image Pull the image under test from VCR or another registry. For VCR, pass your OIDC token into the sandbox and log in with the `oidc` username: `await sandbox.runCommand({ cmd: 'sh', args: [ '-lc', 'printf "%s" "$VERCEL_OIDC_TOKEN" | sudo docker login vcr.vercel.com --username oidc --password-stdin', ], env: { VERCEL_OIDC_TOKEN: process.env.VERCEL_OIDC_TOKEN! }, }); await sandbox.runCommand({ cmd: 'sh', args: ['-lc', 'sudo docker pull vcr.vercel.com/team-slug/project-slug/my-repository:latest'], });` VCR implements the standard Docker Registry HTTP API v2, so `docker login` works from inside a sandbox the same way it does locally, with no environment-specific setup. The OIDC token expires after 12 hours, so run `vercel env pull` again if you see authentication errors. In external CI systems where `VERCEL_OIDC_TOKEN` is unavailable, use a [Vercel access token](https://vercel.com/docs/sandbox/concepts/authentication#access-tokens) instead, with the team ID that owns the project as the Docker username. Never write either token into scripts or logs; pass it through `env` as shown.

### 3\. Run the container as Docker would

This is the step Path A can't cover: Docker executes the image's real `ENTRYPOINT` and `CMD`. Map the container's port to the sandbox port you exposed:

`await sandbox.runCommand({ cmd: 'sh', args: ['-lc', 'sudo docker run -d --name under-test -p 3000:3000 vcr.vercel.com/team-slug/project-slug/my-repository:latest', ], });` ### 4\. Exercise it Check the container is running, inspect its logs, and hit it over the exposed port: `// Confirm the container stayed up const ps = await sandbox.runCommand({ cmd: 'sh', args: ['-lc', 'sudo docker ps --filter name=under-test --format "{{.Status}}"'], }); console.log(await ps.stdout()); // Up 4 seconds // Inspect startup logs const logs = await sandbox.runCommand({ cmd: 'sh', args: ['-lc', 'sudo docker logs under-test'], }); console.log(await logs.stdout()); // HTTP smoke test through the sandbox's public URL const response = await fetch(sandbox.domain(3000)); console.log(response.status); // 200` An empty result from `docker ps` means the container exited; run `sudo docker logs under-test` to see why, and check the exit code with `sudo docker inspect under-test --format "{{.State.ExitCode}}"`. ### 5\. Optional: validate with backing services Docker in a sandbox is also useful for running containerized services like Redis or Postgres as test dependencies alongside the image under test: `await sandbox.runCommand({ cmd: 'sh', args: ['-lc', 'sudo docker run --rm -d --name redis redis:alpine'], }); await sandbox.runCommand({ cmd: 'sh', args: ['-lc', 'sudo docker exec redis redis-cli PING'], });` This lets migrations, seeds, and integration checks run against real services without provisioning anything outside the microVM. ## Lock down the network (optional, both paths) Sandbox egress traffic passes through a firewall you control. The `networkPolicy` parameter defaults to `allow-all`, but you can pass `deny-all` or an allow list of domains to confirm the image makes only the outbound calls you expect: `const sandbox = await Sandbox.create({ image: 'my-repository:latest', ports: [3000], networkPolicy: { allow: ['api.example.com'] }, });` Domain entries without a wildcard are exact matches only. To allow all subdomains, use a leading wildcard such as `*.example.com`. Domain allow lists match traffic by SNI, so they apply to TLS traffic only. If the image makes plain-HTTP outbound calls, those are blocked under a domain allow list even when the domain is listed; use `subnets.allow` to permit non-TLS destinations. You can also change the policy at runtime with `sandbox.update({ networkPolicy })`, for example to allow the image pull, then switch to `deny-all` before starting the container. If your smoke test passes under a restrictive policy, the code paths you exercised made no unexpected outbound calls. Paths the test didn't reach, such as deferred jobs or retries, can still have outbound dependencies. One behavior to watch in Path B: a container has its own trust store, so it can't see the sandbox's proxy CA certificate, and HTTPS requests from inside the container fail TLS verification when the firewall terminates them. ## Automate it Structure the validation as a single script that exits nonzero on failure, so CI can gate a deploy on it. This example follows the create, check, assert, and stop pattern with Path A: `import { Sandbox } from '@vercel/sandbox'; async function main() { const sandbox = await Sandbox.create({ image: process.env.IMAGE_REF!, // pin by digest in CI ports: [3000], timeout: 10 * 60 * 1000, }); try { await sandbox.runCommand({ cmd: 'node', args: ['server.js'], detached: true, }); // Poll until the server answers or the deadline passes const deadline = Date.now() + 60_000; let ok = false; while (Date.now() < deadline) { try { const res = await fetch(sandbox.domain(3000)); if (res.ok) { ok = true; break; } } catch {} await new Promise((r) => setTimeout(r, 2000)); } if (!ok) throw new Error('Image failed validation: no 200 within 60s'); console.log('Image passed validation'); } finally { await sandbox.stop(); } } main().catch((err) => { console.error(err); process.exit(1); });` Run it in CI with `VERCEL_OIDC_TOKEN` (or a Vercel access token) and `IMAGE_REF` set. For one-off manual runs, the [Sandbox CLI](https://vercel.com/docs/vercel-sandbox/cli-reference) offers `sandbox create` with `--publish-port` and `--network-policy` flags. The same script works as a tool for AI agents that build images and need to verify them before promotion.

## What a passing run does and doesn't validate

Treat a green sandbox run as one ring of validation, not production parity:

| A passing run confirms                                                                  | A passing run does not confirm                                                                              |
| --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| The image's filesystem, toolchain, and dependencies work                                | Fluid compute lifecycle behavior: `$PORT` resolution and the 30-second `SIGTERM` grace period on scale-down |
| The start command boots a working server (Path B tests the real `ENTRYPOINT` and `CMD`) | Production request routing and CDN behavior                                                                 |
| The server answers HTTP requests with expected status and body                          | Performance under Active CPU pricing and concurrent load                                                    |
| The image's outbound network profile matches your policy                                | Behavior of environment variables and integrations only present in production                               |

For the next ring of validation, deploy the image to a [preview deployment](https://vercel.com/docs/deployments) and test it on the real Functions runtime before promoting to production.

## Troubleshooting

### Sandbox.create() returns image\_not\_ready

VCR prepares an optimized `linux/amd64` snapshot after each push, and `Sandbox.create()` fails with `image_not_ready` until preparation finishes. Check the readiness state on the repository details page, then retry. Confirm the image was built with `--platform linux/amd64`.

### The server never responds in Path A

Custom Sandbox images skip Docker `ENTRYPOINT` and `CMD`, so the server only runs if you started it with `runCommand()`. Start it explicitly with `detached: true`, and confirm it listens on a port you declared in the `ports` array.

### TLS failures inside containers in Path B

The sandbox trusts its proxy CA certificate at `/etc/pki/ca-trust/source/anchors/vercel-proxy-ca.pem`, but containers can't see it. Mount the certificate into the container and add it to the container's trust store:

`sudo docker run --rm \ -v /etc/pki/ca-trust/source/anchors/vercel-proxy-ca.pem:/usr/local/share/ca-certificates/vercel-proxy-ca.crt:ro \ my-image \ sh -c "update-ca-certificates && my-command"`

Use `update-ca-certificates` on Debian or Ubuntu base images and `update-ca-trust` on Amazon Linux, Fedora, or RHEL. If the app reads a CA bundle from an environment variable, point that variable (such as `NODE_EXTRA_CA_CERTS`) at the mounted path instead.

### The sandbox times out mid-validation

Sandboxes run for 5 minutes by default. Pass a longer `timeout` at creation, or call `sandbox.extendTimeout()` while running. The ceiling is 45 minutes on Hobby plans and 24 hours on Pro and Enterprise plans.

## Best practices

- **Pin by digest, not tag**: Validate `my-repository@sha256:...` so the artifact you tested is byte-for-byte the one you deploy. Tags can move between validation and deploy.
  
- **Keep runs cheap**: Data a sandbox downloads (packages, images, and Git repositories) is free, but traffic on exposed ports and data sent out is billable per GB, so keep smoke payloads small. Sandbox creation itself costs $0.60 per million creations.
  
- **Reuse persistent sandboxes for Path B**: The Docker installation and pulled images survive between sessions, which skips 30 or more seconds of setup on every run. Use `Sandbox.getOrCreate()` with an `onResume` hook that restarts `dockerd`.
  
- **Fail loudly**: Exit nonzero on any failed check so CI blocks the deploy, and print the container logs on failure so the run is debuggable from CI output alone.
  

## Related resources and next steps

- Read the [Vercel Sandbox documentation](https://vercel.com/docs/sandbox) for the isolation model and authentication setup
  
- Explore the [JS SDK reference](https://vercel.com/docs/sandbox/sdk-reference) for `runCommand()`, ports, snapshots, and network policy APIs
  
- Learn how custom images work in [Sandbox images](https://vercel.com/docs/sandbox/concepts/images)
  
- Set up repositories and authentication in [Vercel Container Registry](https://vercel.com/docs/container-registry)
  
- See the broader picture in [Running Docker on Vercel](https://vercel.com/kb/guide/docker)
  
- Review [Sandbox pricing and limits](https://vercel.com/docs/sandbox/pricing) before scheduling validation runs in CI