It all started from this article about setting up nginx and certbot in docker. In the end of the manual the author made the automatic certificate renewal for nginx with this command:
command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"
I'm not the only one who didn't understand this part, so there was a question on SO: Why do sleep & wait in bash?
The answer was given that the original command was not perfect and here is the corrected version:
/bin/sh -c 'nginx -g \"daemon off;\" & trap exit TERM; while :; do sleep 6h & wait $${!}; nginx -s reload; done'
But in this command I see nginx -g \"daemon off;\" &
Why do we first put nginx on foreground and then stuff it in background? What are implications and why not just launch nginx in background at first?
Another question: as I understand, the while
cycle stays in foreground for docker, unlike the original command. But if nginx if background, does it mean that if it dies, docker does not care? In foreground while
is still working, no problem.
And the last question: why in this commands sometimes we see $${!}
and sometimes ${!}
. Example of ${!}
from the same SO question:
docker run --name test --rm --entrypoint="/bin/sh" nginx -c 'nginx -g "daemon off;" & trap exit TERM; while :; do sleep 20 & wait ${!}; echo running; done'
I know it's a character escaping, but I don't figure out the rules for this case.
But in this command I see nginx -g \"daemon off;\" & Why do we first put nginx on foreground and then stuff it in background? What are implications and why not just launch nginx in background at first?
The reason was mainly to highlight the differences and there are no implications. The command is equivalent to:
"/bin/sh -c 'nginx; trap exit TERM; while :; do sleep 6h & wait $${!}; nginx -s reload; done'
Another question: as I understand, the while cycle stays in foreground for docker, unlike the original command. But if nginx if background, does it mean that if it dies, docker does not care? In foreground while is still working, no problem.
The command basically creates three processes: the shell process (/bin/sh
), sleep 6H
and the nginx
server. A fourth process (nginx -s reload
) is forked every 6 hours.
Docker always monitors the process with PID 1
which in this case is the shell (/bin/sh
). If the shell dies the container exits. If the nginx
server, which is a child of the shell process, dies docker, indeed doesn't care.
The "corrected" version doesn't address these issues. It has the same problems as the original one. The answer to the SO question only highlights that the sleep
and wait
is not needed unless you want to handle signals in a timely manner. It means that:
"/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx ..."
does exactly the same thing as:
"/bin/sh -c 'while :; do sleep 6h; nginx ..."
In conclusion, a proper implementation would have nginx
as the main process (PID 1
) and another process running in background waking up every 6h to signal the server to reload the configuration. Neither the original, nor the corrected command implement all this properly.
To fix the before mentioned problems the command should be like this:
'while :; do sleep 6h; nginx -s reload; done & exec nginx -g "daemon off;"'
The exec
system call replaces the content of the shell process with the nginx
server making nginx
the main process in foreground.
All the signals are now propagated correctly to the server (see also Controlling nginx).
Note: This solution still has a flaw. The shell process (the while loop) is not monitored. If for any reason this process exits the only thing docker does
is to send an alert.
Hope this sheds some light.
Answer to my last question regarding ${!}
and $${!}
:
Apparently, if we write a command in docker-compose file with one dollar sign (${!}
), it will be expanded by docker-compose into 'pid of last background command relative to shell that launched docker-compose'. So, the entrypoint in container will look like this:
/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait someUnknownLastPID; done;'"
With $${!}
the dollar sign is escaped in docker-compose processing. The entrypoint in container will be something like
/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait ${!}; done;'"
Source: https://stackoverflow.com/a/40621373/11931043