Run Nextcloud as a rootless Podman Quadlet
Replaces the docker_container playbook, which bind-mounted /var/lib/nextcloud at /var/www/html on monoco and ran cron.php from root's crontab. Nextcloud now runs on emily behind the shared Caddy, with its database in the shared Postgres and its cache in the shared Valkey, each attached the way every other service attaches to them. /var/www/html is the unusual part: it holds the code as well as the state -- version.php, core/, lib/ and apps/ alongside config/config.php, data/ and custom_apps/ -- and the entrypoint's version check reads version.php from the mount. That is why the migration copies the whole tree rather than only the three irreplaceable directories (docs/backup-plan.md 2.1 lists the rest as regenerable, and they are, but only from an image that is the same version or newer). It is also what makes an image bump an in-place upgrade: a newer image rsyncs its code in and runs `occ upgrade` on first start, and an older one refuses to start at all, so the version on both hosts is worth checking before the copy. UserNS=keep-id:uid=33,gid=33. 33 is www-data, the uid the server actually writes as -- `docker exec nextcloud id -u` reports 0, because the entrypoint drops privileges after starting (docs/podman-compat-audit.md 3.2). 33 is also what the data is owned by on monoco, so the copy needs no chown pass: extracting as the connection user is already the ownership the container expects. One file is created by container-root and never chowned, the entrypoint's nextcloud-init-sync.lock, so the role pre-creates it -- the entrypoint truncates that file rather than recreating it, so its owner then never changes, and the webroot ends up with no subuid-owned file in it. The Valkey attach is not optional. config.php carries memcache.locking pointed at Redis, written by the image from REDIS_HOST, and the same variable rewrites PHP's session-handler config on every start -- so an unresolvable `redis` does not just make things slower, it fails uploads at write time. Hence the `redis` alias rather than the container name, and hence roles/valkey flushing handlers before the app starts, the same as roles/postgres. cron.php moves from root's crontab to a systemd user timer, because root cannot `podman exec` into another user's rootless containers and there is no container user to own a crontab (docs/podman-migration.md 8b). Unlike the Quadlet units this pair is plain and is genuinely enabled, which is what brings it back after a reboot; journalctl --user -u nextcloud-cron shows every run, which the crontab never did. Readiness is three gates, because no one of them is sufficient: is-active (which Restart=always satisfies even while crash-looping), `occ status` reporting installed, and status.php served over loopback by Apache rather than by the PHP CLI. The second is followed by a wait for maintenance mode to clear rather than an assert: maintenance mode is on for the whole of a first-start `occ upgrade`, so asserting it off would fail a healthy deploy halfway through. The case that never clears is a config.php copied with `maintenance => true`, which docs/backup-plan.md 3.4 tells an operator to set before a consistent dump, and which is invisible from outside because status.php is exempt from maintenance mode -- so the play names it and prints the command that clears it. Register and attach ordering is the same as the other service playbooks: register runs in pre_tasks, because the unit references nextcloud.network and Quadlet fails unit generation when that file does not exist, and both attaches flush handlers before the app starts, which is also what pulls Caddy onto the network. The Caddy snippet is unchanged, HSTS header included; only caddyfile_path moves, from /var/lib/caddy/conf/Caddyfile to ~/podman-data/caddy/conf/Caddyfile. The Docker-era roles/network is deleted rather than ported: it existed to attach an already-created container to a network with the `redis` alias, and podman_network has no connected:/appends: equivalent (docs/podman-compat-audit.md 3.3). Nothing is published host-side; Caddy reaches nextcloud:80 over nextcloud_net. The rootless-Podman prerequisites are not applied here: they are the shared task file at caddy/roles/podman, and a host that has run caddy/playbook.yml has them. valkey/playbook.yml has to have been run first, or the first Restart Valkey handler fails against a unit that does not exist yet.
This commit is contained in:
1 parent
3587bc9001
commit
43ea43e88b
16 files changed
+1307
-88
No files matched your search
+101
-17
@@ -1,28 +1,112 @@
|
||||
---
|
||||
container_name: nextcloud
|
||||
container_image: nextcloud:latest
|
||||
|
||||
docker_network: nextcloud_net
|
||||
|
||||
postgres_container: postgres
|
||||
caddy_container: caddy
|
||||
redis_container: valkey
|
||||
# ---------------------------------------------------------------------------
|
||||
# Rootless Podman / Quadlet
|
||||
# ---------------------------------------------------------------------------
|
||||
# Duplicated on purpose from caddy/vars/default.yml: a play cannot include that
|
||||
# file wholesale, because it also sets `host_directory` — to the Caddy data
|
||||
# directory — and every playbook defines that for itself. Keep these three in
|
||||
# sync with caddy/vars/default.yml. (Its fourth, podman_unprivileged_port_start,
|
||||
# belongs to the podman tasks, which this play does not run: Nextcloud listens on
|
||||
# 80 inside the container and publishes nothing host-side.)
|
||||
#
|
||||
# podman_user is derived from the connection: this play carries no become, so the
|
||||
# Quadlet units belong to whoever Ansible connects as. Requires gather_facts (set
|
||||
# in playbook.yml).
|
||||
podman_user: "{{ ansible_facts['user_id'] }}"
|
||||
podman_data_dir: "{{ ansible_facts['user_dir'] }}/podman-data"
|
||||
quadlet_dir: "{{ ansible_facts['user_dir'] }}/.config/containers/systemd"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Nextcloud
|
||||
# ---------------------------------------------------------------------------
|
||||
host: cloud.hopsngrains.com
|
||||
|
||||
caddyfile_path: /var/lib/caddy/conf/Caddyfile
|
||||
container_name: nextcloud
|
||||
unit_name: "{{ container_name }}.service"
|
||||
|
||||
# Fully qualified on purpose: rootless Podman resolves short names against
|
||||
# unqualified-search-registries, and there is no TTY to answer a prompt when the
|
||||
# systemd unit starts. The Docker-era playbook said `nextcloud:latest`.
|
||||
image: docker.io/library/nextcloud:latest
|
||||
|
||||
# The Quadlet file is <network_name>.network. caddy/roles/register pins the
|
||||
# Podman network name to <network_name>_net (so: nextcloud.network,
|
||||
# nextcloud_net).
|
||||
network_name: "{{ container_name }}"
|
||||
|
||||
# The image's VOLUME and Apache document root — and, unlike the other services
|
||||
# here, it holds the *code* as well as the state: config.php, data/,
|
||||
# custom_apps/ and every app's PHP files live under it. The Docker-era playbook
|
||||
# bind-mounted the whole thing from /var/lib/nextcloud, and this one keeps that
|
||||
# shape on purpose. See readme.md, "What is state, and what is not".
|
||||
host_directory: "{{ podman_data_dir }}/{{ container_name }}"
|
||||
webroot: /var/www/html
|
||||
|
||||
# The uid the *server* writes as: the image's entrypoint starts as container-root,
|
||||
# rsyncs the code in with --chown www-data:www-data, and Apache then drops to
|
||||
# www-data. `docker exec nextcloud id -u` reports 0, which is misleading
|
||||
# (docs/podman-compat-audit.md §3.2).
|
||||
app_uid: 33
|
||||
app_gid: 33
|
||||
|
||||
# The port Apache listens on inside the container, and the one Caddy proxies to.
|
||||
container_port: 80
|
||||
|
||||
# POSTGRES_* and REDIS_HOST are read by the entrypoint on every start — REDIS_HOST
|
||||
# in particular rewrites PHP's session handler config — and POSTGRES_PASSWORD is in
|
||||
# there, so it goes in this 0600 file rather than in the world-readable unit.
|
||||
env_file: "{{ host_directory }}/nextcloud.env"
|
||||
|
||||
# The shared Postgres unit this app attaches itself to (postgres/playbook.yml).
|
||||
# roles/postgres owns this app's drop-ins there and the "Restart Postgres"
|
||||
# handler that applies them.
|
||||
postgres_container: postgres
|
||||
postgres_unit: "{{ postgres_container }}.service"
|
||||
|
||||
# The credentials and database name the Docker-era playbook embedded in its
|
||||
# `env:` block, kept as-is so the already-restored `nextcloud` database (320 MB,
|
||||
# one of the 12 in postgres/migrate-from-monoco.md) and config.php agree.
|
||||
pg_user: postgres
|
||||
pg_password: '123456'
|
||||
pg_database: nextcloud
|
||||
|
||||
# `db` is the host config.php actually connects to — the Docker-era container set
|
||||
# POSTGRES_HOST=db, and postgres/roles/attach adds that alias to the shared
|
||||
# Postgres unit.
|
||||
db_host: db
|
||||
|
||||
# The shared Valkey unit this app attaches itself to (valkey/playbook.yml), and
|
||||
# the alias it is reached by. REDIS_HOST is not cosmetic: config.php's
|
||||
# `memcache.locking` is Redis, so an unresolvable `redis` breaks uploads rather
|
||||
# than merely slowing them down.
|
||||
valkey_container: valkey
|
||||
valkey_unit: "{{ valkey_container }}.service"
|
||||
redis_host: redis
|
||||
|
||||
# The shared Caddy unit and its Caddyfile, both owned by caddy/playbook.yml.
|
||||
caddy_name: caddy
|
||||
caddyfile_path: "{{ podman_data_dir }}/{{ caddy_name }}/conf/Caddyfile"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Background jobs
|
||||
# ---------------------------------------------------------------------------
|
||||
# Nextcloud wants `cron.php` every five minutes. The Docker-era playbook put it in
|
||||
# *root's* crontab as `docker exec -u www-data nextcloud php cron.php`; root
|
||||
# cannot `podman exec` into another user's rootless containers, so it becomes a
|
||||
# systemd user timer next to the Quadlet units (docs/podman-migration.md §8(b)).
|
||||
cron_unit: "{{ container_name }}-cron"
|
||||
cron_schedule: "*:0/5"
|
||||
user_unit_dir: "{{ ansible_facts['user_dir'] }}/.config/systemd/user"
|
||||
|
||||
# The site block, unchanged from the Docker-era vars: Caddy terminates TLS and
|
||||
# Nextcloud has always asked for HSTS. There is no request-body limit to carry
|
||||
# over — Caddy streams bodies and imposes none by default, whereas the dead nginx
|
||||
# front end needed client_max_body_size spelled out.
|
||||
snippet_block: |
|
||||
{{ host }} {
|
||||
reverse_proxy {{ container_name }}:80
|
||||
reverse_proxy {{ container_name }}:{{ container_port }}
|
||||
|
||||
header {
|
||||
Strict-Transport-Security "max-age=15552000; includeSubDomains; preload"
|
||||
}
|
||||
}
|
||||
|
||||
host_directory: nextcloud
|
||||
|
||||
db_name: "nextcloud"
|
||||
db_user: "postgres"
|
||||
db_pass: "123456"
|
||||
db_host: "db"
|
||||
Reference in new issue
Block a user