deployment templates for lichen
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

README.md

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#

  1. Copy the inventory example and edit it:

    cp inventory.example.yml inventory.yml
    # set ansible_host, ansible_user, lichen_domain
    
  2. Supply the admin password. Easiest: via --extra-vars at runtime. For anything non-trivial, use ansible-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:

  1. Install Docker if missing (via get.docker.com)
  2. Copy the docker-compose/ files to the server (/srv/lichen by default)
  3. Render .env from your inventory vars
  4. 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#

  1. Create a BorgBase repo and upload your SSH public key in their dashboard.
  2. 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"
    
  3. 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