---
title: Deploy .NET with ASP.NET Core on Vercel with Docker
description: Build a .NET application with Docker and deploy it to Vercel Functions. Learn how to configure environment variables, integrations, and preview deployments for your ASP.NET Core application.
url: /kb/guide/dot-net-asp-net-on-vercel-with-docker
canonical_url: "https://vercel.com/kb/guide/dot-net-asp-net-on-vercel-with-docker"
published: 2026-08-11
last_updated: 2026-08-11
authors: Anshuman Bhardwaj
related:
  - /docs/cli
  - /docs/container-registry
  - /docs/services
  - /docs/vercel-blob
  - /docs/global-config
  - /docs/deployments/environments
  - /docs/fluid-compute
  - /docs/cron-jobs
  - /docs/workflows
  - /kb/guide/docker
  - /docs/project-configuration/vercel-json
  - /docs/functions/container-images
  - /docs/frameworks
install_vercel_plugin: npx plugins add vercel/vercel-plugin
---

ASP.NET Core minimal APIs are self-hosted web services: Kestrel binds a port, and the published output runs as an ordinary Linux process. Vercel doesn't ship a first-party .NET runtime. Instead, you deploy one as a container that runs on Vercel Functions. Add a `Dockerfile.vercel` and set the service `runtime` to `container`. Vercel builds the image, stores it in its registry, and serves it from a Vercel Function that autoscales with traffic and scales to zero when idle.

This guide walks through deploying the app into an ASP.NET Core runtime image, binding Kestrel to Vercel's `PORT`, and configuring integrations for managed storage instead of using the container file system.

## Prerequisites

