Skip to content
Guides · Starters

How Do I Set Up a .NET Dev Environment in a Cloud Sandbox?

Updated October 27, 2026 .md

Short answer: A new sandbox has no .NET. Microsoft's dotnet-install.sh put SDK 10.0.403 in ~/.dotnet in 10 seconds. A first build of a real ASP.NET Core solution took 22 seconds on small; from a named snapshot with the NuGet cache and bin/obj, it took 2.7 seconds. Raise the inotify limit before running two dotnet watch sessions.

We ran every command on this page on prod on 2026-10-27 between 12:03 and 14:15 UTC, on small sandboxes (2 vCPU, 4 GB) in eu-central. The project is ardalis/CleanArchitecture at commit e7c79be (2026-09-20): an ASP.NET Core 10 solution with six src projects, four test projects, central package management and TreatWarningsAsErrors on.

Results#

Step Time on small
Install SDK 10.0.403 with dotnet-install.sh (624 MB) 9.9 s
dotnet restore, empty package folder, warm HTTP cache 9.6 s
dotnet build, first 21.7 s
dotnet build, nothing changed, build servers running 2.3 s
dotnet build, one file in the Core project changed 3.8 s
dotnet build, nothing changed, after dotnet build-server shutdown 3.8 s
Unit tests (12) / integration tests (4), --no-build 2.5 s / 2.8 s
dotnet restore + dotnet build, warm package folder, no bin/obj 1.9 + 19.2 s
dotnet watch: start to first 200 about 12 s
dotnet watch: hot reload of a method body 1.4 s
From the named snapshot: restore / build / both test projects 1.8 / 2.7 / 4.8 s
From the named snapshot: restore + build with bin/obj removed 15.4 s

The lesson: in .NET, the NuGet cache is big but cheap to refill; the compile is what you wait for. A warm package folder saved only a few seconds of restore, and the build still took 15 to 19 seconds. Keep bin and obj in the snapshot too, and MSBuild finds almost nothing to do.

1. Check what the sandbox has#

sbx new --name dotnetdev-1 --type small
sbx exec sb_0dsmmxal12 bash -lc "command -v dotnet || echo no-dotnet; grep PRETTY /etc/os-release"
no-dotnet
PRETTY_NAME="Ubuntu 24.04.5 LTS"

There is no .NET in the image on 2026-10-27; the rest is listed in What's pre-installed in a sandbox. Ubuntu's own archive has SDK packages. After sudo apt-get update:

dotnet-sdk-8.0:
  Candidate: 8.0.132-0ubuntu1~24.04.1
dotnet-sdk-10.0:
  Candidate: 10.0.113-0ubuntu1~24.04.1

Those are the 1xx feature band. Microsoft's current SDK on 2026-10-27 was 10.0.403 (runtime 10.0.14, LTS), from the official release index, dated 2026-10-25. Feature bands matter because of global.json. With the default rollForward of latestPatch, a repo that pins 10.0.403 will not accept a 10.0.1xx SDK (global.json rules). We read the apt candidates; we did not install them.

2. Install the current SDK into your home directory#

Microsoft's install script installs without root and lets you pick a channel or an exact version:

sbx exec sb_0dsmmxal12 --timeout 300 bash -lc '
  curl -sSLo dotnet-install.sh https://dot.net/v1/dotnet-install.sh && chmod +x dotnet-install.sh
  ./dotnet-install.sh --channel 10.0 | tail -1
  cat >> ~/.profile <<"EOF"
export DOTNET_ROOT=$HOME/.dotnet
export PATH=$PATH:$DOTNET_ROOT:$DOTNET_ROOT/tools
export DOTNET_CLI_TELEMETRY_OPTOUT=1
EOF'
sbx exec sb_0dsmmxal12 bash -lc "dotnet --list-sdks; dotnet --list-runtimes"
dotnet-install: Installation finished successfully.
10.0.403 [/home/user/.dotnet/sdk]
Microsoft.AspNetCore.App 10.0.14 [/home/user/.dotnet/shared/Microsoft.AspNetCore.App]
Microsoft.NETCore.App 10.0.14 [/home/user/.dotnet/shared/Microsoft.NETCore.App]

