{"id":51546094,"url":"https://github.com/devcoons/baikal-extended-docker","last_synced_at":"2026-07-09T18:30:37.574Z","repository":{"id":367613250,"uuid":"1279546621","full_name":"devcoons/baikal-extended-docker","owner":"devcoons","description":null,"archived":false,"fork":false,"pushed_at":"2026-06-26T18:49:57.000Z","size":64,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-26T19:23:22.888Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/devcoons.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-24T19:41:55.000Z","updated_at":"2026-06-26T18:50:02.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/devcoons/baikal-extended-docker","commit_stats":null,"previous_names":["devcoons/baikal-extended-docker"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/devcoons/baikal-extended-docker","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devcoons%2Fbaikal-extended-docker","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devcoons%2Fbaikal-extended-docker/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devcoons%2Fbaikal-extended-docker/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devcoons%2Fbaikal-extended-docker/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/devcoons","download_url":"https://codeload.github.com/devcoons/baikal-extended-docker/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devcoons%2Fbaikal-extended-docker/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35309827,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-09T02:00:07.329Z","response_time":57,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-07-09T18:30:36.842Z","updated_at":"2026-07-09T18:30:37.559Z","avatar_url":"https://github.com/devcoons.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Baïkal Extended (Docker)\n\nA production-ready, multi-architecture Docker image for [Baïkal](https://sabre.io/baikal/)\n— the lightweight CalDAV + CardDAV server — with two things added on top of the\nupstream project:\n\n1. **A self-healing container setup** (healthcheck, auto-restart, reboot persistence).\n2. **An occasion-reminders extension** that turns contact birthdays and\n   anniversaries into calendar reminders, event-driven and per user.\n3. **Automated backups** of the database and config, rotated on a schedule.\n\nUpstream Baïkal is kept untouched as a git submodule in [`src/`](src/) (tracking\n[sabre-io/Baikal](https://github.com/sabre-io/Baikal)); everything this project\nadds lives at the repository root, so Baïkal can be upgraded cleanly.\n\nPublished image: **`docker.io/devcoons/baikal-extended`**\n\n---\n\n## Contents\n\n- [What you get](#what-you-get)\n- [Quick start (published image)](#quick-start-published-image)\n- [Quick start (build it yourself)](#quick-start-build-it-yourself)\n- [First-time setup](#first-time-setup)\n- [Birthday \u0026 anniversary reminders](#birthday--anniversary-reminders)\n- [Backups](#backups)\n- [Behind a reverse proxy (Caddy + HTTPS)](#behind-a-reverse-proxy-caddy--https)\n- [Database backends](#database-backends)\n- [Bringing your own SQLite database](#bringing-your-own-sqlite-database)\n- [High availability](#high-availability)\n- [Configuration reference](#configuration-reference)\n- [Building \u0026 publishing](#building--publishing)\n- [Updating Baïkal](#updating-baïkal)\n- [Project layout](#project-layout)\n- [License](#license)\n\n---\n\n## What you get\n\n- **Baïkal** on Apache + PHP 8.2 (CalDAV, CardDAV, admin UI).\n- **Multi-arch**: builds for `linux/amd64`, `linux/arm64`, `linux/arm/v7`, etc.\n- **Birthday \u0026 anniversary reminders**: for any user with a calendar named\n  `Important Dates`, every contact with a birthday/anniversary gets an all-day\n  `\"\u003cname\u003e's Birthday\"` / `\"\u003cname\u003e's Anniversary\"` reminder at 08:00. When the\n  year is known, the **upcoming** occurrence shows the age (`\"Bob's Birthday\n  (41)\"`) while the recurring series for later years shows just the name.\n  Event-driven (reacts to contact changes within seconds) with a daily cron\n  backstop.\n- **Automated backups**: a daily, rotated snapshot of the database (consistent,\n  zero-downtime for SQLite via `VACUUM INTO`) plus the config.\n- **Self-healing**: container healthcheck, `restart: always`, an `autoheal`\n  sidecar for hung processes, and a systemd unit for reboot persistence.\n- **Backend-agnostic**: works with SQLite (default), MySQL, or PostgreSQL.\n\n---\n\n## Quick start (published image)\n\nThe fastest way to run it — no build required:\n\n```bash\ndocker run -d --name baikal \\\n  -p 8080:80 \\\n  -v baikal-config:/var/www/baikal/config \\\n  -v baikal-specific:/var/www/baikal/Specific \\\n  docker.io/devcoons/baikal-extended:0.11.1\n```\n\nThen open \u003chttp://localhost:8080/admin/install/\u003e and complete the web installer.\n\nOr with Compose:\n\n```yaml\nname: baikal\n\nservices:\n  baikal:\n    image: docker.io/devcoons/baikal-extended:0.11.1\n    container_name: baikal\n    restart: always\n    ports:\n      - \"8080:80\"\n    volumes:\n      - baikal-config:/var/www/baikal/config\n      - baikal-specific:/var/www/baikal/Specific\n    environment:\n      - TZ=Europe/Paris\n\nvolumes:\n  baikal-config:\n  baikal-specific:\n```\n\n```bash\ndocker compose up -d\n```\n\n---\n\n## Quick start (build it yourself)\n\nThe repository ships a full `docker-compose.yml` (with the HA setup and the\nbirthday extension wired in). Initialize the submodule first, then build:\n\n```bash\ngit submodule update --init --recursive\ndocker compose up -d --build\n```\n\nOpen \u003chttp://localhost:8080/admin/install/\u003e and complete the installer.\n\nChange the host port with `BAIKAL_HTTP_PORT` (default `8080`):\n\n```bash\nBAIKAL_HTTP_PORT=9000 docker compose up -d --build\n```\n\n\u003e Using Podman? `podman-compose up -d --build` works too. The image is built\n\u003e and tested with both Docker and Podman.\n\n---\n\n## First-time setup\n\n1. Browse to `/admin/install/` and follow the wizard.\n2. Choose a database backend (SQLite is the default and needs no extra service).\n3. Set the admin password and create your user accounts.\n4. In each client (Thunderbird, iOS, DAVx5, …), point CalDAV/CardDAV at:\n   - `http(s)://\u003chost\u003e/dav.php/`\n   - Auto-discovery via `/.well-known/caldav` and `/.well-known/carddav` is\n     handled by the image.\n\nPersistent data lives in two volumes:\n\n| Path | Contents |\n|------|----------|\n| `/var/www/baikal/config` | `baikal.yaml` and local configuration |\n| `/var/www/baikal/Specific` | SQLite database and instance data |\n\n---\n\n## Birthday \u0026 anniversary reminders\n\nThe headline feature. For **each user that has a calendar named `Important Dates`**,\nthe extension scans that user's contacts and, for every contact with a `BDAY`\nand/or `ANNIVERSARY`, maintains an entry in that calendar:\n\n- All-day events with a reminder that fires at **08:00** local time\n- Title: `\"\u003ccontact name\u003e's Birthday\"` / `\"\u003ccontact name\u003e's Anniversary\"`\n- When the source year is known, you get up to **three objects**: a **last\n  occurrence** event for the most recent past celebration (within the last year)\n  with the age then, a **next occurrence** event carrying the upcoming age/years\n  (`\"Bob's Birthday (41)\"`), and a **yearly series** for all later years titled\n  with the name only. So a birthday that just passed stays visible with the age\n  reached, the upcoming one shows the new age, and future years show just the\n  name; the daily run rolls everything forward automatically as dates pass.\n\nTitles are configurable via templates (`{name}` plus `{age}`/`{years}`), e.g. set\n`BAIKAL_EXT_BIRTHDAY_TITLE_TEMPLATE=\"{name} turns {age}\"` for `\"Bob turns 41\"`.\nAnniversaries can be turned off with `BAIKAL_EXT_ANNIVERSARY_ENABLED=false`.\n\nIt is strictly per user (a user's contacts only ever write to that same user's\ncalendar) and only ever touches events it created itself — your own calendar\nentries are never modified. Birthdays and anniversaries are tracked independently.\n\n### How it's triggered\n\n- **On startup:** once the database is ready (works for SQLite and MySQL/\n  PostgreSQL), the container runs one full sync. This seeds events on first boot\n  and **migrates events created by older versions** to the current format. It\n  waits in the background, so Apache is never delayed. Controlled by\n  `BAIKAL_EXT_BIRTHDAY_RUN_ON_START` (default `true`) and bounded by\n  `BAIKAL_EXT_BIRTHDAY_START_TIMEOUT` (default 300s).\n- **Event-driven (no polling):** a small plugin inside Baïkal's DAV server reacts\n  to contact create/update/delete and triggers a sync for just that user within\n  seconds.\n- **Daily cron backstop** (`30 0 * * *` by default): catches the yearly date\n  rollover and anything missed while the watcher was down.\n- **Manual**, anytime:\n\n```bash\ndocker compose exec baikal baikal-birthdays run            # sync everyone\ndocker compose exec baikal baikal-birthdays run --dry-run -v   # preview only\ndocker compose exec baikal baikal-birthdays run --user=alice   # one user\ndocker compose exec baikal baikal-birthdays --help\n```\n\n(If you ran the container with `docker run --name baikal`, use\n`docker exec baikal baikal-birthdays run`.)\n\n### What happens when a contact changes\n\n| Action | Result on next sync |\n|--------|---------------------|\n| Contact with a birthday/anniversary added | Event **created** |\n| Contact's date/name changed | Event **updated** |\n| Contact deleted, or date removed | Event **deleted** |\n| Nothing relevant changed | Left **unchanged** |\n\nBy default a user **without** an `Important Dates` calendar is skipped. Set\n`BAIKAL_EXT_BIRTHDAY_CREATE_CALENDAR=true` to have it created automatically.\n\n### What gets created (example)\n\nFor a contact `Bob Stone` with `BDAY = 1985-08-20`, synced on 2026-06-26:\n\n| Object | Date | Recurs? | Title |\n|--------|------|---------|-------|\n| last occurrence | 2025-08-20 | no | `Bob Stone's Birthday (40)` |\n| next occurrence | 2026-08-20 | no | `Bob Stone's Birthday (41)` |\n| series | 2027-08-20 | yearly | `Bob Stone's Birthday` |\n\nSo the most recent and next birthdays show the age; 2027, 2028, … show just the\nname. After 2026-08-20 passes, the daily run advances \"last\" to 2026 (age 41),\n\"next\" to 2027 (age 42), and the series to start 2028 — always at most one event\nper year, never duplicated. This is why a birthday that happened *last month*\nstays on the calendar instead of disappearing the next day.\n\n### Edge cases handled\n\n- **Unknown year** (`BDAY = --08-20`): a single name-only yearly event (no age,\n  no split — there is nothing to count). This already covers past years.\n- **Leap day** (`Feb 29`): the \"last\"/\"next\" events land on real Feb 29 dates\n  (with the correct age) and the series recurs on leap years only.\n- **Date already passed this year**: \"last\" is this year's occurrence (kept), and\n  \"next\" jumps to next year.\n- **Born this year** (no past occurrence yet): no \"last\" event is created.\n- **No name** (a contact with a date but no `FN`/`N`): skipped.\n- **Age display off** (`BAIKAL_EXT_BIRTHDAY_SHOW_AGE=false`): a single name-only\n  yearly event anchored at the source year, so it still covers past years.\n- **Only the extension's own events are touched** — matched by URI/UID prefix and\n  an `X-BAIKAL-EXT-SIG` signature; your own calendar entries are never modified.\n\nThe extension reuses Baïkal's own bundled `sabre/dav` and `sabre/vobject`\nlibraries (rather than touching the database schema directly), so it stays\ncompatible with newer Baïkal versions and works across all three backends. See\n[`src_ext/README.md`](src_ext/README.md) for the design.\n\n---\n\n## Backups\n\nA daily, rotated backup of your data is created automatically. Each run writes a\ntimestamped `tar.gz` containing a **consistent database snapshot** plus a copy of\nthe config directory:\n\n- **SQLite** uses `VACUUM INTO`, so the snapshot is consistent even while Baïkal\n  is serving requests — no downtime, no locking.\n- **MySQL / PostgreSQL** use `mysqldump` / `pg_dump` when those clients are\n  present in the image (otherwise the run logs a clear note; dump externally).\n\nArchives default to `Specific/backups/` (inside the persisted volume), keeping\nthe newest `7`. Trigger one anytime:\n\n```bash\ndocker compose exec baikal baikal-backup\n```\n\nTo restore: extract an archive, put `config/baikal.yaml` back, and restore the\ndatabase (`db.sqlite` to the SQLite path, or `dump.sql` via `mysql`/`psql`).\n\nTunables: `BAIKAL_EXT_BACKUP_ENABLED`, `BAIKAL_EXT_BACKUP_CRON`, `BAIKAL_EXT_BACKUP_DIR`,\n`BAIKAL_EXT_BACKUP_KEEP`, `BAIKAL_EXT_BACKUP_RUN_ON_START` (see the\n[configuration reference](#configuration-reference)).\n\n\u003e The in-server hook is added to Baïkal via a patch applied at build time\n\u003e (`src_ext/patches/`), so the `src/` submodule stays pristine and the change\n\u003e survives upgrades. If a future Baïkal version moves the hook point, the build\n\u003e fails loudly rather than silently dropping the feature.\n\n---\n\n## Behind a reverse proxy (Caddy + HTTPS)\n\nThe container serves plain HTTP on port **80**. Put a TLS-terminating proxy in\nfront of it. Baïkal honors `X-Forwarded-Proto`, so HTTPS URLs are generated\ncorrectly.\n\n`docker-compose.yml`:\n\n```yaml\nname: baikal\n\nservices:\n  baikal:\n    image: docker.io/devcoons/baikal-extended:0.11.1\n    container_name: baikal\n    restart: always\n    expose:\n      - \"80\"\n    volumes:\n      - ./baikal/config:/var/www/baikal/config\n      - ./baikal/Specific:/var/www/baikal/Specific\n    environment:\n      - TZ=Europe/Paris\n\n  caddy:\n    image: caddy:2\n    container_name: caddy\n    restart: always\n    ports:\n      - \"11000:443\"\n    volumes:\n      - ./Caddyfile:/etc/caddy/Caddyfile:ro\n      - ./caddy_data:/data\n      - ./caddy_config:/config\n    depends_on:\n      - baikal\n\nvolumes:\n  caddy_data:\n  caddy_config:\n```\n\nMinimal `Caddyfile`:\n\n```caddyfile\nyour.domain.com {\n    reverse_proxy baikal:80\n}\n```\n\n\u003e Set `TZ` (container clock) and Baïkal's own `system.timezone` (in the installer)\n\u003e to the same zone, so the 08:00 birthday reminders fire at the right local time.\n\n---\n\n## Database backends\n\nSQLite is the default and requires nothing extra. To run with **MySQL**, the\nbundled compose has a `mysql` profile:\n\n```bash\ndocker compose --profile mysql up -d --build\n```\n\nDuring installation choose MySQL and use:\n\n| Setting  | Value |\n|----------|-------|\n| Host     | `mysql` |\n| Database | `baikal` (or `MYSQL_DATABASE`) |\n| User     | `baikal` (or `MYSQL_USER`) |\n| Password | `baikal` (or `MYSQL_PASSWORD`) |\n\nOverride defaults via a `.env` file:\n\n```env\nBAIKAL_HTTP_PORT=8080\nMYSQL_ROOT_PASSWORD=change-me\nMYSQL_DATABASE=baikal\nMYSQL_USER=baikal\nMYSQL_PASSWORD=change-me\n```\n\nPostgreSQL is also supported by Baïkal (and the extension); configure it in the\ninstaller.\n\n---\n\n## Bringing your own SQLite database\n\nAn existing **Baïkal** SQLite database works as-is, because this image uses the\nsame schema. To migrate:\n\n1. Place the file where Baïkal expects it (default\n   `/var/www/baikal/Specific/db/db.sqlite`):\n\n```bash\nmkdir -p ./baikal/Specific/db\ncp /path/to/your.sqlite ./baikal/Specific/db/db.sqlite\n```\n\n2. Provide a **matching `baikal.yaml`** in `./baikal/config/` — ideally your\n   original one. The `auth_realm` must match (default `BaikalDAV`), since user\n   passwords are stored as `md5(user:realm:pass)`; a mismatched realm breaks\n   client logins.\n3. If the DB came from an **older** Baïkal, the first request redirects to\n   `/admin/install/` to run the schema upgrade — log in and confirm. Your data\n   is preserved.\n\nQuick check that it's a Baïkal database:\n\n```bash\nsqlite3 your.sqlite \".tables\"\n# expect: addressbooks calendars calendarinstances calendarobjects cards principals users ...\n```\n\n---\n\n## High availability\n\nThe bundled stack keeps Baïkal running through the common failure modes:\n\n| Failure mode | What recovers it |\n|--------------|------------------|\n| Process crash / non-zero exit | `restart: always` relaunches the container |\n| Alive but unresponsive (hung Apache/PHP) | `HEALTHCHECK` marks it `unhealthy`; the `autoheal` sidecar restarts it |\n| Host / daemon reboot | Docker daemon relaunches `restart: always` containers; for Podman use the systemd unit |\n\n### How it works\n\n- **Healthcheck** — `curl http://127.0.0.1/` runs every 30s. The root path\n  redirects through PHP, so a passing probe proves Apache *and* PHP are serving,\n  not just answering TCP.\n- **`restart: always`** — applies on crash and (for Docker) on boot. A manual\n  `stop` is respected and not auto-restarted.\n- **`autoheal`** — the runtime's restart policy only reacts to a process that\n  *exits*; a hung process stays \"up\". The\n  [`willfarrell/autoheal`](https://github.com/willfarrell/autoheal) sidecar\n  watches containers labelled `autoheal=true` and restarts any that go\n  `unhealthy`.\n\n### Verifying recovery\n\n```bash\ndocker compose kill -s SIGKILL baikal   # simulate a crash\ndocker compose ps                       # baikal returns to \"Up (healthy)\"\n```\n\n\u003e `docker stop` / `podman stop` are intentional and are **not** auto-restarted —\n\u003e only crashes and unhealthy states are.\n\n### autoheal on Podman\n\n`autoheal` talks to the container socket. Docker exposes it at\n`/var/run/docker.sock` (the default). For rootless Podman:\n\n```bash\nsystemctl --user enable --now podman.socket\nDOCKER_SOCK=/run/user/$(id -u)/podman/podman.sock podman-compose up -d\n```\n\n### Surviving a reboot\n\n- **Docker**: ensure the daemon starts at boot\n  (`sudo systemctl enable --now docker`); `restart: always` does the rest.\n- **Podman** (daemonless): use one of the provided systemd units, which also\n  restart on failure and after reboot.\n  - `systemd/baikal.container` — a **Quadlet** unit (recommended): native crash\n    recovery + `HealthOnFailure=restart` + boot persistence.\n  - `systemd/baikal.service` — supervises the whole compose stack.\n\n```bash\n# rootless Quadlet example\nloginctl enable-linger \"$USER\"\nmkdir -p ~/.config/containers/systemd\ncp systemd/baikal.container ~/.config/containers/systemd/baikal.container\nsystemctl --user daemon-reload\nsystemctl --user start baikal\n```\n\nSee the headers of `systemd/baikal.container` and `systemd/baikal.service` for\nrootful and Docker variants.\n\n\u003e Note: the image's built-in `HEALTHCHECK` is honored by Docker. When the image\n\u003e is built with Podman's default OCI format the directive is ignored at the image\n\u003e level, but the **compose** and **Quadlet** healthchecks cover Podman.\n\n---\n\n## Configuration reference\n\n### Web server\n\n| Variable | Default | Description |\n|----------|---------|-------------|\n| `BAIKAL_HTTP_PORT` | `8080` | Host port (compose only) |\n| `BAIKAL_SERVERNAME` | _(unset)_ | Apache `ServerName` (e.g. `dav.example.com`) |\n| `BAIKAL_SERVERALIAS` | _(unset)_ | Apache `ServerAlias` (space-separated) |\n| `BAIKAL_SKIP_CHOWN` | _(unset)_ | If set, skip `chown` of `config/` \u0026 `Specific/` at startup |\n| `TZ` | _(image default)_ | Container timezone |\n\n### Birthday \u0026 anniversary extension\n\n| Variable | Default | Purpose |\n|----------|---------|---------|\n| `BAIKAL_EXT_BIRTHDAY_WATCH` | `true` | Event-driven watcher (reacts to contact changes) |\n| `BAIKAL_EXT_BIRTHDAY_WATCH_DEBOUNCE` | `3` | Seconds to coalesce a burst of changes |\n| `BAIKAL_EXT_BIRTHDAY_ENABLED` | `true` | Periodic cron backstop |\n| `BAIKAL_EXT_BIRTHDAY_CRON` | `30 0 * * *` | Cron schedule |\n| `BAIKAL_EXT_BIRTHDAY_CALENDAR` | `Important Dates` | Destination calendar name |\n| `BAIKAL_EXT_BIRTHDAY_ALARM_TIME` | `08:00` | Reminder time (`HH:MM`, local) |\n| `BAIKAL_EXT_BIRTHDAY_ADDRESSBOOK` | _(all)_ | Restrict the scan to one address book |\n| `BAIKAL_EXT_BIRTHDAY_CREATE_CALENDAR` | `false` | Auto-create the calendar if missing |\n| `BAIKAL_EXT_BIRTHDAY_RUN_ON_START` | `true` | Run one full sync on startup once the DB is ready (also migrates old events) |\n| `BAIKAL_EXT_BIRTHDAY_START_TIMEOUT` | `300` | Max seconds to wait for the DB on startup |\n| `BAIKAL_EXT_BIRTHDAY_TITLE_TEMPLATE` | `{name}'s Birthday` | Birthday title (`{name}`, `{age}`) |\n| `BAIKAL_EXT_BIRTHDAY_SHOW_AGE` | `true` | Append `(age)` when the birth year is known |\n| `BAIKAL_EXT_ANNIVERSARY_ENABLED` | `true` | Also sync `ANNIVERSARY` / `X-ANNIVERSARY` |\n| `BAIKAL_EXT_ANNIVERSARY_TITLE_TEMPLATE` | `{name}'s Anniversary` | Anniversary title (`{name}`, `{years}`) |\n| `BAIKAL_EXT_ANNIVERSARY_SHOW_YEARS` | `true` | Append `(years)` when the year is known |\n\n### Backups\n\n| Variable | Default | Purpose |\n|----------|---------|---------|\n| `BAIKAL_EXT_BACKUP_ENABLED` | `true` | Install the backup cron job |\n| `BAIKAL_EXT_BACKUP_CRON` | `0 3 * * *` | Cron schedule for backups |\n| `BAIKAL_EXT_BACKUP_DIR` | `\u003cSpecific\u003e/backups` | Where archives are written |\n| `BAIKAL_EXT_BACKUP_KEEP` | `7` | How many archives to retain |\n| `BAIKAL_EXT_BACKUP_RUN_ON_START` | `false` | Also back up shortly after start |\n\n---\n\n## Building \u0026 publishing\n\nBuild a local image:\n\n```bash\ndocker build -t baikal:local .\n```\n\n### Multi-architecture build \u0026 push\n\nOfficial `php`/`composer` base images are multi-arch. Use Buildx:\n\n```bash\ndocker buildx create --name baikal-builder --use 2\u003e/dev/null || docker buildx use baikal-builder\ndocker buildx build \\\n  --platform linux/amd64,linux/arm64,linux/arm/v7 \\\n  -t docker.io/devcoons/baikal-extended:0.11.1 \\\n  -t docker.io/devcoons/baikal-extended:latest \\\n  --push \\\n  .\n```\n\nLoad a single-platform image into the local daemon instead of pushing:\n\n```bash\ndocker buildx build --platform linux/amd64 -t baikal:local --load .\n```\n\n### Helper script\n\n`scripts/build-push.sh` builds and tags with both the Baïkal version and\n`latest`:\n\n```bash\n./scripts/build-push.sh            # build only (auto-detects version tag)\ndocker login docker.io            # or: podman login docker.io\n./scripts/build-push.sh --push     # build and push\n./scripts/build-push.sh --push 1.0.0   # custom tag\n```\n\n---\n\n## Updating Baïkal\n\nBump the submodule to a new upstream release and rebuild:\n\n```bash\ncd src\ngit fetch origin\ngit checkout \u003ctag-or-commit\u003e\ncd ..\ndocker compose build --no-cache\ndocker compose up -d\n```\n\nIf the birthday hook patch no longer applies to the new Baïkal sources, the build\nfails at the patch step — update `src_ext/patches/` to match.\n\n---\n\n## Project layout\n\n```\n.\n├── Dockerfile                      # multi-stage build from src/ + extension + patch\n├── docker-compose.yml              # full stack: baikal + autoheal (+ optional mysql)\n├── docker/\n│   ├── apache.conf\n│   ├── docker-entrypoint.sh\n│   └── docker-entrypoint.d/        # startup hooks (db dir, perms, cron, watcher, backups)\n├── src_ext/                        # occasion-reminders + backup extension (NOT in submodule)\n│   ├── bootstrap.php               # PSR-4 autoloader for BaikalExt\\\n│   ├── bin/                        # CLI (baikal-birthdays, baikal-backup) + inotify watcher\n│   ├── lib/                        # Config, BirthdayService, sabre plugin, ...\n│   ├── patches/                    # build-time patch adding the Baïkal hook\n│   └── README.md                   # extension design \u0026 details\n├── systemd/\n│   ├── baikal.container            # Podman Quadlet unit (recommended for Podman)\n│   └── baikal.service              # compose-supervisor unit\n├── scripts/\n│   └── build-push.sh               # build + tag + push helper\n└── src/                            # git submodule → sabre-io/Baikal (pristine)\n```\n\n---\n\n## License\n\nBaïkal is licensed under GPL-3.0 (see [`src/LICENSE`](src/LICENSE)). The packaging\nand extension code in this repository are provided under the same terms.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdevcoons%2Fbaikal-extended-docker","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdevcoons%2Fbaikal-extended-docker","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdevcoons%2Fbaikal-extended-docker/lists"}