- A [Vercel account](https://vercel.com)
  
- Docker Desktop or a running Docker daemon
  
- [Vercel CLI](https://vercel.com/docs/cli) (`npm install -g vercel`)
  
- .NET SDK 10 (optional when using only Docker)
  

## How it works

The project is one ASP.NET Core app with the following parts:

- `Program.cs` is a minimal API built with `WebApplication.CreateSlimBuilder`. It binds Kestrel to `0.0.0.0` on `$PORT` and serves two routes, `/` and `/health`.
  
- `Dockerfile.vercel` builds in two stages: the .NET 10 SDK restores and publishes the app, then an ASP.NET Core runtime image carries only the published output.
  
- `vercel.json` declares one service with `runtime: "container"` pointing at that Dockerfile, plus a catch-all rewrite sending every request to it.
  
- On deploy, Vercel builds the image, stores it in the [Vercel Container Registry](https://vercel.com/docs/container-registry), and runs it as a Function that autoscales with traffic and scales to zero when idle.
  

## Steps

### 1\. Initialize the project

Start by creating a project folder:

```bash
mkdir vercel-docker-dotnet
cd vercel-docker-dotnet
```

In this folder, create the `Program.cs` file with the central server code:

```php
var builder = WebApplication.CreateSlimBuilder(args);
var port = Environment.GetEnvironmentVariable("PORT") ?? "80";
builder.WebHost.UseUrls($"http://0.0.0.0:{port}");

var app = builder.Build();
app.MapGet("/", () => Results.Json(new { message = "Hello from .NET on Vercel" }));
app.MapGet("/health", () => Results.Json(new { status = "ok" }));
app.Run();
```

The application code contains the imports, configuration, shutdown handling, and dependency manifests needed to run this snippet. It listens on `0.0.0.0`, reads `PORT`, and falls back to port 80.

Create a new file called `HelloWorld.csproj` with the following code:

```xml
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <InvariantGlobalization>true</InvariantGlobalization>
  </PropertyGroup>
</Project>
```

### 2\. Add `Dockerfile.vercel`

To deploy this project on Vercel, create the `Dockerfile.vercel` file in the project root:

```dockerfile
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY HelloWorld.csproj ./
RUN dotnet restore
COPY Program.cs ./
RUN dotnet publish -c Release --no-restore -o /app/publish /p:UseAppHost=false

FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled
WORKDIR /app
COPY --from=build /app/publish ./
ENV PORT=80 DOTNET_EnableDiagnostics=0
ENTRYPOINT ["dotnet", "HelloWorld.dll"]
```

The SDK stays in the build stage; the final ASP.NET image contains only the runtime and published output. The dependency manifest is copied before the source, so Docker can reuse dependency layers when only application code changes.

### 3\. Add `vercel.json`

Create a `vercel.json` configuration file to set up a [Vercel Service](https://vercel.com/docs/services) for this backend and rewrites for incoming requests:

```json
{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "services": {
    "api": {
      "root": ".",
      "entrypoint": "Dockerfile.vercel",
      "runtime": "container"
    }
  },
  "rewrites": [
    { "source": "/(.*)", "destination": { "service": "api" } }
  ]
}
```

The service explicitly selects the container runtime and points to `Dockerfile.vercel`. The catch-all rewrite sends every public path to that service.

### 4\. Run locally

To test the app locally, run:

```bash
vercel dev -L
```

The `-L` flag runs locally without cloud authentication, since we haven’t initialized a Vercel project.

Use the URL printed by the CLI, usually `http://localhost:3000`:

```bash
curl http://localhost:3000/
curl http://localhost:3000/health
```

### 5\. Deploy to Vercel

Authenticate and create a production deployment:

```bash
vercel login
vercel deploy --prod
```

For Git-based deployments, push this directory to a repository, import it in Vercel, and keep `Dockerfile.vercel` and `vercel.json` at the configured project root.

## Extend your application

You have now deployed your minimal ASP.NET application to Vercel. You can extend this application to meet your use case, build additional APIs, switch frameworks, and much more. Here are some Vercel features to help you take your application to production:

### Environment variables

You can add the environment variables from the project settings on your Vercel dashboard. Otherwise, you can use the Vercel CLI to add environment variables with `vercel env add NAME`. During development, pull values with `vercel env pull`. Store secrets as environment variables and never commit them or bake them into the image.

### Database and storage

Vercel runs your Docker container as a Vercel Function, which doesn’t have persistent storage. Therefore, you should not use the container file system for a durable state. Choose storage by data type:

- Files and uploads: [Vercel Blob](https://vercel.com/docs/vercel-blob)
  
- Relational data: Any [Marketplace Postgres integration](https://vercel.com/marketplace/category/storage?category=storage&search=postgres)
  
- Sessions, caching, and rate limits: Any [Marketplace Redis integration](https://vercel.com/marketplace/category/storage?category=storage&search=redis)
  
- Frequently read configuration: [Vercel Global Config](https://vercel.com/docs/global-config)
  

Marketplace integrations inject credentials as environment variables. Use pooled database connections, keep pools small, and place the database near the function region.

### Iterate with preview deployments

A connected Git repository gets a unique [Preview Deployment](https://vercel.com/docs/deployments/environments) for every branch push or pull request. From the CLI, run `vercel deploy` without `--prod` for the same disposable workflow, then promote only tested changes to production. This lets you test updates without affecting the production environment.

### Scale automatically with Fluid compute

[Vercel Services](https://vercel.com/docs/services) run on [Fluid compute](https://vercel.com/docs/fluid-compute) by default. Vercel scales the container with traffic, shares instances across concurrent requests, and pauses them between work. Active CPU billing stops while code waits on I/O, and when no requests are running, which can reduce idle compute costs; memory and invocation charges still apply.

## Troubleshooting

### The build succeeds but every request returns 502

**Cause:** The app isn't listening where Vercel is sending traffic. Vercel routes container traffic to port `80` unless you override `PORT` in project settings, but the .NET base images set `ASPNETCORE_HTTP_PORTS=8080`. Since .NET 8, the default ASP.NET Core container port is 8080, not 80. Without the explicit binding, the application listens on the image’s default port, `8080`, while Vercel routes traffic to `80`.

**Fix:** Keep the bind in code: `builder.WebHost.UseUrls($"http://0.0.0.0:{port}")` with `port` read from `PORT` or delete it and set `ASPNETCORE_HTTP_PORTS` to the same value as `PORT`. Binding `localhost` or `127.0.0.1` fails the same way. `EXPOSE` has no effect on either.

### The container exits immediately with a permission error on port 80

**Cause:** The chiseled base image runs as the non-root `app` user, and 80 is a privileged port. Whether the bind succeeds depends on the runtime's `net.ipv4.ip_unprivileged_port_start` setting, so the same image can work under Docker Desktop and fail elsewhere.

**Fix:** To avoid privileged-port restrictions across environments, set `PORT=8080` in the project’s environment variables. Vercel will route traffic to the configured port.

### Nothing appears in runtime logs

**Cause:** Vercel broadcasts `stdout` and `stderr`. A file-based sink, a Windows EventLog provider, or a buffered writer that never flushes produces a silent deployment.

**Fix:** Log through the console provider only. Note that `DOTNET_EnableDiagnostics=0` in the Dockerfile disables the diagnostics server (`dotnet-counters`, `dotnet-trace`), not logging.

### Uploads larger than about 4.5 MB fail with a 413

**Cause:** The Vercel Function request and response body limit is 4.5 MB. Kestrel's own `MaxRequestBodySize` default is far higher, so your app never sees the request and never logs an error.

**Fix:** Have the browser upload directly to storage and send only the resulting URL through your endpoint. Raising Kestrel's limit or `[RequestSizeLimit]` changes nothing. ### Background jobs and timers stop running between requests **Cause:** There's no process when there's no traffic. A `BackgroundService`, `Timer`, or fire-and-forget `Task.Run()` survives only as long as the instance, and scale-in can terminate it mid-work. **Fix:** Move scheduled work to [Vercel Cron](https://vercel.com/docs/cron-jobs), invoking an HTTP endpoint, and multi-step or long-running work to a durable [Workflow](https://vercel.com/docs/workflows). Keep the web process request-scoped and idempotent.

## Next steps

- Learn how [Vercel Services](https://vercel.com/docs/services) routes traffic between backends and frontends
  
- Learn how to run [Docker on Vercel](https://vercel.com/kb/guide/docker)
  

- Review every available field in the [vercel.json reference](https://vercel.com/docs/project-configuration/vercel-json)
  
- Read the [container images documentation](https://vercel.com/docs/functions/container-images) for the execution model and limits
  
- Read the [Vercel Frameworks docs](https://vercel.com/docs/frameworks) to learn about deploying other frameworks