lotus-land-story: run monica
Moved as-is from ramble-hard, whose running build had drifted to 4.1.2
from the 3.7 its history pins.

Assisted-by: Claude Opus 5 via Claude Code
change yrlrzotslnqsvqtqktlyznxvyzoluuvv
commit 96041c67e2dfb6536e879e47a629dcf3a7c49794
author Alpha Chen <alpha@kejadlen.dev>
date
parent yowzqlrz
diff --git a/host_vars/lotus-land-story.sops.yml b/host_vars/lotus-land-story.sops.yml
index 843c681..f9814be 100644
--- a/host_vars/lotus-land-story.sops.yml
+++ b/host_vars/lotus-land-story.sops.yml
@@ -35,6 +35,10 @@ miniflux:
 minio:
     cluster_token: ENC[AES256_GCM,data:Huof3MtMLigijMwQbB6aNn+12lF5poE9QwqYk5LFzbhu9k49Vof/GnhTEe1v4Qm9S/3/6XwGfzmurK4J9q9oaAaBafVHbDrpPtKytxBcvxtAPqEmnjBpeKT55fmlWmkkc29EPamJHKqw5DXFcn/xwK3zfi2fJdyeORuhS5a6GQ67tyLyGn3rui5+pWMa+WyzavjmjQ0OdENkcbOfLZUfqsBYzBP17C69e7wliWXyAVJouC76lAM+ufZrj+Bsgzw=,iv:LtSvVZ/X48H5nykMdYrjrihnAJod4cZxVXHKvrQNhwk=,tag:Fw4Xxic7Qzy/RFnMKuYzHw==,type:str]
     bucket_token: ENC[AES256_GCM,data:aY0bJf8i0jfSomJHsxQtdMmt6mtTLLU5B1HjZtuMcdquEDEe6S/xyQIQd201IR9xUKfvElUm4kYW8FPK7EvtUoR+fFCjB1hk9lK2k1r72LUifeYud7pXUbv04l7YCL45qcCkefk/MqsQlRPyQaP9Lj3NrGgZ95WW+l3mki6KoHa3nzNhYZNd2rFNWkLGgssO2f3JPAylHB7fBioQXZsilmozO8admWz915Czy88v/FCALsMNMDu5kyln7580dpk=,iv:rQh3hJyLnUg9PNo5+h3+oH8zrXSERdZLT0MwDwCCuM4=,tag:7PqTzKmIkMxECsK3sNGohw==,type:str]
