The role created ~/podman-data/nextcloud/redis-session.ini and the unit bind-mounted
it over the image path, purely because the entrypoint cannot write there itself.
Removed: the task, the mount, and -- because it is the only thing that triggers the
write -- REDIS_HOST from the environment file. The entrypoint now skips
configure_redis_session entirely and starts.
The Redis attach stays. The valkey role still puts nextcloud_net on the cache
container with the alias `redis`, so an instance whose config.php names that host
reaches it. Writing that configuration is config.php's business, and the readme
gives the three `occ config:system:set` lines for anyone who wants it, including
that this playbook will not do it for them.
The only thing lost is PHP session storage, which falls back to the container's own
filesystem -- a redeploy logs users out. That is the trade for not shipping a file
whose sole purpose is to work around a directory the image owns as root.
The env template and the valkey role comment both say why REDIS_HOST is absent, so
that adding it back is recognisable as the regression it is.
The timer was justified partly on XDG_RUNTIME_DIR: a user crontab "would need it
faked into every line". That is wrong here. The caddy playbook enables lingering,
so /run/user/<uid> exists whether or not anyone is logged in, and that is where
podman keeps the state `exec` reads. Nothing needs to be set.
The one real constraint is that the job cannot go in root's crontab: root cannot
`podman exec` into another user's rootless containers. This user's crontab is
fine, and the play runs as that user, so `ansible.builtin.cron` with no `user:`
writes exactly there.
That replaces four tasks and two template files with one task and no files, and it
is what the service had before the Quadlet conversion. The cost is that cron sends
its output to local mail instead of the journal; the readme says so and names the
timer as the alternative.
While here, the XDG_RUNTIME_DIR asides come out of the comments that carried them:
the play runs over SSH, so `systemctl --user` works because systemd's user manager
is reachable over that connection, and that is all a reader needs to know.
The role had grown machinery that the problem does not require. Removed:
- The init-sync lock pre-create. It guarded against one file in the webroot being
owned by a subuid, which is cosmetic: the file is 0644, so it is readable and
removable by host-side tools either way.
- The image ID read/pull pair. No other service playbook pulls an image, and the
update path is two commands the readme now spells out.
- redis-session.ini.j2 and its three variables. The mount is load-bearing -- the
entrypoint cannot write into the image's conf.d and dies trying -- but the
template is not: the entrypoint writes that file itself on every start. One
touched empty file and one Volume line replace the template and the variables.
- The config.php stat, the wizard note, the maintenance-mode wait, the occ status
report, the getent checks, the timer report. The installed/not-installed
branching existed only because the readiness gates were written for an installed
instance; with a gate that fits both, none of the branching is needed.
- Comments that narrated this play's earlier runs rather than the system.
What is left is four tasks before the start, a start-plus-gate block whose rescue
collects the journal, and four tasks for the background-job timer: 149 lines and
about 130 of them code, against 313 before. The readme loses its migration-era
bulk for the same reason and is 297 lines, in line with the other services'.
The one gate is now Nextcloud's own status.php inside the container. It answers
only once the entrypoint has finished -- including any `occ upgrade`, which runs
before Apache starts, so an update in progress is waited out rather than missed --
it reports `installed` either way, so a fresh webroot and an installed instance go
through the same check, and it carries `maintenance` as well, which is the one
state a probe would otherwise call healthy while every page returned 503.
The playbook is meant to be usable on its own, on any host, the way every other
one here is: the first run creates the container, and every run after that
converges it. It had picked up a migration shape instead -- a readme that assumed
a webroot copied in from monoco, a warning that nagged when config.php was
missing, and, worse, readiness gates that only passed for an instance that was
already installed.
That last one was a real bug for the intended use. On a host with no webroot, the
image initialises the code, writes no config.php because it has no admin
credentials to install with, and serves the setup wizard -- so `occ status` never
reports installed: true, and the play would have spent its whole retry budget and
then failed on a first run that was going exactly as designed.
What replaces it:
- The readiness gate is Apache answering status.php inside the container, which
is Nextcloud's own endpoint and reports `installed` either way. It is the one
thing `is-active` cannot tell us, and it passes for a wizard that has not been
completed.
- config.php's presence then decides the report: wait for maintenance mode to
clear and print `occ status` for an instance, or say what is left for a human to
do for a fresh one -- and succeed. The two optional credentials in
vars/default.yml install unattended instead, in which case the same endpoints
come back installed and the wizard note never appears.
- The app creates its own database. Postgres is a shared unit with one default
database and nothing else in this repo knows this app's database name, so a
fresh cluster previously reached the installer and failed on "database
nextcloud does not exist". It is a lookup and a `createdb`, a no-op afterwards,
and it fails with the instruction to run postgres/playbook.yml if the cluster
is not there rather than with a bare psql error.
- Updates. An update in a Quadlet deployment is "pull, then recreate": the unit
names a tag, `podman run` reuses the local image, and nothing fetches a newer
one. The role pulls on every run and compares the image ID either side, so an
unchanged tag is not a change and a running site is not restarted for nothing.
- Handlers are flushed before verification, so a queued recreate -- new image,
changed unit, changed environment -- happens before the checks rather than after
them, and the checks describe what will keep running.
Removed: the entire monoco migration section and its copy commands, the
config.php warning that existed only to serve it, the host-specific notes, and the
narrative comments about this play's first runs. References to the audit and
migration planning docs go with them; the ownership reasoning is stated inline,
since it does not depend on where an instance came from. `hosts:` is now the only
host-specific thing in the directory.
The readme is rewritten around the two paths it now has -- "First run" and
"Updates" -- with the deploy order, the session-file trap and the debugging
commands kept.
The real first run of this play did not fail on a transition. The journal shows
the entrypoint dying on every attempt:
nextcloud[302063]: => Configuring PHP session handler...
nextcloud[302063]: /entrypoint.sh: 121: cannot create
/usr/local/etc/php/conf.d/redis-session.ini: Permission denied
With REDIS_HOST set, the entrypoint generates PHP's session-handler config on
every start, and the target directory belongs to the image's root: the php base
image does `mkdir -p "$PHP_INI_DIR/conf.d"` with no chown. The uid this container
runs as has no write bit on it, the entrypoint exits 2 before Apache is reached,
Restart=always retries that, and systemd's start limit turns it into a `failed`
unit. Nothing in that shape says "permissions": the symptom is a unit state and a
message that only exists in journalctl.
The container now mounts a host-owned file over that exact path. A bind-mounted
file replaces the directory entry, so the directory's ownership stops mattering
and the write lands on podman_user. The role templates the five lines the
entrypoint would have written, so a refused write is no longer fatal either --
PHP still gets Redis sessions instead of a crash loop.
This also corrects a reading of the previous run that this play got wrong and
documented wrongly: `activating` is not evidence of a healthy start. A
crash-looping container spends its time `activating` between restarts, so the
earlier "reports a healthy start as a failure" comment and commit message were
describing a start that was in fact failing. The wait is still right -- it stops
promptly at a terminal state and hands over to a rescue that has the logs -- but
its comment now says what it actually buys.
Apache's own runtime directories need none of this, and the php base image is why:
it chowns /var/run/apache2, /var/lock/apache2 and /var/log/apache2 to www-data and
chmods them 1777, so only the session file was ever going to bite. The readme
gains a section on it, the diagnostic commands the play now collects, and a note
that the uid mapping is not to be derived from a `docker exec id -u` of 0.
The second run of this play stopped at "Wait for the Nextcloud unit to become
active" with:
{"attempts": 2, "cmd": ["systemctl", "--user", "is-active",
"nextcloud.service"], "rc": 3, "stdout": "failed"}
The wait itself did the right thing: `failed` is not `activating`, so it stopped
immediately instead of retrying for five minutes. But it sat outside the start
block, so the rescue that pulls status, journalctl and podman logs never ran, and
the failure carried no reason at all.
Move the wait inside "Start Nextcloud" so one rescue covers both, and widen the
diagnostics for the failure that is most likely after an interrupted run:
- `systemctl --user cat` -- the generated unit, which settles what systemd thinks
it is waiting for when a unit sits in `activating`.
- `podman ps -a --filter name=...` -- a container left behind under this name
makes `podman run` refuse to start, `Restart=always` retries that into systemd's
start limit, and the unit ends up `failed` while `podman logs` shows only the
previous container and explains nothing.
The final message now names that case and the commands that clear it, and points
an `activating`-for-the-whole-budget unit at the generated unit and journalctl
instead.
Running this play against an empty ~/podman-data/nextcloud does not fail. It
creates the webroot, writes the environment file, starts the image, and the
entrypoint initialises a fresh, uninstalled instance there -- its own code
rsynced in, data/.ocdata created, and no config.php, because there is no
NEXTCLOUD_ADMIN_USER to install with.
Copying monoco's tree on top of that then merges two versions of the code, which
is the drift the rest of this role warns about, and nothing in the play's output
says so. The role now stats config/config.php before the container starts and
warns when it is missing, naming both cases: a copy that never landed, and a play
that ran before the copy.
It warns rather than fails because installing a new instance through the web UI is
a legitimate use of this playbook and has no config.php until the installer
finishes. readme.md gains the recovery step next to the deploy step, including
why the rm -rf needs no `podman unshare`.
The first real run of this play stopped at "Verify Nextcloud is running":
fatal: [emily]: FAILED! => {"cmd": ["systemctl", "--user", "is-active",
"nextcloud.service"], "rc": 3, "stdout": "activating"}
Nothing was wrong with the container. `systemctl start` returns while the unit
is still `activating` -- the start job completes before conmon has signalled
readiness -- and `is-active` exits 3 for that state, so checking once reports a
healthy start as a failure. Every other app role here checks is-active only
after a wait that already tolerates startup (postgres waits on pg_isready,
valkey on valkey-cli ping, bitwarden on its healthcheck); Nextcloud is the one
where the check comes first, because its readiness gates are the expensive ones.
The wait retries only through the transition. A unit that is `failed` or
`inactive` is not going to become `active` by waiting, so that fails immediately
with the state in the message rather than five minutes later. What genuinely
needs patience -- the entrypoint rsyncing code and running `occ upgrade` before
Apache answers -- is the readiness gate that follows, which already has the
richer rescue.
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.
Updated the web nginx.conf and the source is not given in the readme.
Restricted to gondor.
Webserver image and container names are now picked from the vars file.