Files
hops/README.md
T
tanshu a7c9324170 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.
2026-10-20 17:47:49 +05:30

7.8 KiB

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
├── .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)
├── deploy.sh                    # Build & push image, then run the Ansible deploy
├── package.json                 # Monorepo management scripts
└── README.md

Running locally

1. Start the frontend and backend together

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

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.

./deploy.sh           # deploy HEAD
./deploy.sh --prune   # also drop old images on emily, keep 3
./deploy.sh --force   # skip the guards

The image is tagged with its commit id and :main. Rollback runs the same play with an older image_tag:

make build-production   # podman build HEAD, push :<commit> and :main
make build-check        # build from HEAD, push nothing

cd ansible && ansible-playbook playbook.yml -e "image_tag=$(git rev-parse --short HEAD)"

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:

make build-production PLATFORMS=linux/amd64

To run the production image locally:

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

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

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

Skip the Caddyfile update until DNS points at emily. Otherwise Caddy retries certificate issuance for a name that still resolves to monoco:

ansible-playbook playbook.yml -e "image_tag=<commit>" --skip-tags caddy

Deployment variables live in ansible/vars/default.yml. The play:

  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. Files under ~/podman-data/hops stay owned by tanshu, and PocketBase's files need no podman unshare to read or back up.

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