The install took 9.9 seconds and used 624 MB. Everything sits under /home/user, so it survives sbx stop and goes into a named snapshot. To match a repo exactly, use ./dotnet-install.sh --jsonfile global.json or --version 10.0.403. sbx exec ... bash -lc reads ~/.profile, so later commands find dotnet on PATH.

3. Restore and build a real solution#

sbx exec sb_0dsmmxal12 --timeout 300 bash -lc "git clone -q https://github.com/ardalis/CleanArchitecture && cd CleanArchitecture && git checkout -q e7c79be54ba0 && dotnet restore Clean.Architecture.slnx"

At this commit, restore failed on 2026-10-27:

src/Clean.Architecture.Web/Clean.Architecture.Web.csproj : error NU1903: Warning As Error: Package 'System.Security.Cryptography.Pkcs' 10.0.0 has a known high severity vulnerability
tests/Clean.Architecture.IntegrationTests/Clean.Architecture.IntegrationTests.csproj : error NU1903: Warning As Error: Package 'SSH.NET' 2025.1.0 has a known high severity vulnerability
…

This is NuGet audit, not the sandbox. Restore checks every package against advisories published after the commit was made. The repo sets TreatWarningsAsErrors, so the NU1902 and NU1903 warnings become errors. Any pinned .NET commit can start failing like this months later. The real fix is to update the packages. To build the commit as it is, keep those two codes as warnings:

export WarningsNotAsErrors="NU1902;NU1903"

MSBuild reads environment variables as properties, so this works for restore, build, test and watch without editing the repo. We ran the timed steps with the same value passed as -p:WarningsNotAsErrors=.... Because they run past the 30-second default of sbx exec, we used --detach and read the log:

== restore, empty NuGet cache
9.580
2.1G	/home/user/.nuget/packages
245
249 MB /home/user/.nuget/packages/kubernetesclient
172 MB /home/user/.nuget/packages/aspire.hosting.orchestration.linux-x64
158 MB /home/user/.nuget/packages/grpc.tools
== build, first
21.725
    15 Warning(s)
    0 Error(s)
== build, nothing changed, build servers warm
2.337
== build, one file in Core changed
3.826
== processes left after build
247 MB /home/user/.dotnet/dotnet /home/user/.dotnet/sdk/10.0.403/MSBuild.dll

Three things to know:

  • There are two NuGet caches. ~/.nuget/packages (2.1 GB, 245 packages) is what builds read. ~/.local/share/NuGet/http-cache (468 MB here) holds the downloads. Our 9.6-second restore had an empty package folder but a warm HTTP cache from the failed first attempt, which had taken 19 seconds to download everything. The Aspire and Kubernetes packages are most of the size.
  • Build servers stay in memory. After a build, an MSBuild node (247 MB) and the Roslyn compiler server keep running, which is why the no-change build took 2.3 seconds. On a 4 GB small, run dotnet build-server shutdown when you need the memory back.
  • Tests that need Docker images. The unit and integration tests passed (12 and 4). The functional tests start SQL Server through Testcontainers, which pulls a SQL Server image; we did not run them. Docker is installed in the sandbox.
== unit tests
Passed!  - Failed:     0, Passed:    12, Skipped:     0, Total:    12, Duration: 86 ms - Clean.Architecture.UnitTests.dll (net10.0)
== integration tests
Passed!  - Failed:     0, Passed:     4, Skipped:     0, Total:     4, Duration: 716 ms - Clean.Architecture.IntegrationTests.dll (net10.0)

4. dotnet watch on a private URL#

The web project's launch profile wants HTTPS on localhost:57679, which needs a dev certificate. Skip the profile and listen on plain HTTP on 127.0.0.1. The sandbox's public IPv6 firewall (ufw) is closed except SSH by default, and binding to 127.0.0.1 keeps the port private whatever the firewall does. Sandboxer's proxy adds TLS in front:

