Make the Nextcloud playbook standalone: first run installs, later runs update
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.
This commit is contained in:
1 parent
1038037e45
commit
be53cec757
11 files changed
+511
-672
No files matched your search
+238
-429
@@ -1,46 +1,44 @@
|
||||
# Nextcloud (rootless Podman + Quadlet)
|
||||
|
||||
Runs Nextcloud on `emily` as a **rootless Podman** container managed by a
|
||||
**systemd user unit** (Quadlet), behind the shared [rootless Caddy](../caddy/readme.md),
|
||||
with its database in the shared [rootless Postgres](../postgres/readme.md) and its
|
||||
cache in the shared [rootless Valkey](../valkey/readme.md). Replaces the
|
||||
`docker_container` deployment on `monoco`.
|
||||
Runs Nextcloud as a **rootless Podman** container managed by a **systemd user
|
||||
unit** (Quadlet), behind the shared [rootless Caddy](../caddy/readme.md), with its
|
||||
database in the shared [rootless Postgres](../postgres/readme.md) and its cache in
|
||||
the shared [rootless Valkey](../valkey/readme.md).
|
||||
|
||||
The container name, network, unit, Caddyfile upstream and database are all
|
||||
`nextcloud`, as they have been since the Docker-era playbook. The **host is
|
||||
unchanged**: `cloud.hopsngrains.com`.
|
||||
Standalone and idempotent, and host-agnostic apart from `hosts:` in
|
||||
[playbook.yml](playbook.yml) and `host` in [vars/default.yml](vars/default.yml):
|
||||
|
||||
Nextcloud is the one service here whose webroot is *also* its code directory.
|
||||
The Docker-era playbook bind-mounted `/var/lib/nextcloud` at `/var/www/html`, and
|
||||
this one keeps that shape: the image's `VOLUME`, `config/config.php`, `data/`,
|
||||
`custom_apps/` and every PHP file live under one mount. That single fact drives
|
||||
both the [migration](#migrating-the-data-from-monoco) and the ownership story
|
||||
below — see [What is state, and what is not](#what-is-state-and-what-is-not).
|
||||
- **First run** creates the webroot, the environment file, the unit and the
|
||||
background-job timer, starts the container, and leaves a fresh instance ready
|
||||
for its [setup wizard](#first-run) — or installs it unattended, if you set the
|
||||
two optional credentials first.
|
||||
- **Every run after that** converges the same things, and Quadlet recreates the
|
||||
container when the unit, the environment or the image changed. There is no
|
||||
update step to remember and nothing to copy in by hand.
|
||||
|
||||
## Ordering
|
||||
Nextcloud is the one service here whose webroot is *also* its code directory: the
|
||||
image's `VOLUME`, `config/config.php`, `data/`, `custom_apps/` and every PHP file
|
||||
live under one mount. That single fact drives the ownership story and the backup
|
||||
guidance below — see [What is state, and what is not](#what-is-state-and-what-is-not).
|
||||
|
||||
The three shared services are already migrated and running on `emily`, and this
|
||||
playbook is the last step:
|
||||
## Deploy order
|
||||
|
||||
1. [caddy/playbook.yml](../caddy/playbook.yml) — the reverse proxy, which owns
|
||||
the Caddyfile this site appends to, supplies the `caddy.service` handlers, and
|
||||
is the one play that applies the rootless-Podman prerequisites.
|
||||
2. [postgres/playbook.yml](../postgres/playbook.yml) — deployed, and its cluster
|
||||
brought across from monoco. **The `nextcloud` database is already restored**
|
||||
(320 MB, the largest of the 12 databases in
|
||||
[postgres/migrate-from-monoco.md](../postgres/migrate-from-monoco.md)).
|
||||
3. [valkey/playbook.yml](../valkey/playbook.yml) — **new**, and this is the first
|
||||
service that needs it. It has to exist before this playbook runs: the attach
|
||||
include only writes drop-ins next to `valkey.container`, so a `Restart Valkey`
|
||||
handler against an undeployed unit fails.
|
||||
4. This playbook — **run it after the file copy below**, not before. The app is
|
||||
what writes to `~/podman-data/nextcloud`, and its first start is when the
|
||||
entrypoint runs its version check against whatever is in the webroot.
|
||||
Three shared services have to exist on the host first, each of them standalone:
|
||||
|
||||
`config.php` connects to the hosts `db` and `redis`. Until this playbook has
|
||||
attached `nextcloud_net` to Postgres and to Valkey, neither name resolves on this
|
||||
network, so running this playbook is what makes the app reachable — there is no
|
||||
separate step.
|
||||
1. [caddy/playbook.yml](../caddy/playbook.yml) — the reverse proxy, which owns the
|
||||
Caddyfile this site appends to, supplies the `caddy.service` handlers, and is
|
||||
the one play that applies the rootless-Podman prerequisites.
|
||||
2. [postgres/playbook.yml](../postgres/playbook.yml) — the cluster. **This play
|
||||
attaches to it and creates its own `nextcloud` database in it**, so the cluster
|
||||
can be brand new and empty.
|
||||
3. [valkey/playbook.yml](../valkey/playbook.yml) — the cache. It has to exist
|
||||
before this playbook runs: the attach include only writes drop-ins next to
|
||||
`valkey.container`, so a `Restart Valkey` handler against an undeployed unit
|
||||
fails.
|
||||
|
||||
Then this playbook. `config.php` connects to the hosts `db` and `redis`; until it
|
||||
has attached `nextcloud_net` to Postgres and to Valkey, neither name resolves on
|
||||
that network, so running this playbook is what makes the app reachable.
|
||||
|
||||
## Layout
|
||||
|
||||
@@ -49,10 +47,10 @@ nextcloud/
|
||||
├── playbook.yml
|
||||
├── vars/default.yml
|
||||
└── roles/
|
||||
├── postgres/ # attaches this app's network to Postgres (+ its handler)
|
||||
├── valkey/ # attaches this app's network to Valkey (+ its handler)
|
||||
├── nextcloud/ # webroot, the 0600 env file, the Quadlet unit, the cron
|
||||
│ # timer, start + verify
|
||||
├── postgres/ # the database, and this app's network on the cluster
|
||||
├── valkey/ # this app's network on the cache
|
||||
├── nextcloud/ # webroot, env file, Quadlet unit, session config,
|
||||
│ # background-job timer, start + verify
|
||||
└── caddy/ # appends this site's block to the shared Caddyfile
|
||||
```
|
||||
|
||||
@@ -70,13 +68,13 @@ Quadlet units and state:
|
||||
| `~/.config/systemd/user/nextcloud-cron.{service,timer}` | the background-job timer (not Quadlet) |
|
||||
| `~/podman-data/nextcloud/` | the whole `/var/www/html`: code, `config/config.php`, `data/`, `custom_apps/` |
|
||||
| `~/podman-data/nextcloud/nextcloud.env` | `POSTGRES_*` + `REDIS_HOST`, mode `0600`, read by systemd |
|
||||
| `~/podman-data/nextcloud/php-conf.d/redis-session.ini` | PHP's session handler, mounted into the image at `/usr/local/etc/php/conf.d/` |
|
||||
| `~/podman-data/nextcloud/php-conf.d/redis-session.ini` | PHP's session handler, mounted into the image |
|
||||
| `~/podman-data/nextcloud/nextcloud-init-sync.lock` | the entrypoint's init lock, pre-created so it stays host-owned |
|
||||
|
||||
## What is state, and what is not
|
||||
|
||||
Per [docs/backup-plan.md §2.1](../docs/backup-plan.md), the instance is 1.2 GB
|
||||
and only a fraction of it is irreplaceable:
|
||||
The webroot is about 1.2 GB on a working instance, and most of it is not worth
|
||||
backing up:
|
||||
|
||||
| Path | Keep? | Why |
|
||||
|---|---|---|
|
||||
@@ -86,12 +84,13 @@ and only a fraction of it is irreplaceable:
|
||||
| `themes/` | if themed | only exists when a theme was installed |
|
||||
| `version.php`, `core/`, `lib/`, `apps/`, `3rdparty/`, `dist/` | regenerable | shipped by the image, rsynced into the webroot on start |
|
||||
|
||||
**The migration copies all of it anyway**, and that is deliberate. Because the
|
||||
mount holds the code, the entrypoint decides what to do by reading
|
||||
`/var/www/html/version.php` **from the mount**: copy the whole tree and the first
|
||||
start is close to a no-op; copy only the state and `version.php` is missing, so
|
||||
every start looks like a brand-new instance — no `occ upgrade`, no version
|
||||
tracking — and Nextcloud would then have to be talked through a manual upgrade.
|
||||
The database is separate and is not in this directory at all. A backup that
|
||||
matters is `config/`, `data/`, `custom_apps/` **and** a `pg_dump` of the
|
||||
`nextcloud` database taken together — files without the database restore to an
|
||||
instance that does not know about its own contents, and the reverse is just as
|
||||
useless. Nextcloud's own guidance is `occ maintenance:mode --on` around the pair;
|
||||
the play waits for maintenance mode to clear on the next run, so a forgotten
|
||||
`--on` is reported rather than silently 503ing.
|
||||
|
||||
## How it registers with Caddy
|
||||
|
||||
@@ -112,39 +111,13 @@ referenced `.network` file does not exist yet. Both attaches below depend on it
|
||||
for the same reason.
|
||||
|
||||
`network_name` is `nextcloud`, so the *file* is `nextcloud.network` and the
|
||||
*Podman network* is `nextcloud_net` (`NetworkName=` in the network unit pins it,
|
||||
keeping the Docker-era name).
|
||||
|
||||
The Docker-era `roles/network` is **deleted rather than ported**. It existed to
|
||||
attach an already-created container to a network — `docker_network`'s
|
||||
`connected:`/`appends:`, with the `redis` alias for the old `valkey` container —
|
||||
which `podman_network` has no equivalent for
|
||||
([docs/podman-compat-audit.md §3.3](../docs/podman-compat-audit.md)). Nothing has
|
||||
to be connected after the fact now: the app's own unit names its `Network=`, and
|
||||
Caddy, Postgres and Valkey learn about the network from drop-ins written in the
|
||||
same run.
|
||||
*Podman network* is `nextcloud_net`.
|
||||
|
||||
The site block is appended to the shared Caddyfile by
|
||||
[roles/caddy](roles/caddy/tasks/main.yaml) with `blockinfile`, exactly as the
|
||||
other services do. **Only `caddyfile_path` changed** — from
|
||||
`/var/lib/caddy/conf/Caddyfile` to `~/podman-data/caddy/conf/Caddyfile` — and the
|
||||
block itself is the Docker-era one, HSTS header included:
|
||||
|
||||
```
|
||||
cloud.hopsngrains.com {
|
||||
reverse_proxy nextcloud:80
|
||||
|
||||
header {
|
||||
Strict-Transport-Security "max-age=15552000; includeSubDomains; preload"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `marker` in the role is unchanged from the Docker era, so an existing block
|
||||
on a reused Caddyfile is replaced in place rather than duplicated. There is no
|
||||
request-body limit to carry over: Caddy streams request bodies and imposes none
|
||||
by default, so the `client_max_body_size 525M` the dead nginx front end carried
|
||||
has no equivalent and needs none.
|
||||
other services do — plus the HSTS header Nextcloud asks for. There is no
|
||||
request-body limit to set: Caddy streams request bodies and imposes none by
|
||||
default.
|
||||
|
||||
## How it attaches to Postgres
|
||||
|
||||
@@ -158,26 +131,31 @@ into `postgres.container` by Quadlet:
|
||||
| `postgres.container.d/50-nextcloud.conf` | `Network=nextcloud.network` |
|
||||
| `postgres.container.d/10-network-aliases.conf` | `NetworkAlias=db` |
|
||||
|
||||
`config.php`'s `dbhost` is `db`, which is what the Docker-era container's
|
||||
`POSTGRES_HOST=db` wrote; the `postgres` container name resolves too, and the play
|
||||
checks both.
|
||||
`db` is the alias `config.php` connects to; the `postgres` container name resolves
|
||||
too, and the play checks both.
|
||||
|
||||
[roles/postgres](roles/postgres/tasks/main.yaml) wraps that include for two
|
||||
[roles/postgres](roles/postgres/tasks/main.yaml) wraps that include for three
|
||||
reasons:
|
||||
|
||||
- it defines the `Restart Postgres` handler the include notifies; and
|
||||
- it **flushes handlers** immediately afterwards. On a first attach the drop-in
|
||||
is new, so Postgres has to be *recreated* to join `nextcloud_net` — and that
|
||||
restart is a handler, which by default runs at the end of the play, after the
|
||||
`nextcloud` role has already started the app. The app would then come up before
|
||||
`db` resolves, fail its own readiness check, and fail the play on a machine
|
||||
where nothing is wrong. The flush also fires the `Restart Caddy` queued by the
|
||||
register pre_task, which is what attaches Caddy to the network. On later runs
|
||||
nothing is pending and it is a no-op.
|
||||
- it defines the `Restart Postgres` handler the include notifies;
|
||||
- it **flushes handlers** immediately afterwards. On a first attach the drop-in is
|
||||
new, so Postgres has to be *recreated* to join `nextcloud_net` — and that restart
|
||||
is a handler, which by default runs at the end of the play, after the `nextcloud`
|
||||
role has already started the app. The app would then come up before `db`
|
||||
resolves, fail its readiness check, and fail the play on a machine where nothing
|
||||
is wrong. The flush also fires the `Restart Caddy` queued by the register
|
||||
pre_task, which is what attaches Caddy to the network. On later runs nothing is
|
||||
pending and it is a no-op;
|
||||
- it **creates the `nextcloud` database** if the cluster does not have it yet.
|
||||
Postgres is a shared unit that ships one default database, and nothing else in
|
||||
this repo knows what this app's database is called, so without that task a
|
||||
first run on a fresh host would reach the setup wizard only to fail on
|
||||
"database nextcloud does not exist". It is a `createdb` guarded by a lookup, so
|
||||
it is a no-op on every run after the first.
|
||||
|
||||
Note the cost stated in [postgres/readme.md](../postgres/readme.md): a new
|
||||
drop-in recreates Postgres, which is a few seconds of downtime for **every**
|
||||
attached app, not just this one.
|
||||
Note the cost stated in [postgres/readme.md](../postgres/readme.md): a new drop-in
|
||||
recreates Postgres, which is a few seconds of downtime for **every** attached app,
|
||||
not just this one.
|
||||
|
||||
## How it attaches to Valkey
|
||||
|
||||
@@ -189,37 +167,29 @@ The same idea at the cache end, via
|
||||
| `valkey.container.d/50-nextcloud.conf` | `Network=nextcloud.network` |
|
||||
| `valkey.container.d/10-network-aliases.conf` | `NetworkAlias=redis` |
|
||||
|
||||
`redis` is not a name this playbook chose. The image writes `config.php`'s Redis
|
||||
host from `REDIS_HOST`, the Docker-era `env:` block set it to `redis`, and the
|
||||
same variable rewrites PHP's session-handler config on **every** start. Renaming
|
||||
it would mean editing `config.php` on a live instance for nothing.
|
||||
`redis` is not a name this playbook chose: the image writes the Redis host into
|
||||
`config.php` and into PHP's session config from `REDIS_HOST`, which is `redis`
|
||||
here. Renaming it would mean editing a live instance's configuration for nothing.
|
||||
|
||||
It is also **not optional**. `config.php` carries `memcache.locking` pointed at
|
||||
Redis, so an unresolvable `redis` does not merely make things slower — uploads
|
||||
fail, and they fail at write time rather than at startup. That is why
|
||||
`roles/valkey` flushes handlers before the app starts (same reason as Postgres),
|
||||
and why `REDIS_HOST` is in the environment file at all.
|
||||
It is also **not optional**. Go through the setup wizard and Nextcloud puts
|
||||
`memcache.locking` on Redis; the image's own install does too when the unattended
|
||||
credentials are set. An unresolvable `redis` therefore does not merely make things
|
||||
slower — uploads fail, and they fail at write time rather than at startup. That is
|
||||
why `roles/valkey` flushes handlers before the app starts (the same reason as
|
||||
Postgres) and why `REDIS_HOST` is in the environment file at all.
|
||||
|
||||
The cost of the attach is a recreate of Valkey, and it is **not** the same story
|
||||
as Postgres's. No keys are lost — RDB persistence is on by default, the stop is
|
||||
graceful, and the new container loads the snapshot back off the bind mount — but
|
||||
for the seconds it is down, Nextcloud's PHP sessions and its `memcache.locking`
|
||||
are unreachable, so logins and uploads stall rather than fail cleanly. See
|
||||
[valkey/readme.md](../valkey/readme.md), "The trade-off".
|
||||
The cost of the attach is a recreate of Valkey: no keys are lost (RDB persistence
|
||||
is on by default, the stop is graceful, and the new container loads the snapshot
|
||||
back off the bind mount), but for the seconds it is down, PHP sessions and
|
||||
`memcache.locking` are unreachable. See [valkey/readme.md](../valkey/readme.md),
|
||||
"The trade-off".
|
||||
|
||||
## Background jobs
|
||||
|
||||
Nextcloud expects `cron.php` to run every five minutes, and the Docker-era
|
||||
playbook scheduled it in **root's crontab**:
|
||||
|
||||
```
|
||||
*/5 * * * * docker exec -u www-data nextcloud php cron.php
|
||||
```
|
||||
|
||||
That cannot survive the move: root cannot `podman exec` into another user's
|
||||
rootless containers, and there is no container user to own a crontab
|
||||
([docs/podman-migration.md §8(b)](../docs/podman-migration.md)). The role
|
||||
installs a **systemd user** pair next to the Quadlet units instead:
|
||||
Nextcloud expects `cron.php` every five minutes. It cannot be a crontab entry:
|
||||
root cannot `podman exec` into another user's rootless containers, and there is no
|
||||
container user to own a crontab. The role installs a **systemd user** pair next to
|
||||
the Quadlet units instead:
|
||||
|
||||
| Unit | What it does |
|
||||
|---|---|
|
||||
@@ -227,15 +197,13 @@ installs a **systemd user** pair next to the Quadlet units instead:
|
||||
| `~/.config/systemd/user/nextcloud-cron.timer` | `OnCalendar=*:0/5`, `Persistent=true` |
|
||||
|
||||
Unlike the Quadlet units these are plain units, so they really are `enable`d —
|
||||
that is what brings the timer back after a reboot. No `sudo`, no crontab, and
|
||||
`journalctl --user -u nextcloud-cron` shows every run and every failure, which the
|
||||
old crontab never did.
|
||||
that is what brings the timer back after a reboot. `journalctl --user -u
|
||||
nextcloud-cron` shows every run and every failure.
|
||||
|
||||
The background-job *mode* is left alone: `config.php` came from monoco with
|
||||
whatever it had, and this playbook does not switch it to `cron`. Both the timer
|
||||
and AJAX polling run jobs; job locking is what keeps them from colliding. If you
|
||||
want AJAX disabled, that is `occ background:cron`, a deliberate change rather
|
||||
than a migration step.
|
||||
The background-job *mode* is left alone. Both this timer and AJAX polling run
|
||||
jobs, and job locking keeps them from colliding; switching AJAX off is
|
||||
`occ background:cron`, a deliberate change rather than something this play
|
||||
imposes.
|
||||
|
||||
## Settings
|
||||
|
||||
@@ -244,19 +212,21 @@ than a migration step.
|
||||
| `podman_user` | **Derived** from the connection (`ansible_facts['user_id']`), not set by hand |
|
||||
| `podman_data_dir` | `~/podman-data` |
|
||||
| `quadlet_dir` | `~/.config/containers/systemd` |
|
||||
| `host` | `cloud.hopsngrains.com` — the site block Caddy serves |
|
||||
| `host` | `cloud.hopsngrains.com` — the hostname Caddy serves, and the trusted domain for an unattended install |
|
||||
| `container_name` | `nextcloud`, and the Quadlet unit / container name |
|
||||
| `image` | `docker.io/library/nextcloud:latest` — fully qualified on purpose |
|
||||
| `network_name` | `nextcloud` → `nextcloud.network` / `nextcloud_net` |
|
||||
| `host_directory` / `webroot` | `~/podman-data/nextcloud` → `/var/www/html` |
|
||||
| `app_uid` / `app_gid` | `33` — `www-data`, the uid the server writes as |
|
||||
| `app_uid` / `app_gid` | `33` — `www-data`, the uid the webroot is owned by |
|
||||
| `container_port` | `80`, the port inside the container that Caddy proxies to |
|
||||
| `env_file` | `~/podman-data/nextcloud/nextcloud.env`, mode `0600` |
|
||||
| `php_conf_dir` / `php_session_ini` | PHP's session config on the host, and the path in the image it is mounted over |
|
||||
| `postgres_container` / `postgres_unit` | The shared database unit this app attaches to |
|
||||
| `pg_user` / `pg_password` / `pg_database` | Credentials, and the database name — `nextcloud` |
|
||||
| `pg_user` / `pg_password` / `pg_database` | Credentials, and the database the play creates and the app uses — `nextcloud` |
|
||||
| `db_host` | `db` — the alias `config.php` connects to |
|
||||
| `valkey_container` / `valkey_unit` | The shared cache unit this app attaches to |
|
||||
| `redis_host` | `redis` — the alias `config.php` and PHP sessions connect to |
|
||||
| `nextcloud_admin_user` / `nextcloud_admin_password` | Empty by default; set both to install unattended |
|
||||
| `cron_unit` / `cron_schedule` / `user_unit_dir` | The background-job timer |
|
||||
| `caddy_name` / `caddyfile_path` | The shared Caddy unit and its Caddyfile |
|
||||
|
||||
@@ -273,31 +243,34 @@ Like Caddy, `podman_user` is derived from the connection and the play carries no
|
||||
connecting as `root`**; the prerequisites, including that check, live in
|
||||
`caddy/playbook.yml`.
|
||||
|
||||
`UserNS=keep-id:uid=33,gid=33` — `33` is the uid the server writes as. Whatever
|
||||
the entrypoint's *own* starting uid under this mapping, the writes that matter land
|
||||
on `podman_user`: the code is rsynced with `--chown www-data:www-data`, and Apache's
|
||||
workers run as `www-data`
|
||||
([docs/podman-compat-audit.md §3.2](../docs/podman-compat-audit.md)). `33` is also
|
||||
what the data is owned by on monoco, which is what makes the copy below need no
|
||||
`chown` pass.
|
||||
`UserNS=keep-id:uid=33,gid=33`. `33` is `www-data`, the uid the webroot is owned
|
||||
by: the entrypoint rsyncs the code in with `--chown www-data:www-data` and Apache
|
||||
runs its workers as that account. `keep-id` maps that one uid onto the connection
|
||||
user, so the whole webroot stays owned by `tanshu` and plain `tar` and `rm -rf`
|
||||
keep working on it.
|
||||
|
||||
Two paths the entrypoint writes fall outside that arrangement, and both are handled
|
||||
explicitly rather than left to chance — each one is somewhere a naive deployment
|
||||
ends up with a file owned by a `165536+` subuid, or with no deployment at all:
|
||||
Two paths the entrypoint writes fall outside that arrangement, and both are
|
||||
handled explicitly rather than left to chance — each is somewhere a naive
|
||||
deployment ends up with a file owned by a `165536+` subuid, or with no running
|
||||
container at all:
|
||||
|
||||
- **`/var/www/html/nextcloud-init-sync.lock`**, which the entrypoint creates with a
|
||||
root-owned shell redirect. The role **pre-creates it** as `podman_user`: the
|
||||
redirect truncates the file rather than recreating it, so its owner never changes
|
||||
afterwards.
|
||||
- **`/var/www/html/nextcloud-init-sync.lock`**, which the entrypoint creates with
|
||||
a root-owned shell redirect. The role **pre-creates it** as `podman_user`: the
|
||||
redirect truncates the file rather than recreating it, so its owner never
|
||||
changes afterwards.
|
||||
- **`/usr/local/etc/php/conf.d/redis-session.ini`**, in the image, which is not
|
||||
writable at all — it is covered in its own section below.
|
||||
writable at all. It has its own section below.
|
||||
|
||||
### The PHP session file
|
||||
`POSTGRES_PASSWORD` is committed in plaintext, like the other playbooks' shared
|
||||
`123456`. It lives in `nextcloud.env` at mode `0600` rather than in the
|
||||
world-readable unit file; a real secret store is the next step, and it is a
|
||||
one-file change here plus the `pg_password` vars in the app playbooks.
|
||||
|
||||
## The PHP session file
|
||||
|
||||
The webroot is not the only thing the entrypoint writes. With `REDIS_HOST` set —
|
||||
which it is, because `config.php` puts `memcache.locking` in Redis and PHP
|
||||
sessions should survive a redeploy — the entrypoint generates that session
|
||||
configuration on **every** start:
|
||||
and it is, because PHP sessions should survive a redeploy — the entrypoint
|
||||
generates that session configuration on **every** start:
|
||||
|
||||
```sh
|
||||
/entrypoint.sh:121
|
||||
@@ -345,14 +318,9 @@ chmods them `1777`:
|
||||
done
|
||||
```
|
||||
|
||||
`POSTGRES_PASSWORD` is the fleet's shared `123456`, which matches
|
||||
`pg_password` in [postgres/vars/default.yml](../postgres/vars/default.yml) and
|
||||
what `config.php` already carries — a migrated app connects unchanged. It lives
|
||||
in `nextcloud.env` at mode `0600` rather than in the world-readable unit file.
|
||||
Both are committed in plaintext as before; flagged as a follow-up, not changed
|
||||
here.
|
||||
so the session file is the only write that was ever going to fail.
|
||||
|
||||
## Running
|
||||
## First run
|
||||
|
||||
```command
|
||||
ansible-playbook -u tanshu nextcloud/playbook.yml
|
||||
@@ -361,191 +329,78 @@ ansible-playbook -u tanshu nextcloud/playbook.yml
|
||||
The play targets `emily` directly, so the inventory must contain it — the one in
|
||||
`prepare-server/` does, and its `ansible.cfg` names it.
|
||||
|
||||
This play applies **only** this app's units and data. The rootless-Podman
|
||||
prerequisites — installing podman, `loginctl enable-linger`, the low-port sysctl
|
||||
and the Quadlet directory — are the shared task file at
|
||||
[caddy/roles/podman/tasks/main.yaml](../caddy/roles/podman/tasks/main.yaml), and
|
||||
are **not** run here. A host that has already run `caddy/playbook.yml` has them.
|
||||
On a host with no webroot yet, this is what happens, in order: the webroot and the
|
||||
PHP session directory are created; the `0600` environment file, the session config
|
||||
and the Quadlet unit are written; the unit is started; anything the run queued (a
|
||||
new image, a changed unit or environment) is applied *before* verification; and
|
||||
then the play waits for Apache to answer `status.php` inside the container.
|
||||
|
||||
`systemctl --user enable nextcloud` is neither needed nor possible: the unit is
|
||||
*generated*, and Quadlet applies `[Install] WantedBy=default.target` itself. The
|
||||
cron **timer** is a plain unit and is enabled explicitly, as described above.
|
||||
At that point there is a running container and **no instance yet** — the image
|
||||
initialises the code, writes no `config.php` (it has no admin credentials to
|
||||
install with), and serves the setup wizard. The play says so and succeeds:
|
||||
|
||||
## Migrating the data from monoco
|
||||
|
||||
The database and the files have to correspond, so this is a freeze, not a
|
||||
best-effort copy. Run it from the machine that holds the `monoco` and `emily` SSH
|
||||
aliases, as in [postgres/migrate-from-monoco.md](../postgres/migrate-from-monoco.md).
|
||||
|
||||
Two things are already done: the `nextcloud` database is restored on `emily`, and
|
||||
the Caddyfile and certificates live there too. What is left is
|
||||
`/var/lib/nextcloud`.
|
||||
|
||||
### 0. Look first
|
||||
|
||||
```bash
|
||||
# What is there, how big, and whether it is readable without sudo. Nextcloud's
|
||||
# data/ is normally 0770 www-data:www-data, so this says "no" — the sudo form
|
||||
# below is the expected one, not the exception.
|
||||
ssh monoco 'du -sh /var/lib/nextcloud; ls -ln /var/lib/nextcloud'
|
||||
ssh monoco 'find /var/lib/nextcloud -type f ! -readable | head' # empty = no sudo needed
|
||||
|
||||
# The version on monoco versus the image emily will run. The entrypoint compares
|
||||
# the two on first start: a NEWER image rsyncs its code in and runs `occ upgrade`
|
||||
# (one major version at a time, so a large gap needs stepping stones), while an
|
||||
# OLDER image refuses to start at all — "downgrading is not supported".
|
||||
ssh monoco 'docker exec nextcloud php -r "require \"/var/www/html/version.php\"; echo implode(\".\", \$OC_Version), PHP_EOL;"'
|
||||
ssh emily 'podman run --rm --entrypoint php docker.io/library/nextcloud:latest \
|
||||
-r "require \"/usr/src/nextcloud/version.php\"; echo implode(\".\", \$OC_Version), PHP_EOL;"'
|
||||
```
|
||||
The container is running, but no instance is installed in
|
||||
/home/tanshu/podman-data/nextcloud yet. Open https://cloud.hopsngrains.com and
|
||||
finish the setup wizard with the database credentials in .../nextcloud.env —
|
||||
database nextcloud, user postgres, host db. ...
|
||||
```
|
||||
|
||||
### 1. Freeze monoco
|
||||
Three things to know about that wizard:
|
||||
|
||||
```bash
|
||||
# Stop the app first: a stopped container cannot write to data/ while it is being
|
||||
# copied, and no maintenance-mode dance is needed. Leave it stopped — its database
|
||||
# is on emily, and two live copies would diverge.
|
||||
ssh monoco 'docker stop nextcloud'
|
||||
```
|
||||
- **DNS first.** `host` must resolve to this host before Caddy can obtain a
|
||||
certificate for it, and before the wizard is reachable at all.
|
||||
- **The database already exists.** [roles/postgres](roles/postgres/tasks/main.yaml)
|
||||
created it, or verified it, before the container started. Point the wizard at
|
||||
host `db`, database `nextcloud`, user `postgres`, and the password in
|
||||
`nextcloud.env`.
|
||||
- **`trusted_domains`.** The wizard installs with `localhost` as the only trusted
|
||||
domain unless you tell it otherwise, so opening the finished instance on `host`
|
||||
can answer "Access through untrusted domain". Add it before or after:
|
||||
|
||||
### 2. Make the database correspond (only if it moved since the cluster move)
|
||||
```bash
|
||||
podman exec -u www-data nextcloud php occ config:system:set trusted_domains 1 --value=cloud.hopsngrains.com
|
||||
```
|
||||
|
||||
The cluster was dumped on **2026-10-15**. If monoco's Nextcloud has served writes
|
||||
since then, the files you are about to copy are newer than the database on
|
||||
`emily`, and restoring the files alone would leave the instance inconsistent.
|
||||
Re-dump the one database and reload it — it is 320 MB, so this is cheap.
|
||||
To skip the wizard entirely, set `nextcloud_admin_user` and
|
||||
`nextcloud_admin_password` in [vars/default.yml](vars/default.yml) before the first
|
||||
run. The entrypoint then runs `occ maintenance:install` itself and sets
|
||||
`trusted_domains` from `host` in the same pass, and the play reports the installed
|
||||
version instead of the wizard note. Both variables are inert once `config.php`
|
||||
exists, so they can stay set (or be removed) afterwards.
|
||||
|
||||
If the app is already running on `emily` from an earlier attempt, stop it first:
|
||||
`DROP DATABASE` needs the connections gone, and a running Nextcloud holds several.
|
||||
## Updates
|
||||
|
||||
```bash
|
||||
ssh emily 'systemctl --user stop nextcloud.service' # only if it is already deployed
|
||||
Nothing special is required: re-run the playbook. What happens depends on what
|
||||
changed.
|
||||
|
||||
```bash
|
||||
# Dump on monoco (Postgres is a separate container and is still running).
|
||||
ssh monoco 'mkdir -p ~/pg-migrate && cd ~/pg-migrate
|
||||
docker exec -e PGPASSWORD=123456 postgres pg_dump -U postgres -Fc nextcloud \
|
||||
| zstd -T0 -6 -o nextcloud-$(date +%Y%m%d-%H%M).dump.zst
|
||||
ls -lh nextcloud-*.dump.zst'
|
||||
| Changed | What the play does |
|
||||
|---|---|
|
||||
| The unit, the environment file or the session config | re-templates it, and Quadlet recreates the container |
|
||||
| A new image behind the tag | pulls it, notices the ID changed, and recreates the container |
|
||||
| Nothing | nothing — no restart, no churn |
|
||||
|
||||
# Transfer, then check the checksum on both sides before trusting it.
|
||||
ssh monoco 'cat ~/pg-migrate/nextcloud-*.dump.zst' \
|
||||
| ssh emily 'cat > ~/pg-migrate/nextcloud.dump.zst'
|
||||
a=$(ssh monoco 'sha256sum ~/pg-migrate/nextcloud-*.dump.zst | cut -d" " -f1')
|
||||
b=$(ssh emily 'sha256sum ~/pg-migrate/nextcloud.dump.zst | cut -d" " -f1')
|
||||
[ -n "$a" ] && [ "$a" = "$b" ] && echo "transfer OK: $a" \
|
||||
|| { echo "MISMATCH — do not restore: $a vs $b"; false; }
|
||||
The image is pulled on every run precisely because a Quadlet unit alone cannot see
|
||||
a tag move: the unit names `nextcloud:latest`, `podman run` reuses whatever image
|
||||
is local, and nothing fetches a newer one. The role reads the image ID either side
|
||||
of the pull, so an unchanged tag reports no change and does not restart a running
|
||||
site. A pull that fails (a host with no route to the registry) is reported and the
|
||||
run continues with the local image.
|
||||
|
||||
# Reload it on emily. This DROPS the existing database — it is the one the
|
||||
# cluster move restored, and the frozen dump replaces it.
|
||||
ssh emily 'cd ~/pg-migrate
|
||||
podman exec -e PGPASSWORD=123456 postgres \
|
||||
psql -U postgres -d postgres -v ON_ERROR_STOP=1 \
|
||||
-c "DROP DATABASE IF EXISTS nextcloud" \
|
||||
-c "CREATE DATABASE nextcloud OWNER postgres"
|
||||
zstd -dc nextcloud.dump.zst | podman exec -i -e PGPASSWORD=123456 postgres \
|
||||
pg_restore -U postgres -d nextcloud --no-owner --exit-on-error
|
||||
podman exec -e PGPASSWORD=123456 postgres \
|
||||
psql -U postgres -d nextcloud -tAc "select count(*) from oc_filecache"'
|
||||
```
|
||||
**Version drift is the one irreversible step.** The entrypoint compares
|
||||
`version.php` in the webroot against the image. A newer image rsyncs its code in
|
||||
and runs `occ upgrade` before Apache answers — and it will only step **one major
|
||||
version at a time**, so several majors behind means several runs with an
|
||||
intermediate image. An image *older* than the installed version makes the
|
||||
container exit with "downgrading is not supported". So: pin the tag if you want
|
||||
no surprises, and take a database dump before a run that is going to upgrade.
|
||||
|
||||
If monoco's container simply sat idle after the cluster move, skip this and go
|
||||
straight to the copy — but the file counts at the end of step 3 are what tell you
|
||||
whether the two halves agree.
|
||||
|
||||
### 3. Copy the files
|
||||
|
||||
```bash
|
||||
# 1. Create the destination: the mount target of the image's /var/www/html.
|
||||
ssh emily 'mkdir -p ~/podman-data/nextcloud'
|
||||
|
||||
# 2. Copy. tar streams it across the workstation; rsync cannot copy
|
||||
# remote-to-remote in one command.
|
||||
#
|
||||
# The WHOLE tree is copied — code as well as state — because /var/www/html is
|
||||
# both. See "What is state, and what is not" above.
|
||||
#
|
||||
# `set -o pipefail` matters here: without it the pipeline reports only the
|
||||
# *receiving* tar's status, so a read error or a dropped stream on the source
|
||||
# side exits 0 and leaves a silently short copy.
|
||||
#
|
||||
# `--no-same-owner` is explicit and important: the source is 33:33 (and partly
|
||||
# root), and under keep-id:uid=33 the *container's* 33 is tanshu on the host.
|
||||
# Extracting as tanshu is therefore already the correct ownership, and
|
||||
# restoring the source's numeric owners would be wrong.
|
||||
set -o pipefail
|
||||
ssh monoco 'sudo tar -C /var/lib/nextcloud -cf - .' \
|
||||
| ssh emily 'tar --no-same-owner -C ~/podman-data/nextcloud -xf -'
|
||||
|
||||
# Without sudo, when step 0 proved everything is readable:
|
||||
# ssh monoco 'tar -C /var/lib/nextcloud -cf - .' \
|
||||
# | ssh emily 'tar --no-same-owner -C ~/podman-data/nextcloud -xf -'
|
||||
|
||||
# 3. Verify by content, over the whole tree. Read as root on the source side
|
||||
# because data/ is 0770.
|
||||
ssh monoco 'cd /var/lib/nextcloud && sudo bash -c "find . -type f -print0 | sort -z | xargs -0 -r sha256sum" | sha256sum'
|
||||
ssh emily 'cd ~/podman-data/nextcloud && find . -type f -print0 | sort -z | xargs -0 -r sha256sum | sha256sum'
|
||||
# identical digests = a byte-faithful copy
|
||||
|
||||
# 4. Nothing may end up owned by root or by a subuid.
|
||||
ssh emily 'find ~/podman-data/nextcloud ! -user tanshu' # empty = correct
|
||||
|
||||
# 5. A cheap sanity check that the two halves agree — compare with the count from
|
||||
# step 2, and with monoco's own tree.
|
||||
ssh emily 'find ~/podman-data/nextcloud/data -type f | wc -l'
|
||||
ssh monoco 'sudo find /var/lib/nextcloud/data -type f | wc -l'
|
||||
```
|
||||
|
||||
Ownership needs no `chown` pass: `keep-id:uid=33` maps the container's `www-data`
|
||||
onto the connecting user, so files extracted as `tanshu` are already what the
|
||||
container expects to own — and what plain `tar`, `rsync` and `rm -rf` keep working
|
||||
with.
|
||||
|
||||
If the link is slow or drops, do it in two hops instead: resumable, and it shows
|
||||
progress.
|
||||
|
||||
```bash
|
||||
rsync -aP --rsync-path='sudo rsync' monoco:/var/lib/nextcloud/ ~/nextcloud-copy/
|
||||
rsync -aP ~/nextcloud-copy/ emily:~/podman-data/nextcloud/
|
||||
```
|
||||
|
||||
### If you ran the playbook before copying
|
||||
|
||||
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: the image's own
|
||||
code rsynced in, `data/.ocdata` created, and no `config.php`, because there is no
|
||||
`NEXTCLOUD_ADMIN_USER` to install with.
|
||||
|
||||
That is not a usable instance, and worse, copying monoco's tree on top of it
|
||||
**merges two versions of the code** — the drift this page spends the rest of its
|
||||
time warning about, produced silently by a play that reported success. The role
|
||||
now warns when it finds a webroot with no `config/config.php`, but the recovery is
|
||||
to start from a clean webroot:
|
||||
|
||||
```bash
|
||||
ssh emily 'systemctl --user stop nextcloud.service
|
||||
rm -rf ~/podman-data/nextcloud'
|
||||
```
|
||||
|
||||
`rm -rf` needs no `podman unshare`: the role creates the webroot as the connection
|
||||
user, and under `keep-id:uid=33` everything the container writes lands on that same
|
||||
user. Then take the copy in step 3 and come back to step 4.
|
||||
|
||||
### 4. Deploy
|
||||
|
||||
```bash
|
||||
cd prepare-server && ansible-playbook -u tanshu ../nextcloud/playbook.yml
|
||||
```
|
||||
|
||||
Run it **after** the copy: the first start is when the entrypoint compares the
|
||||
copied `version.php` with the image and, if the image is newer, runs
|
||||
`occ upgrade`. Starting before the copy would initialise an empty instance and
|
||||
then be overwritten by it.
|
||||
|
||||
Rolling back means rolling back all three halves — `ssh monoco 'docker start nextcloud'`
|
||||
is only meaningful while monoco's own Postgres 17 cluster is intact, and it would
|
||||
then be writing to a database emily's copy has already moved past.
|
||||
`occ upgrade` sets maintenance mode for its duration, and the play **waits for it
|
||||
to clear** rather than asserting it is off — so a long upgrade is tolerated, and an
|
||||
instance left in maintenance mode (a backup that set `--on` and never finished) is
|
||||
reported with the one-line command to clear it. That distinction matters because
|
||||
`status.php` is exempt from maintenance mode: a site in that state looks healthy
|
||||
to a probe while returning `503` to every real request.
|
||||
|
||||
## Verification
|
||||
|
||||
@@ -565,28 +420,23 @@ systemctl --user list-timers nextcloud-cron.timer
|
||||
systemctl --user start nextcloud-cron.service # run one by hand
|
||||
journalctl --user -u nextcloud-cron -n 20
|
||||
|
||||
# The data really is the copied tree:
|
||||
ssh emily 'ls -ln ~/podman-data/nextcloud | head'
|
||||
ssh emily 'test -f ~/podman-data/nextcloud/config/config.php && echo "config.php present"'
|
||||
|
||||
# The database really is the migrated one — run the same query on both hosts.
|
||||
ssh emily 'podman exec -e PGPASSWORD=123456 postgres psql -U postgres -d nextcloud -tAc "select count(*) from oc_filecache"'
|
||||
ssh monoco 'docker exec -e PGPASSWORD=123456 postgres psql -U postgres -d nextcloud -tAc "select count(*) from oc_filecache"'
|
||||
# The database this play created, and its size:
|
||||
podman exec -e PGPASSWORD=123456 postgres psql -U postgres -l | grep nextcloud
|
||||
|
||||
# The site, end to end:
|
||||
curl -sSI https://cloud.hopsngrains.com
|
||||
```
|
||||
|
||||
Then open it **in a browser** and do two things a `curl` cannot: log in, and
|
||||
**upload a file**. Uploading is the check that matters here — it exercises the
|
||||
uid-33 mapping (a wrong `keep-id` shows up as a permissions error), the Redis
|
||||
file locking in `config.php`, and PHP sessions, which the image configures from
|
||||
**upload a file**. Uploading is the check that matters — it exercises the uid-33
|
||||
mapping (a wrong `keep-id` shows up as a permissions error), the Redis file
|
||||
locking in `config.php`, and PHP sessions, which the image configures from
|
||||
`REDIS_HOST`.
|
||||
|
||||
After the upload, confirm on the host that the new file is owned by `tanshu`:
|
||||
|
||||
```bash
|
||||
ssh emily 'find ~/podman-data/nextcloud/data -newermt "-10 minutes" -type f ! -user tanshu'
|
||||
find ~/podman-data/nextcloud/data -newermt "-10 minutes" -type f ! -user tanshu
|
||||
# empty = the write path lands on the right uid
|
||||
```
|
||||
|
||||
@@ -594,19 +444,6 @@ Then reboot the host and confirm both the container and the timer return
|
||||
unattended — without `loginctl enable-linger tanshu` neither will, and the failure
|
||||
is silent.
|
||||
|
||||
Finally, if `config.php` predates the proxy (it should not — monoco already served
|
||||
this site through Caddy, but check), Nextcloud needs the proxy trusted before it
|
||||
will believe `X-Forwarded-For`:
|
||||
|
||||
```bash
|
||||
podman exec -u www-data nextcloud php occ config:system:get trusted_proxies
|
||||
podman exec -u www-data nextcloud php occ config:system:get overwrite.cli.url
|
||||
```
|
||||
|
||||
An empty `trusted_proxies` means every client looks like the proxy; see
|
||||
[caddy/readme.md](../caddy/readme.md), "Client IPs", which also explains why the
|
||||
real address may not survive the rootless port forward at all.
|
||||
|
||||
### Debugging a unit that will not start
|
||||
|
||||
A unit that has hit systemd's start limit reports only a state, and `Restart=always`
|
||||
@@ -615,11 +452,11 @@ may already be gone — Quadlet passes `--rm`). The play collects all of this it
|
||||
when a start fails; by hand:
|
||||
|
||||
```bash
|
||||
ssh emily 'systemctl --user status nextcloud.service --no-pager -l | head -25
|
||||
systemctl --user cat nextcloud.service # what systemd is running
|
||||
journalctl --user -u nextcloud.service --no-pager -n 60
|
||||
podman ps -a --filter name=nextcloud
|
||||
podman logs --tail 40 nextcloud'
|
||||
systemctl --user status nextcloud.service --no-pager -l | head -25
|
||||
systemctl --user cat nextcloud.service # what systemd is running
|
||||
journalctl --user -u nextcloud.service --no-pager -n 60
|
||||
podman ps -a --filter name=nextcloud
|
||||
podman logs --tail 40 nextcloud
|
||||
```
|
||||
|
||||
Read it in this order:
|
||||
@@ -629,14 +466,16 @@ Read it in this order:
|
||||
above; anything about `db`/`redis` is the network attach; `downgrading is not
|
||||
supported` is version drift.
|
||||
2. **`podman ps -a`** — a container left behind under this name makes `podman run`
|
||||
refuse to start. Quadlet passes `--replace`, so this is rare, and it means an
|
||||
interrupted run.
|
||||
refuse to start.
|
||||
3. **`systemctl --user cat`** — the generated unit, if a drop-in or a `Network=`
|
||||
is in question (the usual failure is a `.network` file that does not exist).
|
||||
|
||||
The first real run of this playbook failed at a bare `is-active` check with no
|
||||
diagnostics at all, which is why the wait now lives inside the start block whose
|
||||
rescue collects the above.
|
||||
A `failed` unit needs its start-limit counter cleared before it will try again:
|
||||
|
||||
```bash
|
||||
systemctl --user reset-failed nextcloud.service
|
||||
systemctl --user start nextcloud.service
|
||||
```
|
||||
|
||||
## Notes and gotchas
|
||||
|
||||
@@ -645,79 +484,49 @@ rescue collects the above.
|
||||
crash is a `Permission denied` on `redis-session.ini`, the unit ends up `failed`
|
||||
via systemd's start limit, and nothing about it looks like a permissions
|
||||
problem. The unit mounts a host-owned file over that path; see
|
||||
["The PHP session file"](#the-php-session-file) above, including why a refused
|
||||
write is no longer fatal.
|
||||
- **Maintenance mode travels with `config.php`.** The backup plan recommends
|
||||
`occ maintenance:mode --on` before a consistent dump
|
||||
([docs/backup-plan.md §3.4](../docs/backup-plan.md)); if monoco was left that
|
||||
way, the copied configuration carries `maintenance => true` and the site returns
|
||||
`503` for everything except `status.php`. The play **waits for maintenance mode
|
||||
to clear** — it is on for the whole of a first-start `occ upgrade`, so it cannot
|
||||
simply assert — and if it never clears, fails with the one-line command to turn
|
||||
it off. That is deliberate: "the app is up but every page 503s" points anywhere
|
||||
but at maintenance mode.
|
||||
- **Version drift is the one irreversible step.** The copied `version.php` is what
|
||||
the entrypoint compares against the image. A newer image upgrades the instance
|
||||
on first start (and can only step one major version at a time); an older image
|
||||
makes the container exit with "downgrading is not supported". Check both before
|
||||
copying (step 0) and take a database dump before the first start.
|
||||
- **`latest` floats.** The Docker-era playbook used the unqualified
|
||||
`nextcloud:latest`; this one fully qualifies the same tag, because rootless
|
||||
Podman has no TTY to answer a short-name prompt when the unit starts. Neither
|
||||
pins a digest, so an image bump is a normal `podman pull` away — and it is
|
||||
exactly the event that triggers an in-place upgrade, because the code lives in
|
||||
the mount.
|
||||
- **The code lives in the data directory.** That is the image's design, not this
|
||||
playbook's, and it is why the whole tree is copied and why an image bump edits
|
||||
`~/podman-data/nextcloud`. It is also why the backup plan lists `apps/`, `core/`
|
||||
and `lib/` as regenerable: they are, but only from an image that is the same
|
||||
version or newer.
|
||||
- **`occ` must run as `www-data`.** It refuses to run as any other user, and under
|
||||
`keep-id:uid=33` that is the same user the host sees as `tanshu`. Every
|
||||
`podman exec` in this file therefore carries `-u www-data`; the plain form works
|
||||
for `getent`, which does not care.
|
||||
- **The init lock file.** `~/podman-data/nextcloud/nextcloud-init-sync.lock` is
|
||||
pre-created by the role so it stays owned by `tanshu`. If a run ever reports it
|
||||
as a subuid, the pre-create was skipped on a first start — delete it and re-run
|
||||
the playbook.
|
||||
- **Client IPs.** Nextcloud uses the client address for brute-force protection and
|
||||
for the `X-Forwarded-For` it logs. Whether the original address survives the
|
||||
rootless port forward depends on the forwarder. See
|
||||
[caddy/readme.md](../caddy/readme.md), "Client IPs", before relying on it.
|
||||
- **Registering restarts all of Caddy** (a brief restart of every hosted site),
|
||||
**attaching restarts Postgres** (a few seconds without a database for every
|
||||
attached app) and **restarts Valkey** (seconds without sessions or locks, though
|
||||
the keys themselves survive the graceful stop). See the trade-offs in
|
||||
[caddy/readme.md](../caddy/readme.md), [postgres/readme.md](../postgres/readme.md)
|
||||
and [valkey/readme.md](../valkey/readme.md).
|
||||
["The PHP session file"](#the-php-session-file).
|
||||
- **`activating` is not a healthy state.** A crash-looping container spends its
|
||||
time `activating` between restarts, so the play's wait retries only while the
|
||||
state can still change on its own and otherwise hands over to a rescue that has
|
||||
the logs. A bare `is-active` check would report either state as a failure with no
|
||||
explanation.
|
||||
- **A new drop-in restarts Postgres for everyone** (a few seconds without a
|
||||
database for every attached app), **registering restarts all of Caddy** (a brief
|
||||
restart of every hosted site) and **attaching restarts Valkey** (seconds without
|
||||
sessions or locks, though the keys survive the graceful stop). See the
|
||||
trade-offs in [postgres/readme.md](../postgres/readme.md),
|
||||
[caddy/readme.md](../caddy/readme.md) and [valkey/readme.md](../valkey/readme.md).
|
||||
- **If the Caddyfile does not exist yet**, this playbook creates it containing only
|
||||
the site block. That is a valid Caddyfile; the Caddy playbook's seeded
|
||||
global-options block is then not added, because its seed only fires on a missing
|
||||
or zero-length file. Deploy Caddy first on a fresh host.
|
||||
- **DNS.** `cloud.hopsngrains.com` must resolve to `emily` before Caddy can obtain
|
||||
a certificate for it; the old certificates live in `~/podman-data/caddy/data`
|
||||
and are reused if the hostname is unchanged.
|
||||
- **The backup script still assumes Docker.** `~/bin/nextcloud-backup.sh` uses
|
||||
`--link postgres:db`, which Podman does not have, and its `sudo tar` /
|
||||
`chown $(logname)` gymnastics are unnecessary now that the data is owned by you.
|
||||
It needs the `podman exec` form from
|
||||
[postgres/readme.md](../postgres/readme.md), "Backups"; the database dump should
|
||||
also be taken with `occ maintenance:mode --on` around it, as
|
||||
[docs/backup-plan.md §3.4](../docs/backup-plan.md) says.
|
||||
- **`~/bin/empty-trash.sh` targets a container named `cloud`**, which never
|
||||
existed on this host under that name — it is `nextcloud` — and it runs as root,
|
||||
which cannot `podman exec` into these containers at all.
|
||||
- **Do not restart the container on monoco.** It would write to monoco's old
|
||||
cluster, which is being kept intact purely as a rollback.
|
||||
- **Client IPs.** Nextcloud uses the client address for brute-force protection and
|
||||
for the `X-Forwarded-For` it logs, so the reverse proxy has to be trusted — see
|
||||
[caddy/readme.md](../caddy/readme.md), "Client IPs", which also covers whether
|
||||
the original address survives the rootless port forward at all.
|
||||
- **The webroot holds the code.** That is the image's design, not this playbook's:
|
||||
an image bump edits `~/podman-data/nextcloud`. It is also why `apps/`, `core/`
|
||||
and `lib/` are listed as regenerable above — they are, but only from an image
|
||||
that is the same version or newer.
|
||||
- **`occ` must run as `www-data`.** It refuses to run as any other user, and under
|
||||
`keep-id:uid=33` that is the same user the host sees as `tanshu`. Every
|
||||
`podman exec` in this file therefore carries `-u www-data`; the plain form works
|
||||
for `getent`, which does not care.
|
||||
- **The backup scripts are not this playbook's business.** Whatever dumps the
|
||||
database and archives the webroot has to run as a **systemd user timer** next to
|
||||
the Quadlet units: root cannot `podman exec` into another user's rootless
|
||||
containers. Dump in place rather than through a helper container — `--link` does
|
||||
not exist in Podman:
|
||||
|
||||
```bash
|
||||
podman exec -e PGPASSWORD="..." postgres pg_dump -U postgres nextcloud | bzip2 > nextcloud.sql.bz2
|
||||
```
|
||||
|
||||
## Not done here
|
||||
|
||||
- Rotating the secrets that are committed in plaintext — `pg_password` is the
|
||||
fleet's shared `123456`, and it lives in `nextcloud.env` at mode `0600` rather
|
||||
than in the world-readable unit file.
|
||||
- Switching the background-job mode to `cron` (`occ background:cron`), which the
|
||||
Docker-era deployment also never did.
|
||||
- Replacing the broken `~/bin/nextcloud-backup.sh` with a systemd user timer, and
|
||||
fixing `empty-trash.sh`.
|
||||
- Turning on ongoing monitoring — the `status.php` probe in the role is where a
|
||||
timer or an external check would hook in.
|
||||
- Switching the background-job mode to `cron` (`occ background:cron`).
|
||||
- Pinning the image to a digest, so that a `latest` bump is always deliberate.
|
||||
- A backup timer for the webroot and the database.
|
||||
Reference in new issue
Block a user