Commit Graph
60 Commits
Author SHA1 Message Date
tanshu 7e3cc26d4b Say what a Valkey recreate actually costs, in the handler comment
The comment claimed every attached app "sees a cold cache". That is not what
happens. RDB persistence is on by default, the recreate is a graceful stop, and
valkey-server writes a final dump.rdb on SIGTERM that the new container loads
back off the bind mount -- so keys survive, which is the part the readme already
gets right and this comment did not.

What does not survive is availability for the seconds the container is down:
Nextcloud keeps its PHP sessions and memcache.locking there, and Yamtrack uses it
as its Celery broker, so those seconds are seconds in which logins and queued
tasks are unreachable. Only a hard kill or a power loss costs changes since the
last save point. That is the warning a future reader needs, and it is what the
nextcloud playbook's own handler comment now points at.
2026-10-16 08:44:20 +05:30
tanshu 43ea43e88b 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.
2026-10-16 08:44:14 +05:30
tanshu 3587bc9001 Run Valkey as a rootless Podman Quadlet
Replaces the docker_container playbook, which published 127.0.0.1:6379 and kept
its /data in an anonymous Docker volume -- the one the audit flagged because no
backup script can see it (docs/podman-compat-audit.md 3.1). Valkey becomes the
base Quadlet unit, the same shape as Postgres: it knows nothing about its
clients, and a service attaches itself by dropping a file into
valkey.container.d/ via valkey/roles/attach. Nothing on the host needs 6379, so
there is no PublishPort at all rather than a narrower one.

The name stays `valkey` for the directory, container, unit and Podman network,
and only the alias clients connect on is `redis`: Nextcloud's config.php
(written by the image from REDIS_HOST=redis), outline and overleaf reach it as
`redis`, while paperless-ngx and yamtrack use the container name. Renaming would
touch every REDIS_HOST for no gain.

The uid/gid mapping is the detail worth spelling out, because the audit only
records the uid. The Alpine image's `valkey` account is 999:1000, not 999:999 --
Alpine already has a gid 999, and the Dockerfile says so before creating the
group with the next id. 999 is the uid the anonymous volume was owned by, so the
uid matches what was already on disk, but mapping only the uid would leave every
file valkey-server writes, dump.rdb included, group-owned by a subgid. Both ids
map onto the connection user, so ~/podman-data/valkey stays tanshu:tanshu.

Persistence stays on deliberately, and the mount with it. The contents are
disposable -- nothing is copied off monoco, and a cold start is survivable -- but
this is not purely a cache: Nextcloud keeps its PHP sessions and
memcache.locking in it, and Yamtrack uses it as its Celery broker. With RDB on, a
rebuild is a graceful stop that writes a final snapshot on SIGTERM that the next
container loads, so a redeploy does not log every Nextcloud user out or discard
queued tasks. Tmpfs=/data with `save ""` would remove the mount and the whole
keep-id mapping with it, and is the right shape for a pure cache, but it would
make every redeploy exactly that.

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.

Two ordering details are load-bearing:

- The attach include asserts that <name>.network exists before writing a drop-in
  that names it. Quadlet rejects the whole valkey unit, with an error that never
  mentions the drop-in, when a referenced .network file is missing.
- A service that attaches flushes handlers before starting the app. The restart
  that joins Valkey to the new network is a handler, so by default it runs at the
  end of the play, after the app has already come up and failed to resolve
  `redis`. nextcloud/roles/valkey is the working example.

The play waits on `valkey-cli ping` inside the container rather than trusting
systemctl is-active. Restart=always reports active even while the container
crash-loops, which is exactly what a wrong keep-id looks like.
2026-10-16 08:26:25 +05:30
tanshu 9bf2cbd7a9 Run Bitwarden as a rootless Podman Quadlet
Replaces the docker_container role on monoco with a Quadlet systemd user unit on
emily, and moves state from /var/lib/bitwarden to ~/podman-data/bitwarden.
Postgres is authoritative for the vault and already moved with the cluster
restore, so only the file data comes across -- and the stale db.sqlite3 that has
sat beside it for years is deliberately excluded from that copy. The role warns
if it finds the file in the data directory rather than failing, because with
DATABASE_URL set it is inert but looks exactly like a restore source.

The image runs as container-root and writes /data as root, so
UserNS=keep-id:uid=0,gid=0 maps that onto the connecting user and leaves nothing
owned by a subuid.