# ~/watch.sh in the sandbox
cd ~/CleanArchitecture
export ASPNETCORE_FORWARDEDHEADERS_ENABLED=true WarningsNotAsErrors="NU1902;NU1903" ASPNETCORE_ENVIRONMENT=Development DOTNET_WATCH_SUPPRESS_LAUNCH_BROWSER=1
exec dotnet watch --project src/Clean.Architecture.Web run --no-launch-profile --urls http://127.0.0.1:5000
sbx exec sb_0dsmmxal12 --detach "bash -l watch.sh > watch.log 2>&1"
sbx host sb_0dsmmxal12 5000 --localhost --title "dotnet watch"
https://golden-solar-ginger-5000.box.sandboxer.com?_token=<token>

The app answered on 127.0.0.1:5000 about 12 seconds after the start. In Development it creates and seeds a SQLite file, database.sqlite. Through the hosted URL, a request without the token got 401. The token link got 302 in 0.20 seconds and set a cookie, and with the cookie GET /Contributors returned 200 with JSON in 0.21 seconds.

Set ASPNETCORE_FORWARDEDHEADERS_ENABLED. The proxy ends TLS and talks plain HTTP to the app. Without the variable, the app built its pagination Link header with the wrong scheme:

link: <http://golden-solar-ginger-5000.box.sandboxer.com/Contributors?page=2&per_page=3>; rel="next", …

With the variable set, ASP.NET Core reads X-Forwarded-Proto and the same request returned https://golden-solar-ginger-5000.box.sandboxer.com/Contributors?page=2&per_page=3. The same applies to redirects, OAuth callback URLs and anything else that builds absolute URLs.

Hot reload worked. We added one line to HandleAsync in Contributors/List.cs, setting an X-Edited response header. The log showed:

dotnet watch ⌚ File updated: ./src/Clean.Architecture.Web/Contributors/List.cs
dotnet watch 🔥 C# and Razor changes applied in 1427ms.

The next request through the hosted URL carried x-edited: hot without an app restart. The log also showed a Failed to read '…/obj\Debug/net10.0/staticwebassets.development.json' line with a Windows-style path. It did not stop the app or hot reload. With dotnet watch running, the sandbox used 1.1 GB of its 3.9 GB.

5. Raise the inotify limit before a second watcher#

dotnet watch on this solution held 72 inotify instances. The sandbox allows 128 per user. A second dotnet watch on the web project, for another port or another branch, crashed:

dotnet watch ❌ An unexpected error occurred: System.IO.IOException: The configured user limit (128) on the number of inotify instances has been reached, or the per-process limit on the number of open file descriptors has been reached.

Raise the limit, and write it to /etc/sysctl.d so it is still set after a boot from disk:

echo fs.inotify.max_user_instances=1024 | sudo tee /etc/sysctl.d/60-inotify.conf
sudo sysctl --system

With 1024, both watchers ran at once (146 instances in use), and the second answered 200 on port 5001. The value was still 1024 in a new sandbox made from the snapshot. An editor that watches files over SSH, such as VS Code with the C# extension, uses instances from the same limit. We did not test an editor.

Stopping dotnet watch. Started with --detach, it ignored SIGTERM and SIGINT in our test. sbx exec --kill <pid> ended the bash wrapper but left the watcher and the app running, still holding their inotify instances. This stopped it:

pkill -KILL -f "[d]otnet watch|[D]otnetTools/dotnet-watch|[C]lean.Architecture.Web"

The brackets matter when you send it through sbx exec. A plain pkill -f "dotnet watch" also matched the shell running the command and killed that first, so nothing else happened. For a server that should stay up, use a systemd unit as in Run a service that comes back after resume.

6. Save a named snapshot and start from it#

Stop the watcher and the build servers first. A snapshot keeps files, not processes:

sbx exec sb_0dsmmxal12 "dotnet build-server shutdown"
sbx snapshot sb_0dsmmxal12 dotnetdev-cache
sbx new --name dotnetdev-2 --type small --from dotnetdev-cache

