skip to content

Why does running the grafana/k6 image look like `docker run -i grafana/k6 run - < script.js`?

level: juniorimportance: must knowfreq 62%

answer

  1. the image already runs one binary
  2. arguments begin at the subcommand
  3. run wants exactly one argument
  4. the file never crossed the boundary
  5. a dash means standard input

basics

~20 s

The grafana/k6 image sets ENTRYPOINT ["k6"], so appended arguments start at a k6 subcommand - here, run. The script lives on the host and not in the image, so the filename - makes k6 read it from standard input.

solid answer

~40 s

Two image facts combine. The published image declares `ENTRYPOINT ["k6"]`, so the container's argument list begins exactly where you would otherwise type the subcommand: `docker run --rm grafana/k6 version` and `docker run --rm -i grafana/k6 run -` are both complete commands, and repeating `k6` after the image name is rejected as an unknown subcommand. Second, `k6 run` takes exactly one positional argument, and its own error text says that argument must be either a path to a script file or the literal `-`. Since nothing from your working copy is inside the image, `-` is the quick route: k6 drains standard input and runs those bytes. If the test needs more than one file, mount the directory instead and pass a real in-container path.

code

bash · 8 lines
bash
# Entrypoint is already k6, so the argument list starts at the subcommand
docker run --rm grafana/k6 version
docker run --rm grafana/k6 run --help

# The script is on the host, so read it from stdin with the filename `-`
docker run --rm -i \
  -e K6_NO_USAGE_REPORT=true \
  grafana/k6 run - < tests/smoke.js

go deeper

for a junior

Remember the two moving parts: the image's entrypoint is already k6, so you type the subcommand first, and - is the script argument that means standard input. Being able to write the command from memory is what is being checked.

for a middle

Explain why - exists at all: the host file is not inside the container, and k6 run takes exactly one positional argument that must be a container path or -. Say what breaks when standard input is not attached.

for a senior

Show you can pick the shape that fits the test. A single self-contained script pipes in fine; anything with local imports, data files or written output needs a mount and an in-container path, with UID 12345 able to read it.

for a principal

The judgment call is what the team standardises on. Piping is the least-configuration form and stays uniform across runners; mounting is the one that scales to a real test suite. Decide once and keep every job on the same shape.

## The image is a k6 binary behind a fixed front door The published `grafana/k6` image is deliberately minimal. Its runtime stage copies exactly one artifact - the `k6` binary, built with `CGO_ENABLED=0` - to `/usr/bin/k6`, adds a non-root user with **UID 12345**, sets `WORKDIR /home/k6`, and declares **`ENTRYPOINT ["k6"]`**. There is no shell wrapper, no scripts of your own, and nothing else to invoke. That entrypoint is the line that shapes every command you will ever type against this image. Because the entrypoint is already the binary, everything appended after the image name becomes k6's own argument list, starting where the subcommand would normally go: - `docker run --rm grafana/k6 version` runs `k6 version` - `docker run --rm grafana/k6 run --help` runs `k6 run --help` - `docker run --rm -i grafana/k6 run - < script.js` runs `k6 run -` Writing `grafana/k6 k6 run ...` out of habit fails. k6's root command dispatches only to subcommands such as `run`, `new`, `inspect`, `archive`, `cloud`, `deps` and `version`, and has no behaviour of its own, so the extra `k6` is rejected as an unknown command. `grafana/k6 script.js` fails for the same reason: a bare filename is not a subcommand, and k6 never treats one as an implicit `run`. ## `k6 run` accepts exactly one positional argument `run` is declared with an exact-arity check of one, and its own message spells out what the argument may be: *arg should either be `-`, if reading script from stdin, or a path to a script file*. That leaves two shapes, and choosing between them is the whole of running k6 from the image: 1. **A path the container can resolve.** Host paths are meaningless inside the container, so the file must be mounted first and you pass its in-container location, such as `-v "$PWD/tests:/tests:ro" ... run /tests/smoke.js`. 2. **The literal `-`.** k6's source loader special-cases this one character: it drains standard input, caches those bytes internally under the path `/-`, and runs them as the test script. No filesystem entry is involved at all. Passing both a path and `-`, or neither, trips the same arity check. ## Why the dash is the short path `-` exists precisely because the image cannot see your working copy. Redirecting the file in with `< script.js`, or piping it with `cat script.js |`, carries the bytes across the container boundary through standard input rather than through a mount. That keeps the invocation to a single line and needs no volume, no path translation and no permission arithmetic against UID 12345. The one thing it does require is that the container actually has standard input attached. Started without it, k6 reads zero bytes and fails on an empty script rather than telling you a file was missing - the redirect happened on the host, so from k6's side nothing was ever wrong. ## The two shapes side by side | | `run -` with the file redirected in | mounted directory, in-container path | |---|---|---| | invocation | `docker run --rm -i grafana/k6 run - < smoke.js` | `docker run --rm -v "$PWD:/tests:ro" grafana/k6 run /tests/smoke.js` | | what reaches k6 | one file, as bytes on stdin | everything under the mounted directory | | relative `import` of a local file | not resolvable | resolvable | | files opened at run time | not available | available | | how k6 names the script | the internal path `/-` | the real in-container path | | extra requirement | stdin must be attached | mount must be readable by UID 12345 | ## Why a CI job reaches for the image at all In a containerised CI step the whole invocation is usually one of those two lines and nothing else. The image supplies the binary, so the job needs no install step, no package repository and no version drift between a developer's laptop and the runner - every branch executes the identical k6 build. Adding `-e K6_NO_USAGE_REPORT=true` suppresses k6's usage ping from a machine that has no business making one, which is the one extra thing most CI steps set. ## Where it goes wrong - Repeating `k6` after the image name, or passing the script path with no subcommand at all. - Omitting docker's `-i`, so the container starts with no standard input and k6 runs an empty script. - Handing `run` a host path such as `./tests/smoke.js` with nothing mounted: k6 reports it could not be found on local disk, because from inside the container it genuinely is not there. - Expecting `run -` to also pull in a helper module that sits beside the script on the host. - Assuming the image accepts a `k6` argument, a wrapper script, or a directory of tests.

  • How does k6 refer to a script it read from standard input?
    Its loader caches the bytes under the internal path `/-` and reports the script that way, since there is no real file behind it. That path is also what k6 treats as the script's location when it needs one, which is why a stdin script has no directory of its own.
  • What does `docker run --rm grafana/k6 script.js` do?
    It fails immediately with an unknown-command error. k6's root command only dispatches to subcommands such as `run`, `new` and `inspect`; it has no behaviour of its own, so a bare filename is not a valid first argument and no test is started.
  • Does the image need any k6 installation on the CI runner?
    No. The image contains the `k6` binary at `/usr/bin/k6` and the entrypoint invokes it directly, so the runner needs only a container runtime. That is the main reason CI steps use the image rather than installing k6 from a package repository on every job.

saying these in an interview costs you the question

  • Writes grafana/k6 k6 run, repeating the binary name
  • Thinks docker run grafana/k6 script.js starts a test
  • Treats - as a k6 flag rather than the script argument
  • Omits docker's -i and blames k6 for an empty script
  • Believes the image can read a host path directly
  • Expects the image to accept several script paths at once