Both integrations go through the shared includes rather than local copies --
caddy/roles/register for the network and the Caddy drop-in, postgres/roles/attach
for the database -- so this app writes the same drop-in bytes as every other
service and a new service never means editing Caddy or Postgres. That is also
what lets the Docker-era roles/network go: it existed to attach an
already-created container to a network (docker_network's connected:/appends:),
which podman_network has no equivalent for. The dead files/nginx.conf.j2 goes
with it -- Caddy handles the WebSocket upgrade and imposes no request-body limit,
so its 525M client_max_body_size needs no replacement.

Three ordering details are load-bearing:

- The register include runs in pre_tasks, before the unit is started. Quadlet
  fails unit *generation*, not merely startup, when a referenced .network file
  does not exist yet, so starting first reports "unit not found".
- roles/postgres flushes handlers immediately after attaching. The restart that
  joins Postgres to this network is a handler, so by default it runs at the end
  of the play, after the app has already started and failed to resolve the
  database.
- The play waits on the image's own health check (podman healthcheck run) rather
  than on systemctl is-active. Vaultwarden runs its Diesel migrations before
  Rocket answers, and Restart=always means even a crash-looping container is
  reported active.

DATABASE_URL and DOMAIN move into a 0600 environment file read through
EnvironmentFile=, since the Quadlet unit is world-readable and DATABASE_URL
carries the password. The credentials are unchanged, so the restored database
connects with no reset. readme.md carries the monoco -> emily file copy command
by command.
2026-10-16 07:20:52 +05:30
tanshu 4cd9308ae3 Run Tandoor as a rootless Podman Quadlet
Replaces the docker_container role with a Quadlet systemd user unit, and moves
state from /var/lib/tandoor to ~/podman-data/tandoor. Tandoor's entrypoint
starts as root and drops privileges, but root is what ends up owning the data,
so keep-id maps container root onto the connecting user and everything stays
owned by tanshu rather than a subuid.

The play does not run the rootless-Podman prerequisites: they are the shared
task file at caddy/roles/podman, and a host that has run caddy/playbook.yml has
them. Both integrations go through the shared includes rather than local copies
-- caddy/roles/register for the network and the Caddy drop-in, postgres/roles/
attach for the database -- so this app writes the same drop-in bytes as every
other service and a new service never means editing Caddy or Postgres.

Three ordering details are load-bearing:

- The register include runs in pre_tasks, before the unit is started. Quadlet
  fails unit *generation*, not merely startup, when a referenced .network file
  does not exist yet, so starting first reports "unit not found".
- roles/postgres flushes handlers immediately after attaching. The restart that
  joins Postgres to this network is a handler, so by default it runs at the end
  of the play -- after the app has already started and failed to resolve `db`.
- The role waits for a real HTTP response from inside the container instead of
  trusting systemctl is-active. The entrypoint runs Django migrations before
  gunicorn answers, and until it does the image's own nginx returns 502, which
  Caddy relays verbatim -- complete with `server: nginx`, which reads as a stray
  reverse proxy that is not actually there.

Also drops the dead nginx.conf.j2 and the network role, both superseded by the
shared includes, and moves .env into the role's templates/ as tandoor.env.j2,
rendered at mode 0600 and read through EnvironmentFile=.
2026-10-16 06:52:27 +05:30
tanshu 4297e5a71e Run Postgres as a rootless Podman Quadlet, and move the 17 cluster across
Replaces the docker_container playbook, which published 5432 on 0.0.0.0 and kept
its data in /var/lib/postgresql. Postgres is now the base Quadlet unit, the same
shape as Caddy: it knows nothing about its clients, and a service attaches itself
by dropping a file into postgres.container.d/ via postgres/roles/attach. Nothing
here changes when a service is added or removed.

Pinned to 18, which is two decisions rather than one tag. Postgres 18 moved both
the image's VOLUME (/var/lib/postgresql/data -> /var/lib/postgresql) and its
default PGDATA (/var/lib/postgresql/data/pgdata -> /var/lib/postgresql/18/docker),
and the 18 entrypoint refuses to start when it finds a cluster in the old layout.
So postgres_major is the single knob: it picks the image tag and the PGDATA path
together, and the mount is now the parent directory. That also settles the
migration: a PGDATA copy cannot cross a major version, so monoco's 17.5 cluster
comes across as a pg_dumpall.

Three things that bit us and are worth keeping:

- PGDATA and its version directory are created by the role, not left to the
  container. The entrypoint runs as container-root, which under keep-id is a
  subuid on the host, and it only chowns PGDATA and its contents -- so
  ~/podman-data/postgres/18 would have been stranded subuid-owned, and the host
  user could not even rm -rf it without podman unshare.

- NetworkAlias= applies to every network the container joins, which is what a
  database hub wants, but it is one fixed value in a shared drop-in rather than
  per-service: two services writing their own would make Quadlet pass
  --network-alias twice. It is not in postgres.container itself because on a
  fresh host that unit has no Network= at all.

- The credentials live in a 0600 file read with EnvironmentFile=, not in
  Environment= lines: the unit is world-readable in ~/.config.

The play no longer uses become, and does not apply the rootless-Podman
prerequisites -- those are the shared task file at caddy/roles/podman, and the
three variables they need are duplicated at the top of vars/default.yml.

migrate-from-monoco.md records the one-time 17.5 -> 18.6 move: the dump, transfer
and restore commands, the checks that prove the restore is complete (per-database
table counts against the source), and the verified result.

The dead nginx/php-fpm templates in postgres/files/ are removed.
2026-10-16 06:52:27 +05:30
tanshu 060173db5e Run Caddy as a rootless Podman Quadlet
Replaces the docker_container role with Quadlet systemd user units under
~/.config/containers/systemd, and moves state from /var/lib/caddy to
~/podman-data/caddy.

Caddy is now the base unit and no longer lists its upstream networks. Each
service registers itself by writing its own <name>.network unit plus a drop-in
into caddy.container.d/, so adding or removing a service never means editing the
Caddy playbook. Drop-ins are used rather than blockinfile because this playbook
re-templates caddy.container on every run, which would wipe appended blocks.

The play runs as the connecting user instead of using become: the rootless units
belong to it, so the connection's PAM session is what makes systemctl --user
work without setting XDG_RUNTIME_DIR. podman_user and the data directory are
derived from facts, so the play fails loudly if that resolves to root.

Two things that bit us and are worth keeping:

- The privileged-port sysctl is applied inline with sysctl -w, not via a
  handler. Handlers only run at the end of the play, by which point Caddy has
  already failed to bind 80/443.
- The Caddyfile is seeded with a minimal valid config. Caddy exits 1 with a
  bare "EOF" on an empty config file, so touching it into existence made the
  service crash-loop.
2026-10-15 22:12:29 +05:30
tanshu 21f848340e Removed nginx menus 2026-10-15 16:07:01 +00:00
tanshu 9af32ef4e0 Removed MariaDB 2026-10-15 16:06:49 +00:00
tanshu f5d43d744d Removed broken setup ubuntu playbook 2026-10-15 16:04:41 +00:00
tanshu 97d6892c83 Add the prepare-server playbook
Two plays: bootstrap as the provider account (ubuntu, over bootstrap.pem), then
everything else as the user that first play just created. The split is forced
rather than stylistic -- the second play logs in as that user, so its
authorized_keys entry and its NOPASSWD sudoers entry both have to exist before
it starts. That is why the key tasks live in the 'user' role and 'wheel' runs
in the first play: move either into play 2 and it fails with an auth error or
"Missing sudo password".

Both accounts and both keys are variables. The inventory sits beside the
playbook and is read via ansible.cfg, which Ansible picks up from the current
working directory rather than the playbook's, so it has to be run from this
directory. bootstrap.pem is gitignored -- it is the provider key for the host.
2026-10-15 21:24:32 +05:30
tanshu f2d1b606ce Move the menu-admin deployment into the menu-admin repo
It lives beside the app it deploys now, with podman building the image and an
inventory carrying one entry per Site (which is what the local 'hosts: monoco'
edit was reaching for). Nothing in this repo referenced it.
2026-09-22 23:19:13 +05:30
tanshu bf824cae26 Wait on the container's own health, not the host's DNS
The app's health check ran on the target host against the container name, which
only resolves inside the Docker network, so the wait could never succeed. Poll
the image's HEALTHCHECK instead and fail with a readable message. Also pin
site.env to the app user when it exists, and say how to install the provisioning
script on the server.
2026-09-22 21:37:01 +05:30
tanshu c87cd15c8c Add the menu-admin playbook
Deploys the menu app for one Site: pulls the tag, creates the data root, runs
the container against a config baked into the image, waits for health, verifies
the data directory, and points Caddy at the app with a marker it can update in
place. The parallel run uses a temporary hostname; cutover sets the retire
flags.
2026-09-22 21:27:13 +05:30
tanshu e54dd127ed Track the nginx-menu playbook as it stands
It has been serving three menu Sites from an untracked directory, so today's
state was not reproducible or revertible.
2026-09-22 21:27:13 +05:30
tanshu b7c5997274 Tandoor updated. 2026-09-21 14:09:22 +00:00
tanshu fabe4bfcfb Added nats 2026-08-29 12:31:09 +00:00
tanshu 399f5c7a6a Gitea updated for the right host and to work with caddy. 2026-08-27 02:46:52 +00:00
tanshu b727fa7c00 Updated nextcloud to use docker network and caddy 2026-02-27 08:17:18 +00:00
tanshu 5b8d77c839 Added caddy docker 2025-07-31 11:14:47 +00:00
tanshu 8a6744a1b1 Moved from redis to valkey 2025-07-31 11:12:18 +00:00
tanshu 5d276dd182 Upgraded postgres to 17 2025-07-31 11:11:27 +00:00
tanshu dc2e11ca69 Registry updated to v3 and from nginx to caddy 2025-07-30 04:17:32 +00:00
tanshu e429ce661e Updated bitwarden to work with caddy 2025-07-30 04:12:29 +00:00
tanshu 3ff953acdb IOT Playbook updated with docker networks and caddy. 2025-07-09 01:16:10 +00:00
tanshu 7c2a66540f Removed Wordpress 2024-12-17 17:53:53 +05:30
tanshu f04a29b5c7 Docker registry 2024-12-17 17:53:30 +05:30
tanshu 2f767a1826 Hugo 2024-12-17 17:52:46 +05:30
tanshu ddf6b5a54c Postgres:
Pinned a version so that watchdog does not upgrade versions.
Made the host directory configurable
2024-09-19 06:51:53 +05:30
tanshu b877098a01 Added: Cyberchef 2024-09-19 06:49:59 +05:30
tanshu 376255e15d Added: paperless-ngx 2024-09-19 06:49:39 +05:30
tanshu 230a360ad7 Added Stirling-PDF 2024-05-27 13:25:44 +05:30
tanshu ceb19235d5 Updated bitwarden to v1.30 it no longer needs to redirect to port 3012 for websockets 2024-02-16 20:19:27 +05:30
tanshu 008ac0d198 Nextcloud broken into roles.
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.
2024-01-23 22:15:11 +05:30
tanshu e26809cec9 Cleanup 2024-01-23 21:16:42 +05:30
tanshu 18fafbb2f1 Moved bitwarden to role based playbook.
Restricted it to gondor host.
2024-01-23 21:15:54 +05:30
tanshu 790ee8b37b Moved snibox to role based playbook.
Restricted it to gondor host.
Moved back to postgres on the same host. This does away with the etc_hosts link, but kept the variable and playbook entry as a template if and when needed.
2024-01-23 21:09:22 +05:30
tanshu 5736e305e4 Gitea broken into roles for better management.
Changed hosts to gondor so that limiting is not necessary
2024-01-22 14:41:55 +05:30
tanshu aa589960b1 Updated the nginx.conf 2023-08-06 09:20:57 +05:30
tanshu 3bbacab487 Broke apart the playbook into roles. 2023-07-14 12:57:35 +05:30
tanshu 7744dd0520 Readme 2023-07-14 10:09:28 +05:30
tanshu 3d4ea2770c Allowed anonymous viewer in Grafana 2023-07-14 08:47:31 +05:30
tanshu d5115c2d58 Fix: Grafana url 2023-07-13 15:05:00 +05:30
tanshu 774b69be38 Add: IOT using Mosquitto // Node red // InfluxDB // Grafana
File and folder permissions are a bitch.
Also, will only leave access to grafana and remove the rest to be accessed using ssh tunnels
2023-07-13 14:32:17 +05:30
tanshu ae6fde8113 Added: wikijs 2023-01-02 12:23:38 +05:30
tanshu 67750b459d Fix: Right permission for the host directory of wordpress 2023-01-02 12:23:26 +05:30
tanshu 77d5fa8eda Added: Tandoor for recipes 2023-01-02 12:23:09 +05:30
tanshu 4cfffeaebd Added Mariadb and wordpress 2022-08-02 06:41:22 +05:30
tanshu 5713d3d410 Updates 2022-08-02 06:40:29 +05:30
tanshu a2e773b3ab Removed: brewman as it is moved into the main project 2022-06-07 17:43:12 +05:30
tanshu f26261fb99 Updated the gitea playbook for ssh passthrough. Still not fully there though. 2021-08-24 09:14:15 +05:30
tanshu 1189c478d6 Redis will only listen on the 127.0.0.1 and not remote connections. 2021-08-18 09:18:30 +05:30
tanshu fbd10208a8 Updated bitwarden 2021-08-18 09:17:10 +05:30
tanshu c6226d8204 Removed barker and moved it the main project 2021-08-18 09:17:00 +05:30
tanshu 499c67a336 New:
Grav
2020-10-17 12:29:51 +05:30
tanshu 60aed543d3 Feat:
Update gitea, otherwise it will not upgrade when new image is available
2020-10-17 12:29:19 +05:30
tanshu 5922227712 New:
Snibox
2020-10-17 12:28:52 +05:30
tanshu 5c22100376 New:
Rabbit MQ - needed for barker
2020-10-17 12:28:36 +05:30
tanshu 426bd8e82a Fix:
Postgres as supplying wrong env variable
2020-10-17 12:28:18 +05:30
tanshu ed8feff302 New:
Fastapi Brewman
  Fastapi Barker
2020-10-17 12:27:59 +05:30