+monica:
+    app_key: ENC[AES256_GCM,data:IZw4NhZfdECZJjpIOsTTzN7hplbJIw1QK7LuZiCSyueAuVT2mW7Lym+LZHtIbqnL2lwa,iv:LhaECQzdDdXiziYXhjNTtUNiIYC4eunUHwJbznb8jnY=,tag:HiZigEU8/acbq+cBP2u9nw==,type:str]
+    db_password: ENC[AES256_GCM,data:jGFSSj44b6aiN+k3om6JV6wYdhO0g7NeM7WTHRM6lI0=,iv:KT6AQRSfd3USSdSz4D7xn6VC935rPCfgVoIFsneGiXk=,tag:1+L6+YVbL98uemphc3E6gw==,type:str]
+    mail_password: ENC[AES256_GCM,data:xL4rNXq8QOiOT/16JAqqO9bNBlHRkytHsXM8c9I0dKqOzvT4geMm//2COU/vzOChYcYg+aOcgX+b9xj/bzlnvBT0cVpl,iv:G6KSTUaiL3AVY+EKrBfGsOmIlrK3QwpIpEPGpssSii8=,tag:pMU6XRfJ1UdU1djTZ/WcVw==,type:str]
 paperless_ngx:
     secret: ENC[AES256_GCM,data:p9q584YTKqLybDtxmwOSi8SwC+kG6IZ+i091ws7Z3p0iqKTU67qhFZwnvzIpUFYhtFH6kgLjldMj4YGtkvTmItvan8xnB8NTZWXLi2SJTpWg82n/o78=,iv:dI+MrhqCb4fj3+uFV5ZI6EQaIoVjqDGcYxK5wmxCSSQ=,tag:8mAd613aoTHALAY68Sjq9g==,type:str]
 quire:
@@ -65,7 +69,7 @@ sops:
             LEedZQPpxvyODlXv/10aLZhO2DZBOtjw9W2ctfg9WoyX7OxZvdbFnA==
             -----END AGE ENCRYPTED FILE-----
           recipient: age15ars55ulrp9hz6r0utvluayrglt40wv0t997njez0y9l3e43cersn6kgh7
-    lastmodified: "2026-09-13T15:34:26Z"
-    mac: ENC[AES256_GCM,data:7ssjK98SU6ekf94ydXezaN6uj0TteDtQaPAD7r2zXO8/MYoZyWblJ30tSDT+HulRjGpOravmCJqsKyow/TCCYXpaCjG1+HGbHOAMogv/p230y/EfjxEvd879k0Ru1VxQahQ6h6eThCspjqwQfpZzyjR8QSfch6Mg6GEHmdFLhbU=,iv:MXIgfkwPM8hm13pmO1/XYXsteBX28FxPoPrBBlUlbgY=,tag:5CpuX+AtM4RYealtG/YLSw==,type:str]
+    lastmodified: "2026-09-15T02:50:40Z"
+    mac: ENC[AES256_GCM,data:0NYFZqexlz6MELdolQNyPKlPHhhWchsmR1ytpSzbAM2SWINHOtu1wi0V1qj5gWeWJk8vC2Tyg57M+KXR2m4zxOD+OYGDEscasbHcuG8reemHhmicVjF6Z19qIGgeHaAXdhaUBcq3hOr+GVPywVPm/z4m31VSgEScvFDxfRtgS8I=,iv:JWuhs5qko4lrlsxFareYy0EpOr8RJtDiLNmvdQTP6jc=,tag:Pzv+ICJ9KwSqWBSlOp02Mg==,type:str]
     unencrypted_suffix: _unencrypted
     version: 3.13.3
diff --git a/host_vars/lotus-land-story.yml b/host_vars/lotus-land-story.yml
index 74d54e7..636d774 100644
--- a/host_vars/lotus-land-story.yml
+++ b/host_vars/lotus-land-story.yml
@@ -13,6 +13,8 @@ versions:
   homebox:          0.25.0
   ketchup:          20260525-e75f249
   miniflux:         2.3.3
+  monica:           4.1.2
+  monica_mysql:     "5.7"
   paperless_ngx:    3.1.2
   quire:            latest # TODO Pin the version once we're not in active development anymore
   soju:             0.10.1 # remember to update the docker-soju image to bump this
diff --git a/lotus-land-story/main.yml b/lotus-land-story/main.yml
index 77d8bd3..ff43bee 100644
--- a/lotus-land-story/main.yml
+++ b/lotus-land-story/main.yml
@@ -61,6 +61,7 @@
 
 - import_playbook: grafana.yml     # noqa: name[play]
 - import_playbook: miniflux.yml    # noqa: name[play]
+- import_playbook: monica.yml      # noqa: name[play]
 - import_playbook: gotosocial.yml  # noqa: name[play]
 - import_playbook: ketchup.yml     # noqa: name[play]
 - import_playbook: caddy.yml       # noqa: name[play]
diff --git a/lotus-land-story/monica.yml b/lotus-land-story/monica.yml
new file mode 100644
index 0000000..cba47b3
--- /dev/null
+++ b/lotus-land-story/monica.yml
@@ -0,0 +1,158 @@
+# Carried over unchanged from ramble-hard's docker-compose setup: the stock
+# apache image with supervisord added so apache, cron, and the queue share one
+# container.
+
+- name: Set up Monica
+  hosts: lotus-land-story
+  tasks:
+
+    - name: Check for the copied Monica datadir
+      ansible.builtin.stat:
+        path: /mnt/lotus-land-story/monica/db/mysql
+      register: monica_datadir
+
+    # Otherwise MySQL initializes an empty datadir the data copy then lands on.
+    - name: Warn that Monica is skipped until its data is copied
+      ansible.builtin.debug:
+        msg: >-
+          Skipping Monica: copy ramble-hard's /usr/share/monica/{app,db} first;
+          see docs/lotus-land-story/2026-09-14-monica-migration.md.
+      when: not monica_datadir.stat.exists
+
+    - name: Skip Monica until its data is copied
+      ansible.builtin.meta: end_host
+      when: not monica_datadir.stat.exists
+
+    # app/ and db/ are left to the data copy, which preserves the ownership
+    # mysql and www-data expect.
+    - name: Create Monica directories
+      ansible.builtin.file:
+        path: /mnt/lotus-land-story/monica/{{ item }}
+        state: directory
+        mode: "0755"
+      loop:
+        - build
+        - php
+
+    - name: Write Monica Dockerfile
+      ansible.builtin.copy:
+        dest: /mnt/lotus-land-story/monica/build/Dockerfile
+        mode: "0644"
+        content: |
+          FROM docker.io/library/monica:{{ versions.monica }}-apache
+
+          # Use the default production configuration
+          RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
+
+          # supervisord dependencies
+          RUN set -ex; \
+              \
+              apt-get update; \
+              apt-get install -y --no-install-recommends \
+                  supervisor \
+              ; \
+              rm -rf /var/lib/apt/lists/*
+
+          COPY supervisord.conf /etc/supervisord.conf
+
+          CMD ["supervisord", "-c", "/etc/supervisord.conf"]
+      register: monica_dockerfile
+
+    - name: Write Monica supervisord config
+      ansible.builtin.copy:
+        dest: /mnt/lotus-land-story/monica/build/supervisord.conf
+        mode: "0644"
+        content: |
+          [supervisord]
+          nodaemon=true
+          user=root
+
+          [program:cron]
+          command=cron.sh
+          autostart=true
+          autorestart=true
+
+          [program:queue]
+          process_name=%(program_name)s_%(process_num)02d
+          command=queue.sh
+          numprocs=1
+          stdout_logfile=/proc/1/fd/1
+          stdout_logfile_maxbytes=0
+          stderr_logfile=/proc/1/fd/2
+          stderr_logfile_maxbytes=0
+          autostart=true
+          autorestart=true
+          startretries=0
+
+          [program:httpd]
+          process_name=%(program_name)s_%(process_num)02d
+          command=entrypoint.sh apache2-foreground
+          stdout_logfile=/proc/1/fd/1
+          stdout_logfile_maxbytes=0
+          stderr_logfile=/proc/1/fd/2
+          stderr_logfile_maxbytes=0
+          autostart=true
+          autorestart=true
+      register: monica_supervisord
+
+    - name: Write Monica PHP upload limit
+      ansible.builtin.copy:
+        dest: /mnt/lotus-land-story/monica/php/uploads.ini
+        mode: "0644"
+        content: |
+          [www]
+          upload_max_filesize = 10M
+
+    - name: Build Monica
+      community.docker.docker_image:
+        name: monica-supervisord
+        tag: "{{ versions.monica }}"
+        source: build
+        build:
+          path: /mnt/lotus-land-story/monica/build
+          pull: true
+        force_source: "{{ monica_dockerfile is changed or monica_supervisord is changed }}"
+
+    # The MYSQL_* env only seeds an empty datadir; the copied one ignores it.
+    - name: Run Monica db
+      community.docker.docker_container:
+        name: monica-db
+        image: docker.io/library/mysql:{{ versions.monica_mysql }}
+        restart_policy: unless-stopped
+        env:
+          MYSQL_RANDOM_ROOT_PASSWORD: "true"
+          MYSQL_DATABASE: monica
+          MYSQL_USER: monica
+          MYSQL_PASSWORD: "{{ monica.db_password }}"
+        volumes:
+          - /mnt/lotus-land-story/monica/db:/var/lib/mysql
+        networks:
+          - name: lotus_land_story
+
+    - name: Run Monica
+      community.docker.docker_container:
+        name: monica
+        image: monica-supervisord:{{ versions.monica }}
+        restart_policy: unless-stopped
+        env:
+          APP_ENV: production
+          APP_KEY: "{{ monica.app_key }}"
+          DB_HOST: monica-db
+          DB_USERNAME: monica
+          DB_PASSWORD: "{{ monica.db_password }}"
+          MAIL_MAILER: smtp
+          MAIL_HOST: smtp.sendgrid.net
+          MAIL_PORT: "25"
+          MAIL_USERNAME: apikey
+          MAIL_PASSWORD: "{{ monica.mail_password }}"
+          MAIL_ENCRYPTION: tls
+          MAIL_FROM_ADDRESS: admin@kejadlen.dev
+          MAIL_FROM_NAME: Monica
+          APP_EMAIL_NEW_USERS_NOTIFICATION: alpha+monica@kejadlen.dev
+        volumes:
+          - /mnt/lotus-land-story/monica/app:/var/www/html/storage
+          - /mnt/lotus-land-story/monica/php/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini
+        networks:
+          - name: lotus_land_story
+
+# vim: ft=yaml.ansible
diff --git a/lotus-land-story/templates/Caddyfile b/lotus-land-story/templates/Caddyfile
index 112bd90..b652c36 100644
--- a/lotus-land-story/templates/Caddyfile
+++ b/lotus-land-story/templates/Caddyfile
@@ -246,6 +246,12 @@ hey.kejadlen.dev {
 	}
 }
 
+monica.kejadlen.dev {
+	log
+
+	reverse_proxy monica:80
+}
+
 quire.kejadlen.dev {
 	reverse_proxy quire:3000
 }