- Makefile: podman build + push instead of docker buildx. build-production
clones the git remote and builds from it; build-check / build-check-local
build without pushing. Single-architecture (PLATFORM-overridable) rather
than a multi-arch manifest.
- deploy.sh: connect as the unprivileged container user; no become.
- ansible: target emily, rootless Podman Quadlet behind the fleet's shared
Caddy unit (caddy/playbook.yml in ansible-playbooks).
- register role writes greatbear.network (NetworkName=greatbear_net) and
the caddy.container.d drop-in that puts Caddy on it.
- greatbear role: podman login/pull, ~/podman-data/greatbear + .env,
greatbear.container (UserNS=keep-id:uid=0,gid=0, EnvironmentFile,
Network, Volume, no published ports), systemctl --user start and a
podman healthcheck gate, with a diagnostics rescue block.
- caddy role appends the site blocks to the shared Caddyfile and reloads;
handlers reload vs. recreate Caddy (a new network drop-in re-creates it).
- drop the network role, the community.docker requirements and the
ansible/files/ templates.
- .containerignore replaces .dockerignore (Buildah reads it natively).
- README: Podman build, rootless Quadlet deploy. No data migration step —
there is no PocketBase state to carry over.
246 lines
11 KiB
Markdown
246 lines
11 KiB
Markdown
# The Great Bear — Microbrewery Website
|
|
|
|
Full-stack site for The Great Bear microbrewery (SCO 32, Sector 26, Chandigarh), built from the
|
|
Stitch "Industrial Hospitality" design.
|
|
|
|
| Layer | Tech |
|
|
| --------- | ----------------------------------------------------------- |
|
|
| Frontend | [TanStack Start](https://tanstack.com/start) (React 19, Vite 8, Tailwind CSS 4, SSR) |
|
|
| Backend | [PocketBase](https://pocketbase.io) v0.40 — CMS content, form intake, admin UI |
|
|
| Runtime | Single image running both services, as a rootless Podman Quadlet unit |
|
|
| Proxy | Caddy (automatic HTTPS), also a rootless Podman Quadlet unit |
|
|
| Deploys | `deploy.sh [tag]` (Podman build + push) → Ansible playbook |
|
|
|
|
## Pages
|
|
|
|
- `/` — hero, stats, featured brews, experiences, testimonials, quick table booking
|
|
- `/our-brews` — flagship banner, core lineup, seasonal releases, tasting-flight builder
|
|
- `/menu` — filterable gastropub menu with beer pairings
|
|
- `/visit` — reservation form, contact info, map
|
|
|
|
All structured content (brews, menu, testimonials, stats, contact info) is served from
|
|
PocketBase and editable in the admin UI. If PocketBase is unreachable, the site falls back to
|
|
bundled seed content so it never renders empty.
|
|
|
|
## Repository layout
|
|
|
|
```
|
|
app/ TanStack Start application
|
|
src/routes/ Pages (/, /our-brews, /menu, /visit)
|
|
src/components/ Shared UI + client forms
|
|
src/lib/ Server functions & PocketBase client
|
|
src/data/content.json Single source of truth for seed + fallback content
|
|
public/images/ Design assets (downloaded from the Stitch export)
|
|
pb/migrations/ PocketBase schema (snapshot migration, auto-applied)
|
|
scripts/
|
|
seed.mjs Idempotent content seeder (create-if-missing by uid)
|
|
create-collections.sh Dev helper used to build the schema migration
|
|
ansible/ Deployment playbook (roles: register, greatbear, caddy)
|
|
Makefile Podman build targets (build-production / build-check)
|
|
Dockerfile Multi-stage image build (deps → builder → pb-downloader → runner)
|
|
docker-entrypoint.sh Container supervisor: PocketBase + site server
|
|
deploy.sh Build & push image, then run the Ansible deploy
|
|
.containerignore Build-context excludes (Podman/Buildah; .dockerignore fallback)
|
|
```
|
|
|
|
## Local development
|
|
|
|
Frontend only (renders from bundled seed content; forms return errors):
|
|
|
|
```bash
|
|
cd app
|
|
npm install
|
|
npm run dev # http://localhost:3000
|
|
```
|
|
|
|
Full stack with PocketBase (recommended — from the repo root):
|
|
|
|
```bash
|
|
# Terminal 1 — PocketBase on :8090 (data in ./pb_data, schema auto-applied
|
|
# from pb/migrations on first boot)
|
|
./.tmp-pb/pocketbase serve --http=127.0.0.1:8090 --dir=./pb_data --migrationsDir=./pb/migrations
|
|
|
|
# Terminal 2 — one-time setup: create a local admin and seed the content
|
|
./.tmp-pb/pocketbase superuser upsert dev@greatbear.in devpass12345 --dir=./pb_data --migrationsDir=./pb/migrations
|
|
PB_URL=http://127.0.0.1:8090 PB_SUPERUSER_EMAIL=dev@greatbear.in PB_SUPERUSER_PASSWORD=devpass12345 \
|
|
node scripts/seed.mjs
|
|
|
|
# Terminal 2 — the site (talks to PocketBase on 127.0.0.1:8090 automatically)
|
|
cd app && npm run dev
|
|
```
|
|
|
|
- PocketBase admin UI: `http://127.0.0.1:8090/_/` (dev@greatbear.in) — edits show up on the
|
|
site on the next page load
|
|
- Dev data lives in `./pb_data` (git-ignored); delete it for a factory reset
|
|
- The PB binary in `.tmp-pb/` was fetched during setup — if it's missing, grab a release from
|
|
https://github.com/pocketbase/pocketbase/releases (or run PocketBase however you like; only
|
|
the port matters)
|
|
|
|
`npm run build` produces `.output/server/index.mjs`, started with `npm start`. To run the
|
|
**production image** locally instead:
|
|
|
|
```bash
|
|
podman build -t localhost/greatbear:dev .
|
|
podman run --rm -p 3000:3000 -p 8090:8090 \
|
|
-e PB_SUPERUSER_EMAIL=dev@greatbear.in -e PB_SUPERUSER_PASSWORD=devpass12345 \
|
|
-v greatbear_dev_data:/app/pb_data localhost/greatbear:dev
|
|
```
|
|
|
|
## Deploying
|
|
|
|
The image is built with **Podman/Buildah** (not Docker) and always from the
|
|
**committed state of the git remote** — commit and push before deploying. The
|
|
Makefile never prompts, so log in to the registry once per machine:
|
|
`podman login registry.tanshu.com`.
|
|
|
|
```bash
|
|
./deploy.sh # build + push :latest, then deploy that tag via Ansible
|
|
./deploy.sh v1.2.3 # same, with an explicit version tag
|
|
```
|
|
|
|
Or run the pieces yourself:
|
|
|
|
```bash
|
|
make build-production TAG=v1.2.3 # podman build + push from the git remote
|
|
make build-check # build from the git remote, no push
|
|
make build-check-local # build the committed local tree (git archive), no push
|
|
|
|
cd ansible && ansible-playbook -u tanshu playbook.yml -e "tag=v1.2.3"
|
|
```
|
|
|
|
Images are built for the controller's architecture by default. Only cross-build
|
|
when the deploy host differs, and register QEMU binfmt first
|
|
(`qemu-user-static`), the same requirement `docker buildx --platform` had:
|
|
|
|
```bash
|
|
make build-production PLATFORM=linux/arm64
|
|
```
|
|
|
|
### 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.
|
|
|
|
```bash
|
|
cd ansible
|
|
ansible-playbook -u tanshu playbook.yml -e "tag=v1.2.3"
|
|
```
|
|
|
|
All deployment variables live in `ansible/vars/default.yml` (registry, tag,
|
|
domains, PocketBase admin credentials). The play:
|
|
|
|
1. Writes `~/.config/containers/systemd/greatbear.network`
|
|
(`NetworkName=greatbear_net`) and a drop-in that attaches the shared Caddy
|
|
unit to it, so Caddy can resolve `greatbear` by container name.
|
|
2. Logs in to the private registry, pulls
|
|
`registry.tanshu.com/tanshu/greatbear:{tag}`, writes
|
|
`~/podman-data/greatbear/.env`, installs
|
|
`~/.config/containers/systemd/greatbear.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.greatbear.in` (+ apex `greatbear.in`) — frontend on port 3000,
|
|
PocketBase API/admin proxied under `/api/*` and `/_/*`
|
|
- `admin.greatbear.in` — redirects to the PocketBase admin UI
|
|
|
|
Nothing is published on the host; Caddy reaches the container over
|
|
`greatbear_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/greatbear` 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 the server; Caddy obtains TLS
|
|
certificates automatically on first request.
|
|
|
|
## PocketBase admin
|
|
|
|
- URL: `https://admin.greatbear.in/_/`
|
|
- First container start bootstraps the superuser from `PB_SUPERUSER_EMAIL` /
|
|
`PB_SUPERUSER_PASSWORD` (templated from `pb_admin_email` / `pb_admin_password` in
|
|
`ansible/vars/default.yml`, idempotent).
|
|
|
|
Content collections: `brews`, `menu_categories`, `menu_items`, `experiences`, `testimonials`,
|
|
`stats`, `pairings`, `settings`. Reservation/contact submissions land in `reservations`
|
|
(public create only — readable by admins).
|
|
|
|
The seed only creates records that are missing (matched by `uid`), so edits made in the admin
|
|
are never overwritten by redeploys. Schema changes belong in `pb/migrations/`.
|
|
|
|
## Updating the food menu
|
|
|
|
The food menu is managed in code: `app/src/data/menu-data.mjs` (11 categories, ~137 items,
|
|
single source for the PocketBase sync **and** the site's offline fallback).
|
|
|
|
```bash
|
|
# 1. Edit app/src/data/menu-data.mjs, commit & push, deploy (if the image changed)
|
|
# 2. Sync the menu into PocketBase — from anywhere with admin credentials:
|
|
PB_URL=https://admin.greatbear.in \
|
|
PB_SUPERUSER_EMAIL=admin@greatbear.in \
|
|
PB_SUPERUSER_PASSWORD=... \
|
|
node scripts/update-menu.mjs
|
|
```
|
|
|
|
The script **replaces** all `menu_categories` / `ingredients` / `menu_items` records (deletes
|
|
old, inserts new; item ingredient uids are mapped to the fresh ingredient record ids) and
|
|
touches nothing else.
|
|
|
|
### `menu_items` schema (POS-oriented)
|
|
|
|
- `uid` — stable slug identity
|
|
- `category` — slug → `menu_categories`
|
|
- `name` / `variant` — variants are **separate products** whenever their price differs
|
|
("Penne Arrabiata" + variant "Veg" ₹589 / "Chicken" ₹639); the POS can group by `name`
|
|
- `description`
|
|
- `price` — number, rupees with optional paise after the decimal point
|
|
(449 = ₹449, 449.5 = ₹449.50)
|
|
- `ingredients` — many-to-many relation to the `ingredients` collection (Nuts, Dairy,
|
|
Gluten, Soy, Mushroom, Egg, Chicken, Fish, Prawns, Pork, Mutton). Each ingredient has a
|
|
name, an emoji `icon` (rendered as chips on menu cards) and a `diet_class`
|
|
(`veg`/`nonveg`); an item's green/red FSSAI badge is **derived** — it shows non-veg when
|
|
any linked ingredient is non-veg
|
|
- `diet` — select: `veg` / `nonveg` / `mixed` (a bool can't be null in PocketBase, so mixed
|
|
items like Caesar Salad use `mixed`; the site renders no badge for it)
|
|
- `pairing` — optional beer pairing
|
|
- `tag` — badge chip; also carries same-price choice lists (e.g. "Malai / Angara")
|
|
- `image` — optional URL
|
|
- `sku` — unique POS code, `GB-<CAT>-<NNN>` (e.g. GB-MNC-014)
|
|
- `available` — false hides the item from the site (86'd) without deleting it
|
|
- `tax_category` — default `food-5-gst`; adjust per item for POS GST mapping
|
|
|
|
Admin edits to menu records are overwritten by the next sync — treat `menu-data.mjs` as the
|
|
menu's source of truth.
|
|
|
|
## Local development (IDE)
|
|
|
|
`.vscode/launch.json` has run configurations for VS Code / Antigravity:
|
|
|
|
- **GreatBear (full stack)** — compound: PocketBase (:8091) + Vite dev server (:3000)
|
|
- `PocketBase (dev, :8091)` — serves `./pb_data` with `./pb/migrations`
|
|
- `Site (Vite dev, :3000)` — the website against the local PocketBase
|
|
- `Site (production build + preview)` — builds `.output` and previews it
|
|
- `Sync menu into PocketBase` — runs `scripts/update-menu.mjs` against the local PB
|
|
|
|
Dev PocketBase runs on **8091** (8090 is occupied by another service on this machine).
|
|
First-time setup is in the comments at the top of `launch.json`.
|
|
|
|
## Container environment
|
|
|
|
| Variable | Default | Purpose |
|
|
| ----------------------- | ------------------------ | -------------------------------------- |
|
|
| `PORT` | `3000` | Site server port |
|
|
| `HOST` | `0.0.0.0` | Site server bind address |
|
|
| `PB_URL` | `http://127.0.0.1:8090` | PocketBase URL used by the site server |
|
|
| `PB_DATA_DIR` | `/app/pb_data` | PocketBase data directory (volume) |
|
|
| `PB_SUPERUSER_EMAIL` | — | Initial admin (bootstrapped at start) |
|
|
| `PB_SUPERUSER_PASSWORD` | — | Initial admin password |
|