MUSA Graderv0.1.0

User guide

Docker sandbox

Student code is untrusted. By default every notebook runs in its own short-lived container that can't reach the network, the rest of the disk, or the grading machine's credentials.

Build the image#

Build it once per semester, and again after changing the sandbox packages:

docker build -t musa-grader:latest -f docker/Dockerfile .

Isolation flags#

The grader starts one container per submission with:

FlagWhy
--network noneStudent code can't reach the internet or the campus network
--memory 2g --memory-swap 2gA runaway allocation kills the container, not the laptop
--cpus 1.0 --pids-limit 256One submission can't starve the others
--read-only + --tmpfs /tmpThe image can't be modified, and scratch space is discarded
-v <workdir>:/gradingOnly the copied submission is visible, never the rest of the disk

No host environment variables are forwarded, so API keys and credentials on the grading machine are not visible to student code. Outside a container, the grader passes --user <your uid> to the sandbox, so docker mode works on a Linux host as well as on Docker Desktop.

Local mode has none of this

--mode local (or Unsafe Local in Settings) runs student notebooks directly on your machine with no network, memory or filesystem isolation. Use it for the bundled demo submissions or for debugging, not for a real class.

Memory and time#

Assignment 3 is the heavy one: a 24 MB GeoJSON, a spatial join over 34,108 points and two masks of a ten-band raster. It fits inside the 2 GB cap, but a correct submission takes a few minutes rather than a few seconds, so the rubric raises execution_timeout_seconds to 1800. Set the same on the command line with --timeout and --cell-timeout.

What is in the image#

The base list (pandas, matplotlib, seaborn, altair, geopandas, shapely) covers Assignments 1 and 2. Assignment 3 adds rasterio, rasterstats, hvplot, holoviews, bokeh, panel, cartopy, geoviews and mapclassify. Packages are pinned in docker/requirements-sandbox.txt.

  • geoviews: hvplot.polygons() on a GeoDataFrame raises ModuleNotFoundError without it. A package missing from the image is not the student's mistake, but it would give a correct submission a zero.
  • rasterio >= 1.4.4: earlier 1.4.x releases have no linux/arm64 wheel.
  • rasterstats >= 0.21.0: 0.20.0 requires fiona, which has no linux/arm64 wheel at all.

Both pin issues appear only on Apple Silicon, never on an Intel machine.

If a student notebook needs a package that isn't in the image, add it to the pinned list and rebuild. Don't install packages at grading time: the container has no network.

Updating the pins#

Dependabot proposes bumps to requirements-sandbox.txt. CI rebuilds the image for every such PR, imports every package the assignments use, and grades a real notebook in it. Passing CI means the image still works. It doesn't mean every student's numbers are unchanged, so merge sandbox bumps between assignments, not in the middle of grading one.

Running the app in a container#

docker/app.Dockerfile packages the Streamlit UI and CLI, and docker-compose.yml at the repository root runs it:

docker build -t musa-grader:latest -f docker/Dockerfile .
docker compose up -d --build

Open http://localhost:8501. Sessions are saved in results/ and uploads in .musa_grader_data/. Anything you put in submissions/ can be loaded from the UI as submissions.

The app never runs student code itself. It reaches the host's Docker daemon through the mounted socket and starts one sandbox container per submission as a sibling, with every flag in the table above. Three details make this work:

DetailWhy
/tmp/musa-grader is mounted at the same path on both sidesThe host daemon resolves the sandbox's -v <workdir>:/grading, so the path the app sees must exist on the host
The app runs as uid 10001, the sandbox's userWork directories are mode 0700, and the sandbox must be able to open them
The entrypoint joins the socket's group, then drops rootThe socket's gid is 0 on Docker Desktop and docker on Linux

The Docker socket is root-equivalent

Mounting it gives the app container root-equivalent access to the host. That is acceptable for a tool that runs on a TA's own machine and binds to localhost only. Don't expose port 8501 beyond your machine.