The save took 2.1 seconds and the new sandbox was ready in 3.2 seconds. It held the SDK, both NuGet caches, the clone with bin and obj, and the inotify setting. In the new sandbox:

10.0.403
1024
== restore, as saved
1.803
== build, as saved, servers cold
2.725
== unit + integration tests, as saved
4.802
Passed!  - Failed:     0, Passed:    12, Skipped:     0, Total:    12, Duration:
Passed!  - Failed:     0, Passed:     4, Skipped:     0, Total:     4, Duration:

Then we removed every bin and obj directory and ran restore and build again: 15.4 seconds. That is close to the 21.1 seconds (1.9 + 19.2) in the first sandbox, so the package cache alone saves little. Plan for about 3.4 GB under /home/user: 2.1 GB of packages, 625 MB of SDK and 627 MB for the built clone. To name the snapshot by a hash of Directory.Packages.props and rebuild it when packages change, follow Cache dependencies in a named snapshot.

FAQ#

Does a Sandboxer sandbox come with .NET?#

No, not on 2026-10-27. Install it with Microsoft's dotnet-install.sh (SDK 10.0.403 in about 10 seconds, no root), or from Ubuntu's archive (dotnet-sdk-10.0 10.0.113 at the time). Check dotnet --list-sdks against the repo's global.json.

Why does dotnet restore fail with NU1903 on an old commit?#

NuGet audit flags packages with advisories published after the commit. In a repo with TreatWarningsAsErrors, that fails restore. Update the packages, or set WarningsNotAsErrors="NU1902;NU1903" to build the commit as it is.

The Sandboxer proxy ends TLS and forwards plain HTTP. Set ASPNETCORE_FORWARDEDHEADERS_ENABLED=true so the app uses X-Forwarded-Proto. In our test that changed the Link header from http:// to https://.

Why does a second dotnet watch crash with an inotify error?#

One dotnet watch on our solution used 72 of the 128 inotify instances the sandbox allows per user. Set fs.inotify.max_user_instances=1024 in /etc/sysctl.d and run sudo sysctl --system.

Is it worth snapshotting the NuGet cache?#

Snapshot bin and obj together with it. With both, the build took 2.7 seconds; with only the NuGet cache it took 15.4. The download itself was 19 seconds or less.

More guides

Top AI sandbox platforms for code execution in 2026, ranked

For running AI-written code we rank Sandboxer, Freestyle and E2B first: each gives code its own VM with an API or SDK and can pause with memory. Pick Daytona for Windows or GPUs, Modal or Northflank for GPUs at scale, E2B or Northflank to run in your own cloud, and Vercel Sandbox if your app lives on Vercel.

Read the guide
Best sandboxes for running coding agents (Claude Code, Codex, Gemini CLI) in 2026, ranked

For coding agents we rank Sandboxer first: a full Ubuntu VM with Claude Code and Codex preinstalled, Docker, and stop-and-resume that keeps the agent's session in memory. Freestyle and Amp Orbs also keep memory across pauses. Pick E2B for prebuilt agent templates on open source, Docker Sandboxes for free local runs, and Codespaces if you live in GitHub.

Read the guide
Best Daytona sandbox alternatives in 2026, ranked

The best Daytona alternatives depend on why you're switching. For a full Linux microVM on every sandbox with memory-saving stop and Docker, we rank Sandboxer first. E2B suits open source and self-hosting, Modal massive short-job fleets, Northflank bring-your-own-cloud with GPUs, and Vercel Sandbox apps on Vercel.

Read the guide
Best E2B alternatives in 2026, ranked

If you're leaving E2B for longer sessions or a fuller machine, we rank Sandboxer first, then Freestyle: both give each sandbox a full Linux VM that keeps memory across pauses, with Docker and no short session cap. Pick Daytona for Windows or GPUs, Northflank for bring-your-own-cloud, and stay on E2B if you need open source.

Read the guide