The grafana/k6 image runs as UID 12345 - what does that mean for a mounted script and output directory?
answer
- the container is never root
- reading is easy, writing is not
- the working directory is inside the container
- a relative path silently goes nowhere
basics
~20 sThe grafana/k6 image runs as UID 12345 with WORKDIR /home/k6. A mounted script need only be readable by that user, but any directory k6 writes into must be writable by it, and produced files are owned by 12345.
solid answer
~40 sThe image is not root. Its runtime stage creates a user with **UID 12345**, switches to it with `USER 12345`, and sets `WORKDIR /home/k6`. Reading is usually painless: a script tree mounted read-only just has to be world- or group-readable. Writing is where it bites. Anything k6 produces into a mounted host directory is created by UID 12345, so the host directory must be writable by that user, and the resulting file is owned by it - awkward on a runner whose workspace is owned by someone else. There is also a quieter failure: because `WORKDIR` is `/home/k6`, a relative output path lands inside the container, succeeds, and vanishes when the container exits. Either pass `-u` to run as the host user, or mount a directory that UID 12345 can write.
code
bash · 6 lines# Script tree read-only, results directory writable, run as the host user
mkdir -p results
docker run --rm -u "$(id -u)" \
-v "$PWD/tests:/tests:ro" \
-v "$PWD/results:/results" \
grafana/k6 run /tests/smoke.jsgo deeper
Know that the image runs as a non-root user, UID 12345, with its working directory at /home/k6. Mounted files must be readable by that user, and anything you want to keep must be written under a mount.
Distinguish the two failures. A write error means the mounted host directory is not writable by UID 12345; a run that produces nothing means the path was relative and resolved inside the container instead.
Design the step so both are impossible: one read-only script mount, one writable results mount, and an output path that is always under a mount. Be ready to say why the produced files carry an unfamiliar owner.
Decide the team's convention for artifact ownership: override the user so files belong to the runner account, or accept UID 12345 and make every cleanup and archive step tolerate it. Mixing the two per job is what causes flaky pipelines.
## What the image bakes in The runtime stage of the k6 Dockerfile does four things that matter here, and they are all one line each: it adds a user named `k6` with **UID 12345**, switches to that user with `USER 12345`, sets **`WORKDIR /home/k6`**, and declares `ENTRYPOINT ["k6"]`. No process in the container ever runs as root, and `/home/k6` is the directory every relative path is interpreted against. That combination produces two quite different problems - one loud, one silent - and telling them apart is most of the skill here. ## Reading a mounted script Reading is the easy half. When you mount a script tree and pass an in-container path, k6 only needs to open those files: - A read-only mount is enough; k6 never writes into the script tree. - The files must be readable by UID 12345. Repository checkouts are normally world-readable, so this usually just works. - It stops working when the checkout is deliberately tightened - a directory with mode `0700`, or files owned by a user with a restrictive umask - because UID 12345 matches neither the owner nor the group. - Piping with `run -` sidesteps the question entirely: nothing is mounted, so nothing needs permissions. That is one real reason the piped form survives in CI long after the suite outgrows it. ## Writing anything back out Writing is where UID 12345 becomes visible, and it fails in two distinct ways: | symptom | cause | what to look at | |---|---|---| | the run errors while opening a file for writing | the mounted host directory is not writable by UID 12345 | ownership and mode of the host directory, not of the file | | the run succeeds and the host has nothing new | the path was relative, so it resolved under `WORKDIR /home/k6`, which is inside the container | whether the path k6 was given is under a mount at all | | the file appears but the runner cannot clean it up | k6 created it as UID 12345 | ownership of the produced file | The middle row is the dangerous one, because there is no error at all. `/home/k6` belongs to the image's own user and is perfectly writable, so k6 writes the file, reports success, and the container is discarded with the file inside it. The documented browser invocation avoids exactly this by mounting the host directory at `/home/k6/screenshots` - a path *under* the working directory - rather than trusting a relative path to land somewhere useful. ## The ways out 1. **Run as the host user.** Docker's `-u` flag overrides `USER 12345` at run time, which is what k6's own documentation does when it has the image create a file for you: `docker run --rm -u $(id -u) -v $PWD:/app -w /app grafana/k6 new`. The container then writes as you, and the file is yours. 2. **Give UID 12345 a writable directory.** Mount a directory the image's user can write - a dedicated scratch directory rather than the whole workspace - and keep the script mount separate and read-only. 3. **Write to standard output instead of a file.** Nothing needs to be writable if the result leaves through the container's stdout and the job captures it there. ## In a CI job A containerised step is usually created and destroyed inside a workspace owned by the runner's own user, which is very often not UID 12345. Two habits keep this from becoming a recurring incident: - **Separate the two mounts.** Mount the script tree read-only and mount one writable results directory. It makes the failure legible: a read error is a script-mount problem, a write error is a results-mount problem. - **Never pass a relative output path.** Always give k6 a path underneath a mount, so that a run producing nothing is impossible rather than merely unlikely. If artifacts do come back owned by UID 12345, the job's cleanup and archive steps have to cope with that ownership, which is the usual reason teams settle on `-u` instead. ## What not to conclude - The image is not broken and does not need rebuilding; running unprivileged is deliberate. - The UID is a property of the published image, not of k6 the binary - a locally installed `k6` runs as whoever invoked it and has none of these constraints. - A read failure and a write failure have different fixes; widening permissions on the script tree will not make a results directory writable.
- Why can a containerised k6 run succeed and still leave nothing on the host?Because `WORKDIR` is `/home/k6`, a relative path resolves inside the container. That directory belongs to the image's own user, so the write genuinely succeeds - and then the container is removed and the file with it. Always point k6 at a path under a mount.
- Does a read-only script mount avoid the permission problem entirely?It avoids the write half, not the read half. UID 12345 still has to be able to read those files, so a checkout with restrictive ownership or a `0700` directory still fails. It does guarantee k6 cannot modify the script tree, which is worth having.
- Why does k6's own documentation add `-u $(id -u)` when creating a file with the image?Because the created file would otherwise be owned by UID 12345 and awkward for the invoking user to edit or delete. Overriding the user makes the container write as you, so the artifact belongs to the account that asked for it.
saying these in an interview costs you the question
- Assumes the container runs as root
- Widens permissions on the script mount to fix a write error
- Ignores that a relative output path stays inside the container
- Thinks a missing artifact means the run failed
- Expects produced files to be owned by the invoking user