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.
126 lines
6.1 KiB
YAML
126 lines
6.1 KiB
YAML
---
|
|
# ---------------------------------------------------------------------------
|
|
# 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
|
|
|
|
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: 33 is www-data, which owns the webroot, because
|
|
# the entrypoint rsyncs the code in with --chown www-data:www-data and Apache runs
|
|
# its workers as that account. `docker exec nextcloud id -u` reported 0 on the old
|
|
# host, because the entrypoint drops privileges after starting — do not derive the
|
|
# mapping from that number (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
|
|
|
|
# PHP's session-handler config, and the one path in the image this deployment has
|
|
# to write to. The entrypoint generates the file from REDIS_HOST on every start,
|
|
# but its target directory (`/usr/local/etc/php/conf.d`) is created by root in the
|
|
# php base image with no chown, so the uid this container runs as cannot create a
|
|
# file inside it. Mounting a host-owned file over that exact path is what gives
|
|
# the write somewhere it is allowed to land, and the template below keeps the
|
|
# image's own content in it so a refused write is not fatal either. See readme.md,
|
|
# "The PHP session file".
|
|
php_conf_dir: "{{ host_directory }}/php-conf.d"
|
|
php_session_ini: "{{ php_conf_dir }}/redis-session.ini"
|
|
php_session_ini_mount: /usr/local/etc/php/conf.d/redis-session.ini
|
|
|
|
# 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 }}:{{ container_port }}
|
|
|
|
header {
|
|
Strict-Transport-Security "max-age=15552000; includeSubDomains; preload"
|
|
}
|
|
}
|