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=valuelines in/etc/capycal/capycal.env(mode0600), referenced aspassword = { env = "VAR" }. - State:
/var/lib/capycal, created by systemd (StateDirectory=) and passed to Capycal asCAPYCAL_STATE_DIR, since the dynamic user has no home directory. Don’t set a differentstate_dirin 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. Leavestate_dirunset incapycal.toml, or set it to a path inside that volume. - Put the passwords into
.envand reference them aspassword = { env = "..." }.RUST_LOGin.envoverrideslog_level, also on reload. - Time conditions of transforms use the container’s timezone, UTC by default. Add
- /etc/localtime:/etc/localtime:roto 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.
-
Replace
USERNAMEin the plist with your user name. -
Install and start it:
cp contrib/launchd/app.capycal.plist ~/Library/LaunchAgents/ launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/app.capycal.plist -
Under launchd the calendar permission belongs to the
capycalbinary itself. Runlaunchctl kickstart gui/$(id -u)/app.capycalonce 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.