A Compose command: is a Kubernetes args:, and command: replaces the entrypoint

Translating a Compose file to a Deployment, command: in Kubernetes silently drops the image ENTRYPOINT. Gotenberg lost its init wrapper and Paperless' exporter lost its environment. Check what the old container actually ran.

Moving a document-management stack from a Docker host into the cluster, I translated each Compose service by hand. Two of them broke for the same reason, and neither complained clearly.

The names don’t line up between the two systems. Compose’s entrypoint: replaces the image’s ENTRYPOINT and its command: replaces the image’s CMD. Kubernetes calls them command: and args:. So a Compose command: becomes args:. If you write it as command: in a pod spec instead, you also throw away the image’s entrypoint, which is usually doing something.

Check what the old container really ran

Don’t trust the Compose file; it only shows what was overridden. Ask Docker:

docker inspect --format '{{json .Config.Entrypoint}} {{json .Config.Cmd}}' <container>

Gotenberg’s image starts through tini --, which reaps child processes. Using command: for its Compose arguments dropped tini and ran the binary directly. args: kept the entrypoint and passed the flags to it.

When you do bypass the entrypoint

Paperless-ngx’s image uses s6-overlay as its entrypoint. Its management commands, such as document_exporter, are scripts that start with #!/command/with-contenv, which reads the container’s environment from a directory that s6 populates at startup. A CronJob that runs the exporter through command: ["sh", "-c", ...] skips /init, so that directory doesn’t exist:

s6-envdir: fatal: unable to envdir /run/s6/container_environment

Setting S6_KEEP_ENV=1 makes with-contenv fall back to the environment the container was started with. I proved it with a throwaway container on the old host, running the same exporter with and without the variable, before putting it in a manifest.

A third one belongs here, though it isn’t about arguments. A shell pipeline under plain sh has no pipefail, so pg_dump | gzip reports success when the dump was cut off halfway and the size check still passes. Dump to a temporary file in custom format, list it back with pg_restore -l, and rename it into place only if that works.