Tutorial

ASP.NET Core on a Container Host: The Dockerfile You Bring, and What Runs in 512 MB

SnapDeploy Team 2026-11-11 Updated 2026-11-11 8 min read
dotnetaspnet-corecsharpdockerfree-tier

ASP.NET Core is a good fit for a small container: the runtime is container-aware, a minimal API idles in well under 100 MB, and Microsoft publishes multi-architecture images that just work. What a container host will not do for you is write the Dockerfile. SnapDeploy generates Dockerfiles for Node.js, Python, Java, Go, PHP, Ruby and static sites, and .NET is not on that list, so this guide starts with the file you bring. It is twelve lines, and it is better than a generated one anyway.

Tested with .NET 10, the current long-term-support release (supported November 2025 to November 2028). .NET 8 and .NET 9 both reach end of support on 10 November 2026, so a new project should start on 10.

The Dockerfile

FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build
WORKDIR /src
COPY *.csproj ./
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app --no-restore

FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine
WORKDIR /app
COPY --from=build /app .
ENV ASPNETCORE_URLS=http://+:8080 ASPNETCORE_ENVIRONMENT=Production DOTNET_EnableDiagnostics=0
USER app
EXPOSE 8080
ENTRYPOINT ["dotnet", "MyApi.dll"]
  • Restoring before copying the source keeps the package layer cached between builds when only code changed.
  • The aspnet runtime image is the small one; sdk is for the build stage only. The Alpine variants are roughly 110 MB for a minimal API, about half the Debian-based ones.
  • Microsoft's images ship for both x86 and ARM, so the build works unchanged on a host that builds ARM images, which SnapDeploy does.
  • The app user exists in the official images since .NET 8; running as it costs nothing.
  • DOTNET_EnableDiagnostics=0 turns off the debugger and profiler hooks, which a production container does not need.

Ports, hosts and health

The official images listen on port 8080 by default since .NET 8; the ASPNETCORE_URLS line above makes that explicit and binds all interfaces, which is what a container needs (binding to localhost is the classic "build succeeded, nothing answers" mistake). If the platform sets PORT, honour it in Program.cs rather than guessing:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHealthChecks();
var app = builder.Build();

app.MapHealthChecks("/health");
app.MapGet("/api/time", () => new { now = DateTime.UtcNow });

var port = Environment.GetEnvironmentVariable("PORT") ?? "8080";
app.Run($"http://0.0.0.0:{port}");

Behind the platform's proxy, add app.UseForwardedHeaders() with ForwardedHeaders.XForwardedFor | XForwardedProto so redirects and Request.Scheme see HTTPS. Without it, an app that forces HTTPS redirects loops.

Memory on 512 MB

The .NET garbage collector reads the container's memory limit and sizes its heap accordingly, and on fewer than two cores it uses the workstation collector automatically, so there is nothing to tune for a quarter vCPU. A minimal API idles at 40 to 80 MB; an app with EF Core, authentication and a few controllers sits around 120 to 200 MB after warm-up. Both are comfortable in 512 MB. If you see the container killed with exit code 137, look for an unbounded in-memory cache or a large response being buffered, not at the runtime. Start-up on a quarter vCPU is a few seconds for a minimal API and 10 to 20 seconds with EF Core migrations, well inside the roughly one-minute wake a free container takes after sleeping.

A database, and migrations at start

Npgsql for PostgreSQL, Pomelo for MySQL, both with EF Core. Read the connection string from DATABASE_URL, which a SnapDeploy database add-on injects and every provider gives you to paste. EF Core wants the key-value form, so convert the URL once at start:

var url = new Uri(Environment.GetEnvironmentVariable("DATABASE_URL")!);
var userInfo = url.UserInfo.Split(':');
var conn = $"Host={url.Host};Port={url.Port};Database={url.AbsolutePath.TrimStart('/')};Username={userInfo[0]};Password={userInfo[1]};SSL Mode=Require;Maximum Pool Size=5";
builder.Services.AddDbContext<AppDb>(o => o.UseNpgsql(conn));

