In a container

beta

Run the catalogue pull from the published image, keep the profile on a volume, and bake the binary into another image for a scheduled pull.

In a container

Every release publishes the same binary as an image, to two registries:

ghcr.io/pksorensen/pks-agent-woocommerce:<version>          # and :latest
registry.agentics.dk/agentics/pks-agent-woocommerce:<version>

The GHCR one is public — no login to pull it.

Pull a catalogue without installing anything

The credentials come from the environment, which is the only shape that works unattended — init prompts, and a scheduled task has no terminal:

docker run --rm \
  -e PKS_WOO_SITE=https://shop.example.com \
  -e PKS_WOO_KEY -e PKS_WOO_SECRET \
  -v "$PWD/woo:/work/woo" \
  ghcr.io/pksorensen/pks-agent-woocommerce:latest product sync --with-variations

-e PKS_WOO_KEY with no =value passes the variable through from your own shell, so the secret is not in the command you typed and not in your shell history. Nothing is written to disk: with these set, the CLI never creates its config directory, so the container leaves no credential behind.

Set all three of PKS_WOO_SITE, PKS_WOO_KEY and PKS_WOO_SECRET, or none. A partial set is refused rather than falling back to a stored profile, because a typo in one of them would otherwise pull a different shop's catalogue and look like it worked. PKS_WOO_APP_PASSWORD (user:application password) is optional; so is PKS_WOO_REST_MODE (pretty | route), which skips one probe per run.

/work is the working directory, so --dir-relative output (./woo by default) lands on your disk instead of inside a container that is about to be removed. Files come out owned by root unless you add --user "$(id -u):$(id -g)".

Or connect once and keep a profile

For repeated work from a laptop, init stores the store URL and credential on a volume:

docker run --rm -it -v woo-profile:/data -v "$PWD/woo:/work/woo" \
  ghcr.io/pksorensen/pks-agent-woocommerce:latest init

docker run --rm -v woo-profile:/data -v "$PWD/woo:/work/woo" \
  ghcr.io/pksorensen/pks-agent-woocommerce:latest product sync --with-variations

/data is the profile store (PKS_WOO_HOME); without a volume there, every command starts from no connected store. init needs -it: it prompts, and it refuses a pipe. Every command after it is non-interactive.

There is no keyring in a container and no Secret Service to talk to, so the image sets PKS_WOO_NO_KEYRING=1 and the credential is stored as a 0600 file inside a 0700 directory on /data. That is a decision, not a fallback: whatever can read that volume can read the key. Treat the volume as the secret — and prefer the environment for anything that runs on a schedule, where the volume would outlive the run and nothing would rotate it.

Bake the binary into your own image

The binary is static, so it can be copied out of this image and run on any Linux base — Debian slim, Ubuntu, distroless with a shell. A downstream image that wants a catalogue pull of its own does not need Go, and does not need this image at runtime:

FROM ghcr.io/pksorensen/pks-agent-woocommerce:1.0.0 AS woo-cli

FROM node:22-bookworm-slim
COPY --from=woo-cli /usr/local/bin/pks-agent-woocommerce-cli /usr/local/bin/

Pin the version. :latest in a FROM makes an image that builds differently on different days, which is the one property a build should not have.

Every release smoke-tests exactly this path — the binary is copied out of the published image and run on Debian slim — so a release that would break your build fails ours first.

A pull on a schedule

A scheduled pull is one command, with the three variables injected by whatever runs it — Coolify's scheduled tasks, a Kubernetes CronJob's secretKeyRef, a systemd unit's EnvironmentFile. Write into a staging directory and move it into place when the pull is finished, so whatever reads the output never sees half a catalogue:

#!/bin/sh
set -eu
# PKS_WOO_SITE / PKS_WOO_KEY / PKS_WOO_SECRET come from the scheduler's
# environment. Never echo them, and never write them to the output directory.
stamp=$(date -u +%Y%m%d-%H%M)
out=/work/out                          # a mount of its own — output, not credentials
staging="$out/$stamp.part"

for lang in da en de fr; do
  pks-agent-woocommerce-cli product sync \
    --dir "$staging/$lang" --lang "$lang" --with-variations
done

mv "$staging" "$out/$stamp"            # atomic: the directory is complete or absent

Three things worth deciding before the first scheduled run:

  1. When it hits the shop. A full pull is hundreds of authenticated requests against a production site. Pick a quiet hour and agree it with whoever owns the shop.
  2. Which language carries the prices. With WooCommerce Multilingual the per-currency prices sit on the original post, so a drop that leaves that language out can carry every product and no price. Pull it every time, even when only a translation changed.
  3. Who reads the result in the morning. A pull that fails quietly is worse than one that fails loudly; the exit code is the signal, and something should be watching it.