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.
Copy link to headingPrerequisites
- A Vercel account
- Docker Desktop or a running Docker daemon
- Vercel CLI (
npm install -g vercel) - .NET SDK 10 (optional when using only Docker)
Copy link to headingHow it works
The project is one ASP.NET Core app with the following parts:
Program.csis a minimal API built withWebApplication.CreateSlimBuilder. It binds Kestrel to0.0.0.0on$PORTand serves two routes,/and/health.Dockerfile.vercelbuilds 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.jsondeclares one service withruntime: "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, and runs it as a Function that autoscales with traffic and scales to zero when idle.
Copy link to headingSteps
Copy link to heading1. Initialize the project
Start by creating a project folder:
In this folder, create the Program.cs file with the central server code:
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:
Copy link to heading2. Add Dockerfile.vercel
To deploy this project on Vercel, create the Dockerfile.vercel file in the project root:
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.
Copy link to heading3. Add vercel.json
Create a vercel.json configuration file to set up a Vercel Service for this backend and rewrites for incoming requests:
The service explicitly selects the container runtime and points to Dockerfile.vercel. The catch-all rewrite sends every public path to that service.
Copy link to heading4. Run locally
To test the app locally, run:
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:
Copy link to heading5. Deploy to Vercel
Authenticate and create a production deployment:
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.
Copy link to headingExtend 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:
Copy link to headingEnvironment 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.
Copy link to headingDatabase 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
- Relational data: Any Marketplace Postgres integration
- Sessions, caching, and rate limits: Any Marketplace Redis integration
- Frequently read configuration: Vercel Global Config
Marketplace integrations inject credentials as environment variables. Use pooled database connections, keep pools small, and place the database near the function region.
Copy link to headingIterate with preview deployments
A connected Git repository gets a unique Preview Deployment 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.
Copy link to headingScale automatically with Fluid compute
Vercel Services run on 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.
Copy link to headingTroubleshooting
Copy link to headingThe 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.
Copy link to headingThe 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.
Copy link to headingNothing 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.
Copy link to headingUploads 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.
Copy link to headingBackground 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, invoking an HTTP endpoint, and multi-step or long-running work to a durable Workflow. Keep the web process request-scoped and idempotent.
Copy link to headingNext steps
- Learn how Vercel Services routes traffic between backends and frontends
- Learn how to run Docker on Vercel
- Review every available field in the vercel.json reference
- Read the container images documentation for the execution model and limits
- Read the Vercel Frameworks docs to learn about deploying other frameworks