Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Running on a server

For a permanent setup, run the capycal daemon under a service manager. Ready-made service definitions are in contrib/; they expect the binary at /usr/local/bin/capycal. Install steps are in the comment at the top of each file.

Before enabling a service, check the config with capycal --validate -c <config> and one manual capycal --once -v -c <config>.

systemd

System service

contrib/systemd/capycal.service runs Capycal as a dynamic, unprivileged user with a hardened sandbox:

  • Config: /etc/capycal/capycal.toml.
  • Passwords: VAR=value lines in /etc/capycal/capycal.env (mode 0600), referenced as password = { env = "VAR" }.
  • State: /var/lib/capycal, created by systemd (StateDirectory=) and passed to Capycal as CAPYCAL_STATE_DIR, since the dynamic user has no home directory. Don’t set a different state_dir in the config.
sudo install -m 0755 capycal /usr/local/bin/capycal
sudo install -D -m 0644 capycal.toml /etc/capycal/capycal.toml
sudo install -D -m 0600 capycal.env /etc/capycal/capycal.env
sudo cp contrib/systemd/capycal.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now capycal

User service

The quickest way to run Capycal as your user is:

capycal --install-service            # or: capycal -c /path/to/capycal.toml --install-service

It writes ~/.config/systemd/user/capycal.service with the absolute paths of the Capycal binary and the config, reloads systemd, enables and starts the service, and tells you if lingering is off (without it the service stops when you log out; loginctl enable-linger $USER changes that). Run it again after moving the binary or the config. It refuses while another Capycal process runs the config, and it never overwrites a capycal.service it didn’t write. capycal --uninstall-service stops and removes it again. Install the binary to its final place first (for example /usr/local/bin), since the unit points to the binary you run it with.

Passwords go into ~/.config/capycal/capycal.env, files referenced with { file = "..." } or a password manager command.

To set it up by hand instead, contrib/systemd/capycal-user.service is the same unit with the default config and state paths:

mkdir -p ~/.config/systemd/user
cp contrib/systemd/capycal-user.service ~/.config/systemd/user/capycal.service
systemctl --user daemon-reload && systemctl --user enable --now capycal
loginctl enable-linger $USER   # keep it running while you are logged out

Logs and reload

journalctl -u capycal -f            # system service
journalctl --user -u capycal -f     # user service
sudo systemctl reload capycal       # re-read the config (SIGHUP)

Docker

The repository’s Dockerfile builds the CLI from source into a Debian slim image that runs as an unprivileged user. docker-compose.yml mounts the config read-only and keeps the state in a volume:

git clone https://github.com/twiebe/capycal && cd capycal
cp capycal.example.toml capycal.toml
cp .env.example .env
docker compose up -d
  • The image sets CAPYCAL_STATE_DIR=/var/lib/capycal, where the compose file mounts the state volume, so the state survives a recreated container. Leave state_dir unset in capycal.toml, or set it to a path inside that volume.
  • Put the passwords into .env and reference them as password = { env = "..." }. RUST_LOG in .env overrides log_level, also on reload.
  • Time conditions of transforms use the container’s timezone, UTC by default. Add - /etc/localtime:/etc/localtime:ro to the volumes to use the host’s.
docker compose logs -f capycal          # logs
docker compose kill -s HUP capycal      # reload the config
docker compose exec capycal capycal --validate --config /etc/capycal/capycal.toml

The Makefile has shortcuts: make up, make down, make logs, make reload.

launchd (headless macOS)

On a Mac that should sync without the desktop app, run the CLI as a LaunchAgent with contrib/launchd/app.capycal.plist. It runs in your login session, which Calendar.app (EventKit) access requires; a system LaunchDaemon can’t read calendars.

  1. Replace USERNAME in the plist with your user name.

  2. Install and start it:

    cp contrib/launchd/app.capycal.plist ~/Library/LaunchAgents/
    launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/app.capycal.plist
    
  3. Under launchd the calendar permission belongs to the capycal binary itself. Run launchctl kickstart gui/$(id -u)/app.capycal once while logged in and confirm the prompt.

tail -f ~/Library/Logs/capycal.log                          # logs
launchctl kill HUP gui/$(id -u)/app.capycal     # reload the config
launchctl bootout gui/$(id -u)/app.capycal      # stop and remove

Use passwords from files ({ file = "..." }) rather than environment variables, so they don’t end up in the plist.

Don’t run the LaunchAgent and the desktop app with the same config; they share a config lock and the second one does not sync.

Logs

The CLI logs to stdout. With log_format = "json" in [daemon], each line is a JSON object with timestamp, level, message and fields (sync, error, …), ready for Loki, Elasticsearch or jq:

journalctl -u capycal -o cat | jq 'select(.level == "ERROR")'

Useful messages at info level: capycal started, sync started, sync cycle done, deleted synced event, config reloaded successfully, sync cycle failed (error). A block of work starts with <name> started and ends with <name> done (with took, the duration), <name> done with errors (warning), <name> cancelled or <name> failed, for example full sync started and full sync done. Use -v or log_level = "debug" for per-event details.

Reload

A running daemon re-reads its config on SIGHUP (all service managers above forward their reload command as SIGHUP), or by itself with watch_config = true. An invalid new config is rejected and the syncs continue unchanged. See Reloading the config.