@@ -20,7 +20,7 @@ docker compose up --build # from this directory
2020```
2121
2222- Web: open < http://localhost:8080/ > .
23- - Cron: watch the scheduled task fire (about once a minute ) with
23+ - Cron: watch the scheduled task fire (every second ) with
2424
2525 ``` bash
2626 docker compose logs -f cron
@@ -39,11 +39,15 @@ This example adds exactly one such file,
3939
4040``` bash
4141handle_supercronic () {
42- local crontab=/etc/supercronic/crontab
42+ shift # drop the command word; "$@" = operator flags
43+ local crontab=" ${SUPERCRONIC_CRONTAB:-/ etc/ supercronic/ crontab} "
44+ local -a default_opts=() # baked-in defaults (array: space-safe)
4345 run_entrypoint_scripts # reuse a public core-library routine
44- log_notice " starting supercronic cron runner ($crontab )"
45- exec_as_user " $APPLICATION_USER " " $APPLICATION_GROUP " \
46- supercronic " $crontab " # drop root -> app user, then exec
46+ local cron_user=" worker"
47+ setup_user " $cron_user " 1500 " $cron_user " 1500 # provision a dedicated user
48+ log_notice " starting supercronic cron runner ($crontab ) as $cron_user "
49+ exec_as_user " $cron_user " " $cron_user " \
50+ supercronic " ${default_opts[@]} " " $@ " " $crontab " # drop root -> worker, then exec
4751}
4852```
4953
@@ -53,6 +57,16 @@ Starting the container with `command: ["supercronic"]` (see
5357(` run_entrypoint_scripts ` , ` exec_as_user ` , ` log_notice ` ) and ** execs** the cron
5458runner, which then becomes the container's main process.
5559
60+ The hook is written as a reusable template rather than a one-off. Because the
61+ dispatcher invokes ` handle_<cmd> "$@" ` with the full argv, ` $1 ` is always the
62+ command word: ` shift ` it off and ` "$@" ` holds exactly the flags the operator
63+ appended. So ` command: ["supercronic", "-debug", "-split-logs"] ` (or
64+ ` docker run IMG supercronic -debug ` ) forwards those flags straight to the
65+ runner. Defaults live in a ` default_opts ` array (space-safe, and operator flags
66+ that follow can override them), the crontab path is an env-overridable
67+ ` SUPERCRONIC_CRONTAB ` , and the positional crontab stays last where supercronic
68+ expects it — patterns that carry over to any companion command.
69+
5670The contract the hook follows (enforced by the entrypoint):
5771
5872- The file only ** defines** ` handle_* ` functions — no top-level side effects (it
@@ -62,6 +76,25 @@ The contract the hook follows (enforced by the entrypoint):
6276- It does not shadow the core library's function names or the
6377 ` UNIT_* ` / ` ENTRYPOINT_* ` / ` APPLICATION_* ` variables.
6478
79+ ## A dedicated user for the cron role
80+
81+ Rather than reuse the base ` unit ` user, the hook provisions its own ` worker `
82+ user (uid/gid 1500) with the public ` setup_user ` routine and drops to it — so the
83+ cron jobs run under their own least-privilege identity. Two details worth noting:
84+
85+ - ** A new name is required for a custom uid.** ` setup_user ` keeps the existing
86+ id of a user that already exists, and the base ` unit ` user is created by the
87+ core package's postinst with a system-range UID. So passing ` 1500 ` to a
88+ re-provisioned ` unit ` would be ignored (and logged). Using a fresh name
89+ (` worker ` ) is what makes the custom uid take effect.
90+ - ** The uid/gid must be free.** ` setup_user ` dies with an actionable message if
91+ ` 1500 ` is already taken — pick another free id or pre-create the user.
92+
93+ ` worker ` is created with no app directory and is not granted write access to
94+ anything; the demo task only reads ` /www ` (root-owned, world-readable) and prints
95+ its identity. ` cron-task.php ` prints ` uid=…(…) gid=…(…) ` , so the logs show the
96+ jobs running as ` uid=1500(worker) ` , not as ` unit ` .
97+
6598## What it shows
6699
67100- [ ` Dockerfile ` ] ( Dockerfile ) — ` FROM ` the published base, install a pinned +
@@ -72,11 +105,23 @@ The contract the hook follows (enforced by the entrypoint):
72105 uses the default command; ` cron ` overrides it with ` supercronic ` . Both run
73106 under the same hardening (` cap_drop: [ALL] ` , ` cap_add: [SETUID, SETGID] ` ,
74107 ` no-new-privileges ` ) — even the cron role keeps ` SETUID ` /` SETGID ` because it
75- drops to the app user itself via ` setpriv ` (` exec_as_user ` ), just as the Unit
76- master does for its workers.
108+ drops to the ` worker ` user itself via ` setpriv ` (` exec_as_user ` ), just as the
109+ Unit master does for its workers.
77110- The cron role never starts Unit: the hook execs ` supercronic ` before the
78111 entrypoint reaches its first-run/` unitd ` path, so ` config.json ` is simply
79- unused there.
112+ unused there. Because nothing listens on the control socket, the cron service
113+ also ** disables the image ` HEALTHCHECK ` ** (which probes that socket) — otherwise
114+ the container would report unhealthy forever.
115+
116+ ## Verify
117+
118+ ``` bash
119+ docker compose up --build -d
120+ curl -s http://localhost:8080/ | grep ' web role' # web role serves
121+ sleep 3 # the crontab fires every second
122+ docker compose logs cron | grep ' uid=1500(worker)' # cron runs as worker, not unit
123+ docker compose down -v
124+ ```
80125
81126## Why a hook instead of a second image
82127
0 commit comments