Model the build on mozimo and target emily

- Makefile: PLATFORMS, tags :<commit> and :main, build-check with --pull.
- deploy.sh: push, guards, git pull --ff-only, optional --prune, image_tag.
- Replace .containerignore with .dockerignore.
- Add the opt-in image prune to the hops role.
- Tag the Caddyfile task so --skip-tags caddy works before DNS cutover.
- Trim comments and rewrite build and deploy text in plain prose.
This commit is contained in:
tanshu committed 2026-10-20 17:47:49 +05:30
1 parent 7366596a87
commit a7c9324170
14 files changed
+194 -357

No files matched your search

+55 -55
View File
@@ -4,7 +4,7 @@ A modern, full-stack craft microbrewery web application built with **TanStack St
---
## Architecture Overview
## Architecture overview
```
hops/
@@ -36,7 +36,7 @@ hops/
│ └── package.json
├── ansible/ # Deployment playbook (roles: register, hops, caddy)
├── Dockerfile # Multi-stage production container build
├── .containerignore # Build-context excludes (Podman/Buildah)
├── .dockerignore # Build-context excludes (podman and Docker)
├── .gitignore # Git repository ignore rules
├── docker-entrypoint.sh # Process supervisor running PB & Nitro
├── Makefile # Podman build targets (build-production / build-check)
@@ -47,9 +47,9 @@ hops/
---
## Running Locally
## Running locally
### 1. Start Both Frontend & Backend Concurrently
### 1. Start the frontend and backend together
```bash
npm run dev
```
@@ -79,32 +79,36 @@ npm run dev:frontend
## Deploying
The image is built with **Podman/Buildah** (not Docker) from the committed tree
(`git archive HEAD`) and tagged with its **commit id**, so what runs on the host
is exactly what git knows about. `deploy.sh` pushes the commit first, refuses a
dirty tracked tree and requires `HEAD == origin/main`. Log in to the registry
once per machine: `podman login registry.tanshu.com`.
`deploy.sh` pushes the current branch, refuses a dirty tracked tree, and
requires `HEAD == origin/main`. It builds that commit with podman and runs the
Ansible play on emily. Log in to the registry once per machine:
`podman login registry.tanshu.com`.
```bash
./deploy.sh # push, build <commit>, deploy it to emily
./deploy.sh --no-push # skip the push (you already pushed)
./deploy.sh --force # skip the guards
./deploy.sh # deploy HEAD
./deploy.sh --prune # also drop old images on emily, keep 3
./deploy.sh --force # skip the guards
```
Or run the pieces yourself:
The image is tagged with its commit id and `:main`. Rollback runs the same play
with an older `image_tag`:
```bash
make build-production # podman build HEAD, push :<commit> + :latest
make build-check # compile check from HEAD, no push
make build-production # podman build HEAD, push :<commit> and :main
make build-check # build from HEAD, push nothing
cd ansible && ansible-playbook -u tanshu playbook.yml -e "image_tag=$(git rev-parse --short HEAD)"
cd ansible && ansible-playbook playbook.yml -e "image_tag=$(git rev-parse --short HEAD)"
```
The image is built for **linux/arm64** by default because `emily` is aarch64.
Podman has no built-in QEMU, so cross-building from an x86_64 controller needs
binfmt there (`qemu-user-binfmt`); override with `PLATFORM=linux/amd64`.
The image builds for `linux/arm64` by default because emily is aarch64. Podman
has no built-in QEMU, so cross-building from an x86_64 controller needs binfmt
there (`qemu-user-static`). Override with `PLATFORMS=linux/amd64`:
To run the production image locally instead:
```bash
make build-production PLATFORMS=linux/amd64
```
To run the production image locally:
```bash
podman build -t localhost/hops:dev .
@@ -115,55 +119,51 @@ podman run --rm -p 3000:3000 -p 8090:8090 \
### Ansible deployment
Requirements on the controller: `ansible` (no collections — the play uses
built-in modules plus the `podman` / `systemctl --user` CLI). The target host
must already run the fleet's shared Caddy **rootless Quadlet** setup: the
`caddy/playbook.yml` from the `ansible-playbooks` repo owns the Caddy unit, the
rootless prerequisites (Podman, `loginctl enable-linger`, the low-port sysctl)
and the Caddyfile this play appends to.
The controller needs `ansible`. The target host needs the shared Caddy rootless
Quadlet setup from the `ansible-playbooks` repo. That playbook owns the Caddy
unit, the rootless prerequisites, and the Caddyfile this play appends to.
The play targets the inventory host `emily` (present in `/etc/ansible/hosts`),
connects as the unprivileged container user and **does not `become`**: the
Quadlet units belong to that user, and the SSH connection's PAM session is what
makes `systemctl --user` work.
The play targets `emily`, connects as the unprivileged container user, and does
not use `become`. The Quadlet units belong to that user, and the SSH session's
PAM context makes `systemctl --user` work.
```bash
cd ansible
ansible-playbook -u tanshu playbook.yml -e "image_tag=<commit>"
ansible-playbook playbook.yml -e "image_tag=<commit>"
```
All deployment variables live in `ansible/vars/default.yml` (registry, image
tag, domains, PocketBase admin credentials). The play:
Skip the Caddyfile update until DNS points at emily. Otherwise Caddy retries
certificate issuance for a name that still resolves to monoco:
1. Writes `~/.config/containers/systemd/hops.network`
(`NetworkName=hops_net`) and a drop-in that attaches the shared Caddy unit to
it, so Caddy can resolve `hops` by container name.
2. Logs in to the private registry, pulls `registry.tanshu.com/hops:{image_tag}`
(rollback is the same play with an older commit), writes
`~/podman-data/hops/.env`, installs
`~/.config/containers/systemd/hops.container`, starts it as a **systemd user
service** and waits for the image's healthcheck (PocketBase `/api/health` +
the site).
3. Appends the site blocks to the shared Caddyfile
(`~/podman-data/caddy/conf/Caddyfile`) and reloads Caddy:
```bash
ansible-playbook playbook.yml -e "image_tag=<commit>" --skip-tags caddy
```
- `www.hopsngrains.com` (+ apex `hopsngrains.com`) — frontend on port 3000,
PocketBase API/admin proxied under `/api/*` and `/_/*`
- `admin.hopsngrains.com` — redirects to the PocketBase admin UI
Deployment variables live in `ansible/vars/default.yml`. The play:
Nothing is published on the host; Caddy reaches the container over `hops_net`.
1. Writes `~/.config/containers/systemd/hops.network` with `NetworkName=hops_net`,
plus a drop-in that attaches the shared Caddy unit to it. Caddy can then
resolve `hops` by container name.
2. Logs in to the registry, pulls `registry.tanshu.com/hops:{image_tag}`, writes
`~/podman-data/hops/.env`, installs `hops.container`, starts it as a systemd
user service, and waits for the healthcheck.
3. Appends the site blocks to `~/podman-data/caddy/conf/Caddyfile` and reloads
Caddy. `www.hopsngrains.com` and the apex serve the frontend on port 3000 and
proxy `/api/*` and `/_/*` to PocketBase. `admin.hopsngrains.com` redirects to
the PocketBase admin UI.
Nothing is published on the host. Caddy reaches the container over `hops_net`.
The container runs as uid 0, so the unit maps it onto the podman user with
`UserNS=keep-id:uid=0,gid=0`: `~/podman-data/hops` stays owned by `tanshu`, and
PocketBase's files need no `podman unshare` to inspect or back up.
`UserNS=keep-id:uid=0,gid=0`. Files under `~/podman-data/hops` stay owned by
`tanshu`, and PocketBase's files need no `podman unshare` to read or back up.
DNS records for all three hosts must point at `emily`; Caddy obtains TLS
certificates automatically on first request. This deploy starts from empty
data — the schema migration in `backend/pb_migrations/` is applied on first
start, so the seed beers and food are recreated automatically.
DNS for all three hosts must point at emily before Caddy can issue certificates.
The first deploy starts with empty data. PocketBase applies the schema migration
in `backend/pb_migrations/` on first boot and recreates the seed beers and food.
---
## Features & Pages
## Features and pages
| Route | Name | Highlights |
|---|---|---|