With yesterday's cron infrastructure fix in place, both daily jobs got
to actually execute today and revealed bugs that had been hiding behind
the silent failures.
1. Wiki export crashed on docs whose body starts with `---` (markdown
horizontal rule). matter.stringify(str, data) re-parses str as if
it might already contain frontmatter, so a leading `---` makes
gray-matter try to YAML-parse the body and choke on the first
`Title: subtitle` colon. Pass {content: str} instead — the parser
only runs on bare strings, so an object skips the re-parse path.
2. outline-backup.sh referenced docker container names `outline-postgres`
and `outline`, but DokPloy names containers `${project}-${service}-1`,
so the backup got `Error response from daemon: No such container`.
Derive names from $APP_NAME (set to the compose project name) with
POSTGRES_CONTAINER / OUTLINE_CONTAINER overrides for portability.
The cron has been silently failing every day since 2026-03-28. Four
independent bugs were stacked:
1. cron/entrypoint.sh: env dump used `sed` to wrap each line in
`export `, but values with spaces (e.g. GIT_SSH_COMMAND, OIDC_SCOPES)
produced lines like `export GIT_SSH_COMMAND=ssh -o UserKnownHosts...`
which `export` parses as a flag and aborts. busybox ash treats the
builtin error as fatal, so `. /etc/environment.sh; script.sh` never
reaches the script. Now single-quote each value with proper escaping.
2. cron/Dockerfile: NODE_PATH only works for CommonJS `require()`, not
ESM `import`. The export script is `"type": "module"` and failed with
"Cannot find package 'gray-matter'". Install deps at /app/node_modules
instead — Node ESM walks up from /app/scripts and finds it there.
3. docker-compose.yml: `~/.ssh:/root/.ssh:ro` — DokPloy does NOT expand
`~`, so it created a literal `~` directory inside the deployment dir
and mounted that empty dir. The container had no SSH key. Use the
absolute host path `/root/.ssh` instead.
4. cron/entrypoint.sh: even with the SSH key, `git push` would fail
because the git remote is HTTPS and the host's git server runs on
port 2222 (set in /root/.ssh/config). Add a `pushInsteadOf` rewrite
so push uses SSH while DokPloy can keep fetching via HTTPS, and stop
re-running ssh-keyscan against the wrong port — copy the host's
known_hosts (which already has the :2222 entry) instead.
- nginx: deny all requests to hidden files (/.git/config was publicly readable)
- nginx: remove CSS injection and /custom/ static file serving
- cron: install script deps at build time into /opt to avoid ro mount conflict
- docker-compose: widen cron build context for package.json COPY
- Delete unused theme/ghost-guild.css
Alpine crond doesn't inherit the container environment, so
OUTLINE_API_TOKEN and other vars were missing. Dump env at
startup and source it in each cron entry.
- Add cron service to docker-compose with backup (3 AM) and export (4 AM) schedules
- Remove redundant content/articles/ and content/curriculum/ (now in Outline, exported to content/wiki/)
- Fix env var mismatch: support both OUTLINE_API_KEY and OUTLINE_API_TOKEN
- Drop updatedAt from export frontmatter to reduce noisy commits
- Add backups/ to gitignore
9 session pages and 10 PS Guide markdown files for the Baby Ghosts
cooperative foundations curriculum. Import script creates documents in
Outline wiki with cross-links between paired session/PS Guide pages.
Export script pulls all Outline documents via API and writes them as
flat markdown files to content/wiki/ with frontmatter metadata.
Cron wrapper auto-commits changes daily.
Traefik was routing directly to Outline, so the nginx.conf
was unused. Add nginx as an intermediary service to enable
sub_filter injection of OG tags on the homepage and custom
CSS on all pages.
Strip the Nuxt 4 static site and replace with Docker Compose config
for self-hosted Outline wiki (Outline + PostgreSQL 16 + Redis 7).
Adds nginx reverse proxy with WebSocket support and CSS injection,
migration script for existing markdown articles, backup script,
and starter theme CSS.