# Vanilla upstream Cronicle, packaged as a container image. # # Why this exists: the "official" cronicle/cronicle image is published by the # cronicle-edge project and has not been rebuilt since 2025-02-24 (:latest == # :0.9.74) while upstream Cronicle ships roughly monthly. See README.md. # # Deliberately reproduces the layout of cronicle/cronicle:0.9.74 (recovered via # `docker history`) so this is a drop-in replacement. Deviations are called out # in comments below. ARG ALPINE_VERSION=3.22 FROM alpine:${ALPINE_VERSION} # Cronicle release to build, WITHOUT the leading "v" (e.g. 0.9.126). # CI resolves this from upstream's latest GitHub release. ARG CRONICLE_VERSION # Upstream's exact package list, intentionally unmodified. This image is the # runtime for every Shell Script job, so the package set is effectively the job # environment -- trimming it to save a few MB risks breaking job scripts. # Notably: # procps - Cronicle shells out to a real `ps` for job CPU/memory # monitoring; busybox ps is not sufficient. # tini - reaps zombies left by job child processes (PID 1 duties). # jq - used directly by at least one job script. # util-linux, coreutils, bash - job scripts expect GNU/util-linux behaviour # rather than busybox applets. RUN apk add --no-cache \ acl \ bash \ coreutils \ curl \ git \ jq \ nodejs \ npm \ openssl \ procps \ tar \ tini \ util-linux # Matches upstream's image env. # # CRONICLE_foreground / CRONICLE_echo are pixl-config env overrides (the # CRONICLE_ prefix maps to config keys); together they keep the process in the # foreground and stream the log to stdout, which is what makes `docker logs` # useful. # # /usr/local/bin is on PATH because deployments commonly bind-mount a docker # client there for jobs that drive sibling containers. # # DEVIATION: upstream also sets TZ=America/New_York. Omitted on purpose -- this # image defaults to UTC, and the deployment sets TZ explicitly. ENV CRONICLE_foreground=1 \ CRONICLE_echo=1 \ EDITOR=vi \ PATH=/opt/cronicle/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin RUN addgroup cronicle --gid 1099 \ && adduser -D -h /opt/cronicle -u 1000 -G cronicle cronicle WORKDIR /opt/cronicle RUN test -n "$CRONICLE_VERSION" || { \ echo "ERROR: --build-arg CRONICLE_VERSION= is required" >&2; \ exit 1; \ } # Built from the GitHub release tarball rather than npm: upstream marked the # package "private": true as of v0.9.126, so it is not published to the # registry. -f makes a bad/missing tag fail loudly instead of piping an HTML # error page into tar. # # `busybox tar`, not GNU tar, and deliberately so. Alpine 3.22's GNU tar # restores directory modes via the fchmodat2() syscall. Docker's default seccomp # profile denies syscalls it does not know about by returning EPERM, and # releases before ~25.0 predate fchmodat2 -- so on an older daemon GNU tar # reports "Cannot change mode ...: Operation not permitted" for every directory # and then exits 2, failing the build. The files do all extract, but the exit # code is fatal and the directory modes are left unset. Verified: identical # extraction under `--security-opt seccomp=unconfined` produces zero errors. # Busybox tar uses plain fchmodat(), is unaffected, exits 0, and reproduces the # archive's modes exactly. GNU tar is still installed above for runtime parity. RUN curl -fsSL "https://github.com/jhuckaby/Cronicle/archive/refs/tags/v${CRONICLE_VERSION}.tar.gz" -o /tmp/cronicle.tar.gz \ && busybox tar xzf /tmp/cronicle.tar.gz --strip-components 1 -C /opt/cronicle \ && rm /tmp/cronicle.tar.gz # `npm ci`, not upstream's `npm install`, so the build is reproducible from the # lockfile committed in the release tarball. # # --ignore-scripts skips the root package's "postinstall": "pixl-boot install", # which registers Cronicle as a systemd/init service -- meaningless in a # container. No *dependency* declares an install script (verified against the # lockfile), so nothing else is being skipped, and no build toolchain is needed. # # --omit=dev drops pixl-unit, the only devDependency, which is otherwise dead # weight in the runtime image. bin/build.js needs only async, pixl-tools and # uglify-js, all of which are regular dependencies. RUN npm ci --ignore-scripts --omit=dev # Bundles and minifies the front end into htdocs/js/_combo.js. Required; the UI # 404s on its assets without it. RUN node bin/build dist # Regression gate -- see test/newline-regression.js for the full explanation. # Lives inside /opt/cronicle so node resolves pixl-json-stream from the real # installed tree rather than a copy. COPY test/newline-regression.js /opt/cronicle/newline-regression.js RUN node /opt/cronicle/newline-regression.js \ && rm /opt/cronicle/newline-regression.js # Bind mounts normally land on these; create them with tight modes so a missing # mount does not silently expose job data or secrets. RUN mkdir -p /opt/cronicle/data /opt/cronicle/conf \ && chmod 0700 /opt/cronicle/data /opt/cronicle/conf LABEL org.opencontainers.image.title="Cronicle" \ org.opencontainers.image.description="Vanilla upstream Cronicle, built from the GitHub release tarball" \ org.opencontainers.image.version="${CRONICLE_VERSION}" \ org.opencontainers.image.url="https://github.com/jhuckaby/Cronicle" \ org.opencontainers.image.source="https://git.lerch.org/lobo/cronicle-docker" \ org.opencontainers.image.licenses="MIT" # DEVIATION: upstream also COPYs bin/manager and bin/worker into the image. # Those are cronicle-edge helper entrypoints; vanilla Cronicle is started with # `control.sh start`, so they are omitted. No CMD is set, matching upstream -- # the caller supplies it. ENTRYPOINT ["/sbin/tini", "--"]