- 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.
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!
- Email:
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:
- Writes
~/.config/containers/systemd/hops.networkwithNetworkName=hops_net, plus a drop-in that attaches the shared Caddy unit to it. Caddy can then resolvehopsby container name. - Logs in to the registry, pulls
registry.tanshu.com/hops:{image_tag}, writes~/podman-data/hops/.env, installshops.container, starts it as a systemd user service, and waits for the healthcheck. - Appends the site blocks to
~/podman-data/caddy/conf/Caddyfileand reloads Caddy.www.hopsngrains.comand the apex serve the frontend on port 3000 and proxy/api/*and/_/*to PocketBase.admin.hopsngrains.comredirects 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. |