--- # --------------------------------------------------------------------------- # 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 # --------------------------------------------------------------------------- # The one site-specific value: the hostname Caddy serves this instance on and the # hostname that goes into trusted_domains when the install is unattended. 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. image: docker.io/library/nextcloud:latest # The Quadlet file is .network. caddy/roles/register pins the # Podman network name to _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 file live under it. 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` reports 0 in the # upstream image, 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 the entrypoint has # to write to. It 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 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 database this instance uses, and the credentials for it. They have to match # whatever the shared Postgres unit was initialised with (postgres/vars/default.yml) # — this play attaches to a database, it does not create one, so the database has # to exist before the first run. pg_user: postgres pg_password: '123456' pg_database: nextcloud # `db` is the alias postgres/roles/attach adds to the shared Postgres unit, and # the host this instance's config.php connects to. 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" # --------------------------------------------------------------------------- # First-run install (optional) # --------------------------------------------------------------------------- # Leave both empty to install through the web wizard at https://{{ host }} — the # play reports that this is what is left to do and succeeds. Set both to have the # image install unattended on a fresh webroot instead: it runs # `occ maintenance:install` with these credentials and sets trusted_domains to # `host` in the same pass, so the instance is usable immediately. # # They are only consumed while there is no config.php, so they are inert on an # existing instance, and the environment template omits them entirely when they # are empty (an empty NEXTCLOUD_ADMIN_USER would be treated as a value and fail # the install). nextcloud_admin_user: "" nextcloud_admin_password: "" # --------------------------------------------------------------------------- # Background jobs # --------------------------------------------------------------------------- # Nextcloud wants `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. A systemd user timer next to the Quadlet # units is the equivalent. cron_unit: "{{ container_name }}-cron" cron_schedule: "*:0/5" user_unit_dir: "{{ ansible_facts['user_dir'] }}/.config/systemd/user" # The site block. Caddy terminates TLS, and Nextcloud asks for HSTS. There is no # request-body limit to carry over: Caddy streams bodies and imposes none by # default. snippet_block: | {{ host }} { reverse_proxy {{ container_name }}:{{ container_port }} header { Strict-Transport-Security "max-age=15552000; includeSubDomains; preload" } }