Reverts the explicit subnets: the pools were freed by deleting five networks, so
the network role is back to its single idempotent task - ensure the network
exists and Caddy is on it - and the Sites no longer declare a subnet. The
deployment readme keeps the note that a new Site needs a config, and therefore a
new tag, but no longer asks for a free /24.
If the pools fill up again the failure is recognisable: 'all predefined address
pools have been fully subnetted', and the choices are deleting stale networks or
declaring a subnet per Site.
The deploy failed on hngmohali with 'all predefined address pools have been
fully subnetted': every 172.17-172.30/16 is in use, 172.31/16 is the host's own
VPC so Docker will not allocate from it, and all sixteen 192.168.0-240/20 blocks
are taken. Docker therefore refuses a network created without a subnet.
Each Site now names a free network_subnet (10.210.0.0/16, one /24 each), the role
looks the network up first and only creates it when it is absent and with that
subnet, and it fails with an explanation when a Site declares none. Existing
networks are left alone, because Docker cannot change a subnet in place and the
Sites already running have theirs. The test checks every Site declares one, and
the deployment readme says a new Site needs a free subnet.
deploy.sh now deploys every Site by default, with SITE=<name> to narrow it, which
is what the local edit was reaching for.
One image serves every Site, so a new Site is three small files: an inventory
entry, a vars file naming its web address and data root, and a config that seeds
its theme and first category list (baked into the image, which is why a new Site
needs a new tag).
Both reuse the directory their old static menus were served from, and the caddy
role drops the snippet the retired nginx-menu playbook wrote for each hostname,
so a deploy repoints each address at its own container with nothing else to do.
The seeded category lists are a starting point, not a decision: categories, names
and icons are all editable in each Site's own admin. A test now checks the
inventory, config and vars agree for every Site and that no config is orphaned,
because a half-added Site was the obvious way to get this wrong.
The script refused anything under ten characters. That is a policy the owner
should hold, not one the tooling invents; the only rule left is that the password
cannot be empty, since an empty one would let anyone in. Resetting is the same
script with --force, which overwrites the hash and generates a fresh session
secret, signing out anyone who was signed in - now written down in the operating
notes and the deployment readme, including the form that prompts on your own
machine instead of the server.
podman builds an OCI image by default and drops Docker's HEALTHCHECK instruction,
so the container had no health at all and the play failed waiting for one. The
check is now declared on the docker_container task - node is in the image and the
exec form needs no shell quoting - which is both what operators see in docker ps
and what an orchestrator would use.
The wait still asks the app directly rather than reading State.Health, because a
container that already exists is only rebuilt when the image tag changes or when
-e recreate_container=true is passed, and a wait must not depend on a container
attribute an existing one may not have.
The first real deploy left the site on 502 and this is why. podman builds an OCI
image by default and drops the Docker HEALTHCHECK instruction, so the app
container has no State.Health at all; the role waited on that field, exhausted
its retries and failed the play, so the caddy role never ran and Caddy kept
proxying menu.hopsngrains.com to the old nginx container that is now gone.
The wait now asks the app itself: docker exec node -e fetch /healthz, which needs
no healthcheck and no DNS. And because the address may still be claimed by the
snippet the retired nginx-menu playbook wrote, the caddy role drops that marker
before writing its own - two blocks for one address make Caddy refuse the config,
and a refused reload quietly leaves the old proxy in place.
check-site also asked for the font the old design used, so it would have failed a
healthy site; it checks one we ship.
That was the intent of the uncommitted edit, and it is a better location than a
second directory: the app inherits the place the menus were already served from
and only ever reads and writes its own data/ and site.env inside it.
app_root is written as /var/lib/{{ host }}, quoted, because the unquoted form is
what failed to parse. Every documented path follows, and the deployment readme
now says that emptying the old menu files means deleting those files rather than
the directory, which holds the live content.
The deploy failed on 'YAML parsing failed: missing quotes around a template
block'. vars/hopsngrains.yml had been edited to derive its names from the web
address, but the two derived values were unquoted and YAML reads a leading brace
as a flow mapping, so the file did not parse at all.
The names now derive from a title (the domain) with every interpolation quoted,
which is what nginx-menu did and what makes the file copyable for another Site:
host menu.<title>, container menu-app.<title>, network <container>_net, data
/var/lib/menu-app.<title>. Resolving the vars offline gives exactly the values
the docs promise, including /app/config/hopsngrains.site.json inside the image.
The old edit derived from host, so app_root would have been
/var/lib/menu.hopsngrains.com — the directory the old static menus were served
from.
There is no previous system kept serving, so the playbook no longer knows about
one: the retire flags, the temporary web address, the task that removed the old
nginx snippet and the task that stopped the old container are gone. One run
starts the container and points the Site's web address at it, and deploying an
earlier tag is the rollback. The vars lose vhost, retire_nginx_snippet,
retire_nginx_container and legacy_container; the Caddy role keeps only the
snippet upsert.
The deployment readme drops its staged-rollout sections and the glossary of words
that no longer exist, and the plan's sequence and the operating notes describe
the single run instead.
The deployment readme used three pieces of jargon without defining any of them,
which reads as a locked door to anyone doing their first deploy. It now opens
with what each word means, the table of which address serves what at each stage,
and the note that skipping the parallel run is a legitimate choice.
Domain terms stay in CONTEXT.md; these are operational words for this directory,
so they are defined here.
The playbook, its roles, its vars and a new inventory now live in ansible/,
beside the app they deploy, the way the other apps on this machine are laid out.
The inventory follows the same shape: one entry per Site, all on the same box,
each naming its own vars file through var_file, so adding a Site is one line and
a file rather than a command-line flag. ansible_host is written as the
resolvable name rather than an ssh alias so the inventory works on any machine.
The image is now built with podman as a multi-arch manifest and pushed to
registry.tanshu.com/menu-admin, mirroring the Makefile used elsewhere on this
machine: build-production, plus build-check and build-check-local for a build
that does not push. deploy.sh runs the checks, builds, pushes, then runs the
playbook for the Site named by SITE, with SKIP_DEPLOY for a push without a
deploy.
ESLint comes out again: this project's typescript@7 has no JavaScript compiler
API, so typescript-eslint cannot install, and linting only the JavaScript was
not worth keeping. Prettier stays, and the README records why ESLint is absent
and what would bring it back.