tanshu 7366596a87 Fix the Quadlet healthcheck so the deploy gate can pass
podman builds an OCI image by default and drops the Dockerfile's HEALTHCHECK,
so the first deploy failed at "Wait for the container healthcheck to pass" with
"container ... has no defined healthcheck" even though the app was up.

- Declare the same check on the hops.container unit (HealthCmd + interval,
  timeout, retries, start period), the convention menu-admin already uses.
- Flush handlers right after starting, before the gate: the unit template's
  restart handler otherwise runs at the end of the play, so a run that changes
  only the unit would test the stale container.
2026-10-16 10:46:01 +05:30
2026-09-02 16:29:03 +00:00
2026-09-02 18:22:53 +00:00
2026-09-02 17:45:38 +00:00
2026-09-02 17:51:31 +00:00
2026-09-02 16:29:03 +00:00
2026-09-02 16:29:03 +00:00

Hops n Grains Microbrewery

A modern, full-stack craft microbrewery web application built with TanStack Start for the frontend and PocketBase for the backend, faithfully implementing the Google Stitch Heritage & Harvest tactile industrial design system.


Architecture Overview

hops/
├── backend/
│   ├── pocketbase               # Linux AMD64 executable (WSL)
│   ├── pocketbase.exe           # Windows AMD64 executable (PowerShell)
│   ├── pb_migrations/           # Database schema migrations & initial seeds
│   │   └── 1788363300_initial_schema_and_seed.js
│   └── pb_data/                 # SQLite database & data storage
├── frontend/                    # TanStack Start application
│   ├── src/
│   │   ├── routes/
│   │   │   ├── __root.tsx       # Root layout, fonts, nav, footer, reservation context
│   │   │   ├── index.tsx        # Home page (Google Stitch cinematic hero & craft overview)
│   │   │   ├── brews.tsx        # Craft Brews page (Google Stitch menu with IBU/ABV filters)
│   │   │   ├── food.tsx         # Global Comfort Food menu with beer pairing chips
│   │   │   ├── brewery.tsx      # The Brewery & Heritage story and locations
│   │   │   └── live.tsx         # Live On Tap draft board with keg telemetry
│   │   ├── components/
│   │   │   ├── Navbar.tsx       # Frosted glass top bar with mobile menu drawer
│   │   │   ├── Footer.tsx       # Dark industrial footer with newsletter signup
│   │   │   ├── BeerCard.tsx     # Tactile craft beer card with IBU/ABV meters & pairings
│   │   │   ├── FoodCard.tsx     # Food card with veg/non-veg tags & beer pairing
│   │   │   ├── ReservationModal.tsx # Interactive table reservation modal connected to PocketBase
│   │   │   └── TapHandle.tsx    # Custom copper brewing handle slider from DESIGN.md
│   │   ├── lib/
│   │   │   └── pocketbase.ts    # PocketBase SDK client with fallback data resilience
│   │   └── styles.css           # Tailwind theme tokens, Playfair Display, Montserrat, textures
│   └── package.json
├── ansible/                     # Deployment playbook (roles: register, hops, caddy)
├── Dockerfile                   # Multi-stage production container build
├── .containerignore             # Build-context excludes (Podman/Buildah)
├── .gitignore                   # Git repository ignore rules
├── docker-entrypoint.sh         # Process supervisor running PB & Nitro
├── Makefile                     # Podman build targets (build-production / build-check)
├── deploy.sh                    # Build & push image, then run the Ansible deploy
├── package.json                 # Monorepo management scripts
└── README.md

Running Locally

1. Start Both Frontend & Backend Concurrently

npm run dev

Or start them individually:

Backend (PocketBase):

# In WSL / Linux:
cd backend && ./pocketbase serve --http 127.0.0.1:8090

# In Windows PowerShell:
cd backend ; .\pocketbase.exe serve --http 127.0.0.1:8090
  • API Endpoint: http://127.0.0.1:8090
  • PocketBase Admin UI: http://127.0.0.1:8090/_/
    • Email: admin@hopsngrains.com
    • Password: HopsPassword1234!

Frontend (TanStack Start):

npm run dev:frontend
  • Web Application: http://localhost:3000

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              # push, build <commit>, deploy it to emily
./deploy.sh --no-push    # skip the push (you already pushed)
./deploy.sh --force      # skip the guards

Or run the pieces yourself:

make build-production   # podman build HEAD, push :<commit> + :latest
make build-check        # compile check from HEAD, no push

cd ansible && ansible-playbook -u tanshu 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.

To run the production image locally instead:

podman build -t localhost/hops:dev .
podman run --rm -p 3000:3000 -p 8090:8090 \
  -e PB_ADMIN_EMAIL=admin@hopsngrains.com -e PB_ADMIN_PASSWORD=HopsPassword1234! \
  -v hops_dev_data:/app/pb_data localhost/hops:dev

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 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.

cd ansible
ansible-playbook -u tanshu playbook.yml -e "image_tag=<commit>"

All deployment variables live in ansible/vars/default.yml (registry, image tag, domains, PocketBase admin credentials). The play:

  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:

    • 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

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.

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.


Features & Pages

Route Name Highlights
/ Home Cinematic hero matching Google Stitch mockup, brewery pillars, signature craft beer preview, call-to-action buttons.
/brews Our Brews Interactive craft beer catalog matching Stitch design, style filters (Hefeweizen, Lager, IPA, Dark Ale, Cider), and custom copper Tap Handle IBU slider.
/food Global Comfort Food Handcrafted dining menu (Wood-fired pizzas, smash burgers, truffle fries) categorized with recommended craft beer pairings and veg toggles.
/brewery The Brewery 4-step industrial brewing science walkthrough (Mashing, Boiling, Fermentation, Cellar Pouring), and taproom details for Chandigarh & Panchkula.
/live Live On Tap Real-time draft board tracking active tap numbers, ABV, IBU, and live remaining keg volume percentages.
Modal Table Reservation Interactive booking form for Sector 9 Chandigarh or Sector 5 Panchkula, submitting live to PocketBase.
S
Description
No description provided
Readme
176 KiB
0 Stars 1 Watchers 0 Forks
Languages
TypeScript 79.4%
JavaScript 7.4%
Shell 3.6%
Dockerfile 3%
CSS 3%
Other 3.6%