Troubleshooting
Container won't start
Symptom. A student's workspace shows "starting" indefinitely, or the container status is "error". The admin panel shows no running container for that workspace.
Cause. Docker is not running, or the workspace image is missing.
Fix.
# Confirm Docker is running
docker info
# Check whether the workspace image exists
docker images | grep coderunner-workspace
# If missing, pull both images (control + workspace)
docker compose pull
# From-source host checkout: pull or build the workspace image directly
bun run docker:pull:workspace # or docker:build:workspace to build locally
In port mode (the host dev loop, or a docker compose deployment with
FRC_CONTAINER_NETWORK explicitly unset), check for port conflicts: if all
ports in SIM_PORT_RANGE or VSCODE_PORT_RANGE are in use, container startup
fails. Verify the ranges are not overlapping with other services on the host.
Defaults are 25810–25899 (sim NT4) and 33000–33099 (codium-server).
This does not apply to network mode — the default for docker compose
deployments — where workspace containers publish no host ports at all; see
decision 031.
After the image is present and Docker is healthy, the student's container will start on their next workspace open.
Control plane can't reach the Docker socket (permission denied)
Symptom. The control container crash-loops. The logs show
permission denied while connecting to /var/run/docker.sock, and no workspace
containers ever start.
Cause. The control container runs as a non-root user and needs the group
that owns the socket added as a supplementary group to reach it.
CODERUNNER_DOCKER_GID does not match that group. This is a Linux and WSL2
problem: the 0 default matches Docker Desktop's root-owned socket on macOS and
native Windows, but Linux and WSL2 own the socket by their docker group
instead — including under Docker Desktop's WSL2 integration.
Fix. Look up the host's real docker group gid and set it in .env:
stat -c '%g' /var/run/docker.sock # e.g. 999
# in .env
CODERUNNER_DOCKER_GID=999
Then recreate the container to pick up the corrected group_add:
docker compose up -d control
Control plane can't write /data (SQLITE_READONLY / read-only data dir)
Symptom. The control container fails at startup (or on the first write)
with SQLITE_READONLY: attempt to write a readonly database, or logs an error
about being unable to write under /data.
Cause. The bind-mounted ./data (or files inside it) is not owned by the
CODERUNNER_UID:CODERUNNER_GID the control container runs as. This is typically
leftover root:root files from a pre-non-root deployment that ran the control
plane as root, or a ./data that Docker recreated root-owned after the
directory was deleted.
Fix. Confirm the ownership matches the configured uid:gid (default
1000:1000), then reclaim it on the host:
ls -ln data # check the owner uid/gid
stat -c '%g' /var/run/docker.sock # (for the docker gid, if also affected)
# Reclaim the data dir for the control container's uid:gid (adjust to yours)
sudo chown -R 1000:1000 data
Then restart the control plane (docker compose up -d control). Never delete
the ./data directory itself — Docker would recreate it root-owned and this
failure would return.
Build times out
Symptom. A run shows "failed" after approximately 90 seconds with no obvious error. The problem typically occurs on the student's first build of a new project.
Cause. The Gradle build exceeded RUN_BUILD_TIMEOUT_MS (default: 90000 ms,
i.e. 90 seconds). Cold-cache first builds can legitimately take 2–3 minutes on
slower hosts.
Fix. Increase the timeout in your .env:
RUN_BUILD_TIMEOUT_MS=180000
Restart the control plane for the change to take effect. Subsequent builds are
much faster because Gradle's incremental cache (in data/users/*/home/) is
warm.
Sim doesn't start after a successful build
Symptom. The build succeeds (the console shows Gradle output), but the run stays in "building" for 30 seconds and then fails.
Cause. The WPILib simulator process did not report readiness within
SIM_STARTUP_TIMEOUT_MS (default: 30000 ms).
Fix. Increase the startup timeout:
SIM_STARTUP_TIMEOUT_MS=60000
If this happens consistently for one student, check the container logs:
docker logs coderunner-workspace-<hex> --tail 100
(The container name is coderunner-workspace- followed by the workspace id's
hex suffix, with the ws_ prefix dropped.)
Look for the simulator failing to bind its HALSim port. If HALSIM_PORT_RANGE
ports are exhausted, restart the control plane or stop idle containers to free
leases. This only applies in port mode — network-mode deployments don't lease
host ports for workspace containers, so exhaustion here means
MAX_ACTIVE_CONTAINERS instead (see below).
PathPlanner does not load or misses external edits
PathPlanner shows a 503. Its web artifact is missing. In a source checkout,
run bun run fetch:pathplanner, which downloads it and reports why if it
cannot. Published control images always carry it — the release build fails
rather than shipping without it — so in a Compose deployment pull and recreate
the control container. A locally built image can legitimately be missing it:
bun run docker:build:control downloads the artifact best-effort, so rebuild
once the external PathPlanner release is reachable again.
An edit made in VSCodium does not appear. Reload the page. External file changes are not synchronized into an open PathPlanner session.
OAuth login fails
Symptom. Students see an OAuth error page, a "not authorized" flash, or are silently redirected back to the login page.
Cause: wrong callback URL. The OAuth app registration does not include
the actual host URL. Fix by ensuring the callback URL registered with GitHub
or Google matches BETTER_AUTH_URL:
- GitHub:
<BETTER_AUTH_URL>/api/auth/callback/github - Google:
<BETTER_AUTH_URL>/api/auth/callback/google
BETTER_AUTH_URL must be the externally reachable base URL of the control
plane (for example https://coderunner.yourteam.ca). On a local deployment
it is http://<host-ip>:4000.
Cause: email not on allowlist. The student's email or domain is not in
data/allowlist.json. Check and add:
bun run allowlist:list
bun run allowlist:add student@gmail.com
# or allow a whole domain
bun run allowlist:add yourteam.org
On a containerized deployment run these inside the control container instead
(cd /opt/coderunner && sudo on the VM):
docker compose exec control coderunner allowlist list|add <email-or-domain>.
Cause: empty allowlist. If the allowlist is empty, everyone is blocked.
Confirm with bun run allowlist:list and add at least one entry.
Port range exhausted
This applies to port mode only (the host dev loop, or a docker compose
deployment with FRC_CONTAINER_NETWORK explicitly unset). Network mode — the
default for docker compose deployments — never leases host ports for
workspace containers, so it can't hit this failure; its concurrency limit is
MAX_ACTIVE_CONTAINERS alone (see Capacity).
Symptom. Container startup fails with a log message about no free ports, or many students get "server at capacity" even when the concurrency cap has not been reached.
Cause. All ports in SIM_PORT_RANGE, VSCODE_PORT_RANGE, or
HALSIM_PORT_RANGE are leased (or stale leases were not cleaned up).
Fix. Each range supports 90 concurrent leases by default (e.g.
25810–25899). If you have more than 90 simultaneous students, expand the
ranges:
SIM_PORT_RANGE=25810-25999
VSCODE_PORT_RANGE=33000-33199
HALSIM_PORT_RANGE=34000-34199
Stale leases can accumulate if containers were stopped without the control plane running. Restart the control plane; startup reconciles Docker container state against the database and releases stale leases.
Disk full
Symptom. File saves fail, builds fail with I/O errors, or container startup
fails. df -h / shows the data partition at or near 100%.
Cause. Gradle caches, run logs, and Docker image layers have grown to fill the disk.
Fix. Free space in order of safety:
# 1. Prune run logs (safest, often largest single contributor)
find data/users/*/logs/runs -name "*.log" -delete
# 2. Prune stopped managed containers and their layers
bun run docker:cleanup
docker system prune -f
docker builder prune -f
# 3. Prune Gradle caches for all workspaces (stop containers first)
for dir in data/users/*/; do
rm -rf "$dir/home"
mkdir -p "$dir/home"
done
Never delete data/users/*/project/; that is student source code. If space
is critically low, back up project files first:
bun run backup
Control plane crashes or becomes unresponsive
Symptom. The browser shows disconnected. docker compose ps shows the
control container unhealthy or restarting, or its logs show a fatal error.
Cause. An unhandled exception, OOM on the host, or a corrupt database.
Fix. Restart the control plane and check its logs (prefix with
cd /opt/coderunner && sudo on the VM):
docker compose restart control
docker compose logs --tail 100 control
On startup the control plane reconnects to existing containers via Docker labels, reconciles container state with the database, and resumes the idle sweep. Student files and running containers are preserved across restarts.
Student workspace is at capacity (503)
Symptom. A student sees a "Server at capacity" toast when opening their workspace. Other students with running containers are unaffected.
Cause. The active container count has reached MAX_ACTIVE_CONTAINERS.
Fix. Check the admin panel or the admin API for current vs. maximum
container count. If idle containers have not yet been stopped, wait for the
idle sweep (or reduce IDLE_STOP_MINUTES). If the load is legitimate and the
host has headroom, raise the cap at runtime without restarting:
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
-X POST -H "Content-Type: application/json" \
-d '{"value": 15}' \
http://localhost:4000/admin/config/max-active-containers
Verify host memory and CPU before raising the cap further; see Capacity for sizing guidance.