JSONArgsRecommended: JSON arguments recommended for CMD to prevent unintended behavior related to OS signals is a build-check warning: the image builds. It flags a CMD written in shell form, such as CMD sh /loop.sh. Write it in exec form, CMD ["sh", "/loop.sh"], and your program, not a wrapping shell, receives the stop signal.

Whether the shell form actually hurts depends on the image and the command. In the tests below it made no difference on Alpine, and on Ubuntu it turned a clean one-second stop into a kill with exit code 137.

The error

 1 warning found (use docker --debug to expand):
 - JSONArgsRecommended: JSON arguments recommended for CMD to prevent unintended behavior related to OS signals (line 3)

The build exited with code 0. The terminal colour codes in the captured lines are left out.

Why it happens

A shell-form CMD runs as /bin/sh -c "...". If that shell stays process 1, docker stop sends SIGTERM to the shell, the shell does not pass it on, and Docker kills the container after its grace period. The test script traps SIGTERM, prints got SIGTERM and exits 0, so the logs show who received the signal.

Measured with docker stop and a stopwatch: on alpine, CMD sh /loop.sh and even CMD echo booting; sh /loop.sh left sh /loop.sh as process 1; the script printed got SIGTERM, exit code 0, about one second. On ubuntu:24.04, CMD echo booting; sh /loop.sh left /bin/sh -c echo booting; sh /loop.sh as process 1; the stop took 3.4 seconds, the exit code was 137 and got SIGTERM never appeared.

The fix

FROM ubuntu:24.04
COPY loop.sh /loop.sh
CMD ["sh", "-c", "echo booting; exec sh /loop.sh"]

With this CMD process 1 was sh /loop.sh, the stop took 1.0 second, the log showed got SIGTERM and the exit code was 0. The exec makes the shell replace itself with the script. For a single command, the plain exec form CMD ["sh", "/loop.sh"] built on Alpine without the warning and stopped the same way.

How it was reproduced

A three-line Dockerfile per case and a loop.sh that traps TERM, built with docker build --progress=plain -t fix8-bld-q2 fix8-bld-q2 and its variants, then docker run -d, docker exec ... cat /proc/1/cmdline and a timed docker stop from PowerShell. Docker Desktop engine 29.9.2, images alpine and ubuntu:24.04. Names are this test's own.

Frequently asked

Is JSONArgsRecommended an error?
No. It is a build-check warning and the image builds. It suggests exec form so that stop signals reach your program.
Why does docker stop take seconds and exit with 137?
Process 1 did not exit on SIGTERM, often because a shell-form CMD left /bin/sh as process 1. Docker then kills the container, which gives exit code 137.
How do I keep a shell in CMD but still get signals?
Use exec form with sh -c and put exec before the last command, for example CMD ["sh", "-c", "echo booting; exec sh /loop.sh"].

More decoded errors in the Fixes category, including the systemctl in a container post about what process 1 is.