// after Build():
using (var scope = app.Services.CreateScope())
    scope.ServiceProvider.GetRequiredService<AppDb>().Database.Migrate();

Maximum Pool Size=5 keeps one container far inside a PostgreSQL Mini's 50 connections. Running Migrate() at start is safe with a single container and keeps schema and code in the same deploy. Options for the database itself: PostgreSQL or MySQL Mini ($29/month, connection string injected), a $1 DB Sprint Pack for 12 hours, or a free external plan such as Neon.

Deploying

  1. Commit the Dockerfile and a .dockerignore with bin/, obj/ and .git.
  2. Push. Containers → Deploy from GitHub → pick the repository. The port is read from EXPOSE (8080).
  3. Add environment variables: DATABASE_URL (or attach an add-on), any API keys, and ASPNETCORE_ENVIRONMENT=Production if it is not in the Dockerfile.
  4. Deploy. The first build takes a few minutes (SDK image pull plus dotnet restore); later builds reuse the restore layer. Open /health on the container URL.

Test the image before you push

docker build -t api .
docker run --rm -p 8080:8080 -e PORT=8080 api
curl -i http://localhost:8080/health

If dotnet publish fails here it fails on the platform; if the container starts but the health check is refused, the app bound to localhost. Both cost a deploy from the daily allowance when found on the platform and nothing when found here.

Where free stops

  • Free containers sleep after 15 minutes without traffic and wake in about a minute; hosted services and timers inside the app only run while it is awake.
  • 100 container-hours a month across four containers, counted while awake.
  • No free database; custom domains and no sleeping need Always-On ($12/month per container).
  • Data Protection keys, if you use cookies or antiforgery, must be persisted outside the container (in the database, via PersistKeysToDbContext), or every restart signs everyone out.

Configuration the .NET way

ASP.NET Core's configuration reads environment variables automatically, with a double underscore standing in for the colon of a nested key. So a setting the code reads as Configuration["ConnectionStrings:Default"] or Configuration["Smtp:Host"] is set in the container as ConnectionStrings__Default and Smtp__Host, with no code change and no appsettings.Production.json committed with secrets in it. Keep appsettings.json for non-secret defaults and let the container's environment override the rest; that split is what makes the same image work in staging and production.

Numbers used: .NET support dates from Microsoft's support policy (September 2026); SnapDeploy free tier from the free tier docs (4 containers, 512 MB, 0.25 vCPU, 100 container-hours a month, no card); database add-ons from the add-on pricing page.

Ready to Deploy?

Deploy free. 10 deploys a day, 100 hours a month, no credit card.

Run this yourself: Get a dedicated NVIDIA T4 (16 GB) for a flat $499/mo or A10G (24 GB) for $999/mo — never shared, never spot, no hourly metering. See dedicated GPU hosting →

One-click Ollama, vLLM, ComfyUI, Whisper, PyTorch & more — or deploy your own GitHub repo or Docker image. Compare plans.

FIRST OF ITS KIND

Sprint Pack — ₹99 for 24 hours

The first managed container PaaS to offer a one-time 24-hour Always-On pass under $5. Unlimited deploys + Always-On for one Small (512 MB) container — including WebSockets & real-time apps. No subscription, no auto-renewal. Stack two for 48 hours.

Need a managed database too? DB Sprint Pack — ₹99 / 12h for Postgres, MySQL, MariaDB, Mongo, Redis, or RabbitMQ.

Tip: Need 24 hours of Always-On for a demo, weekend, or quick test — or WebSockets & real-time apps? Sprint Pack is ₹99 one-time — no subscription, no auto-renewal.

Need a managed Postgres / MySQL / Mongo / Redis instead? DB Sprint Pack — ₹99 / 12h.

Take SnapDeploy with you

Deploy, monitor and wake your containers from your iPhone.

Download on the App Store

Get DevOps Tips & Updates

Container deployment guides, platform updates, and DevOps best practices. No spam.

Unsubscribe anytime. We respect your privacy.

More Articles