How to run Haskell code in a sandbox
Install GHC in a Linux microVM, from apt or GHCup, then run the file with runghc or compile it with ghc and run the binary.
On Runtime GHC comes from Ubuntu's archive in one command, and a compile that
takes a while is billed only for the CPU it uses. Each sandbox is a
Firecracker microVM running Ubuntu 24.04.5, where ghc, cabal-install and
haskell-stack are apt packages. Runtime charges $0.025 per vCPU-hour of
measured CPU and $0.0075 per GiB-hour of memory, so a 2 vCPU, 4 GiB sandbox
costs $0.08 an hour at full load and $0.03125 while it waits
(pricing).
What to install
Haskell is not in the default image. These are the options, read on 25 September 2026:
| Tool | Ubuntu 24.04 (noble) package | Version on noble | Use it for |
|---|---|---|---|
| GHC | ghc |
9.4.7 | ghc and runghc |
| Cabal | cabal-install |
3.8.1.0 | Packages from Hackage |
| Stack | haskell-stack |
2.9.3.1 | Stackage snapshots |
| GHCup (not in apt) | get-ghcup.haskell.org |
its own | Newer GHC and switching versions |
GHCup installs into ~/.ghcup/bin, which in a sandbox is
/workspace/.ghcup/bin (GHCup). Its
script runs without prompts when BOOTSTRAP_HASKELL_NONINTERACTIVE=1 is set.
Run a Haskell file
runghc interprets a file with no build step. It suits short programs and
exercises; compile anything that loops heavily.
TypeScriptimport { Sandbox } from "withruntime";const program = `import Data.List (sortOn)import qualified Data.Map.Strict as Mmain :: IO ()main = do let ws = words "the quick brown fox jumps over the lazy dog the end" counts = M.toList (M.fromListWith (+) [(w, 1 :: Int) | w <- ws]) mapM_ print (take 3 (sortOn (negate . snd) counts))`;await using sbx = await Sandbox.create({ timeoutSeconds: 900, onLeaseEnd: "stop" });await sbx.exec("sudo apt-get update -q && sudo apt-get install -y -q ghc", { check: true, timeoutMs: 600_000,});await sbx.network.set({ internet: false });await sbx.files.write("/workspace/Main.hs", program);const run = await sbx.exec(["runghc", "Main.hs"], { timeoutMs: 120_000 });console.log(run.exitCode, run.timedOut, run.stdout);Pythonfrom withruntime import Sandboxprogram = """import Data.List (sortOn)import qualified Data.Map.Strict as Mmain :: IO ()main = do let ws = words "the quick brown fox jumps over the lazy dog the end" counts = M.toList (M.fromListWith (+) [(w, 1 :: Int) | w <- ws]) mapM_ print (take 3 (sortOn (negate . snd) counts))"""with Sandbox.create(timeout_seconds=900, on_lease_end="stop") as sbx: sbx.exec("sudo apt-get update -q && sudo apt-get install -y -q ghc", check=True, timeout_ms=600_000) sbx.network.set(internet=False) sbx.files.write("/workspace/Main.hs", program) run = sbx.exec(["runghc", "Main.hs"], timeout_ms=120_000) print(run.exit_code, run.timed_out, run.stdout)Data.Map comes from the containers library, which ships with GHC, so
nothing is fetched from Hackage.
Compile, then run
Compile with optimisation, then run the binary under its own time limit. A type error comes back from the compile step on stderr, with the file, line and column, and the run never starts.
TypeScriptimport { Sandbox } from "withruntime";await using sbx = await Sandbox.create({ image: "haskell", network: { internet: false } });await sbx.files.write( "/workspace/Sum.hs", "main :: IO ()\nmain = print (sum [x * x | x <- [1 .. 1000000 :: Int], even x])\n",);const build = await sbx.exec(["ghc", "-O2", "-rtsopts", "-o", "sum", "Sum.hs"], { timeoutMs: 300_000,});if (build.exitCode !== 0) throw new Error(build.stderr);const run = await sbx.exec(["./sum", "+RTS", "-M512m", "-RTS"], { timeoutMs: 30_000 });console.log(run.stdout);-rtsopts lets the binary take runtime options, and +RTS -M512m -RTS then
caps its heap at 512 MB from inside GHC's runtime, on top of the sandbox's own
memory size. This uses the haskell image built
below.
Build a Cabal project
Cabal downloads the package index and dependencies from Hackage. Let it do that with the network on, then run the tests offline:
Pythonimport sysfrom withruntime import Sandboxwith Sandbox.create(image="haskell", disk_mib=8192, timeout_seconds=3600, on_lease_end="stop") as sbx: sbx.files.upload("./parser", "/workspace/parser") opts = {"cwd": "/workspace/parser", "timeout_ms": 1_800_000, "on_stdout": sys.stdout.write} sbx.exec("cabal update && cabal build --only-dependencies --enable-tests", check=True, **opts) sbx.network.set(internet=False) tests = sbx.exec(["cabal", "test"], **opts) sys.exit(tests.exit_code or 0)Dependencies compile from source, which is the slow part of a first Haskell build. Do it once and snapshot the sandbox, and later runs start with the dependencies already built.
Start every sandbox with GHC
TypeScriptimport { Runtime } from "withruntime";const runtime = new Runtime();await runtime.images.build({ name: "haskell", recipe: { apt: ["ghc", "cabal-install"] } });await using sbx = await runtime.sandboxes.create({ image: "haskell" });console.log((await sbx.exec(["ghc", "--version"])).stdout);Terminalruntime image build --apt ghc,cabal-install --name haskellBuilding an image is free, and the free trial stores your first three images free (custom images).
Why run Haskell in a sandbox at all?
Types do not stop side effects. A Haskell program in IO can delete files,
start processes and open sockets like any other, and Template Haskell runs code
at compile time. A sandbox puts the compiler and the program in a microVM with
its own kernel, with network, CPU, memory and cost enforced on the host
(security). New accounts get 50 free sandbox hours, no card:
Terminalnpx withruntime loginSee also grading student code, running untrusted LLM code and how to run Elixir code in a sandbox.
Sources
Checked 25 September 2026.
- Ubuntu 24.04 (noble) packages: ghc, cabal-install, haskell-stack
- GHCup installation and its bootstrap script (
BOOTSTRAP_HASKELL_NONINTERACTIVE)
Facts on this page were checked on 25 September 2026.