Lichen Ansible Deployment#
Deploy or update a lichen server with Ansible. Wraps the docker-compose/
reference — Ansible handles Docker install, file sync, and lifecycle; the
actual services (lichen + caddy) still run from the same compose stack.
Prerequisites#
- Ansible 2.12+ on your workstation
- A target server with SSH access, sudo, and ports 80/443 open
- A domain with an A record pointing at the server
Setup#
-
Copy the inventory example and edit it:
cp inventory.example.yml inventory.yml # set ansible_host, ansible_user, lichen_domain -
Supply the admin password. Easiest: via
--extra-varsat runtime. For anything non-trivial, useansible-vault:ansible-vault create group_vars/all.yml # add: lichen_admin_password: "..."
Deploy a fresh server#
ansible-playbook -i inventory.yml deploy.yml \
--extra-vars "lichen_admin_password=CHANGEME"
This will:
- Install Docker if missing (via
get.docker.com) - Copy the
docker-compose/files to the server (/srv/lichenby default) - Render
.envfrom your inventory vars docker compose pull+docker compose up -d
Caddy obtains a TLS cert from Let's Encrypt on the first request to your
domain. Wait ~30s after DNS is live, then visit https://<lichen_domain>.
Update to latest#
ansible-playbook -i inventory.yml update.yml
Pulls the latest notplants/lichen-full image and restarts the app service.
Caddy is left alone. Optionally copies a custom binary if one exists at
../docker-compose/bin/lichen-server on your workstation — see
/lichen-mod for building custom binaries.
Variables#
| Variable | Default | Purpose |
|---|---|---|
lichen_domain |
— (required) | dashboard + Caddy TLS |
lichen_admin_user |
admin |
admin login |
lichen_admin_password |
— (required on first deploy) | admin login |
lichen_auth_providers |
file,atproto |
enabled auth backends |
lichen_deploy_dir |
/srv/lichen |
target directory on the server |
lichen_rust_log |
info |
RUST_LOG for the container |
Deploy a custom binary#
override.yml uploads a locally-built lichen-server binary to the server's
bin/ directory and restarts the app container. The compose stack's
entrypoint prefers /opt/lichen-bin/lichen-server over the image-bundled
binary when present, so no other changes are needed.
Build#
Build against x86_64-unknown-linux-musl so the binary runs in the Alpine
image. From the lichen source tree:
cargo build --release --target x86_64-unknown-linux-musl \
--bin lichen-server --features "atproto git"
(see the /lichen-mod Claude skill for details).
Deploy#
ansible-playbook -i inventory.yml override.yml \
--extra-vars "lichen_binary_src=../../../target/x86_64-unknown-linux-musl/release/lichen-server"
The path is relative to the ansible/ directory. Absolute paths also work.
Revert#
ansible-playbook -i inventory.yml override.yml \
--extra-vars "lichen_override_revert=true"
Removes the override file and restarts so the image's built-in binary takes over again.
Backups (optional)#
backup.yml installs Borg + systemd timers that back up the lichen_data
docker volume to a BorgBase repository daily, and verify archive integrity
monthly. The app is not stopped during the backup — lichen's site data is
git-managed and uses atomic writes, so hot archives restore cleanly.
Setup#
- Create a BorgBase repo and upload your SSH public key in their dashboard.
- Add these to
group_vars/all.yml(vault-encrypt the secrets):borg_repo: "ssh://xxxxxx@xxxxxx.repo.borgbase.com/./repo" borg_passphrase: "your-borg-repo-passphrase" borg_ssh_key_src: "~/.ssh/borgbase_ed25519" - Run the playbook:
ansible-playbook -i inventory.yml backup.yml --ask-vault-pass
First run uploads the key, writes /etc/lichen-borg.env, initializes the repo
if empty, installs /usr/local/bin/lichen-backup + /usr/local/bin/lichen-borg-check,
and enables the two timers.
Trigger a backup manually#
ssh root@server 'systemctl start lichen-backup.service'
journalctl -u lichen-backup.service --no-pager -n 50
Restore#
# list archives
borg list "$BORG_REPO"
# extract the latest into a temp dir
mkdir /tmp/restore && cd /tmp/restore
borg extract "$BORG_REPO::lichen-2026-04-17T03:00:00"
# stop the app, rsync the contents back over the live volume, restart
docker compose -f /srv/lichen/docker-compose.yml stop app
rsync -a --delete var/lib/docker/volumes/lichen_lichen_data/_data/ \
/var/lib/docker/volumes/lichen_lichen_data/_data/
docker compose -f /srv/lichen/docker-compose.yml start app
Retention#
Defaults: 7 daily / 4 weekly / 6 monthly. Override via
borg_keep_daily, borg_keep_weekly, borg_keep_monthly.
Files#
ansible/
├── ansible.cfg
├── deploy.yml # install docker + deploy from scratch
├── update.yml # pull latest image + restart
├── override.yml # upload a custom binary (or revert)
├── backup.yml # install borg + timers (optional)
├── inventory.example.yml
├── roles/
│ ├── docker/ # install docker engine + compose plugin
│ ├── lichen/ # sync compose files, render .env, bring up stack
│ └── borg/ # borg install, scripts, systemd units
└── group_vars/
└── .gitkeep # put vault-encrypted vars here