Build with Podman and deploy as a rootless Quadlet on emily
- Makefile: podman build/push from the committed tree (git archive HEAD), tagged :<commit> + :latest; PLATFORM defaults to linux/arm64 for emily. - deploy.sh: push the commit first, refuse a dirty tracked tree or HEAD != origin/main, then build and deploy that exact commit via -e image_tag (rollback = an older commit). - ansible: target emily with no become. The register role writes hops.network (NetworkName=hops_net) plus the caddy.container.d drop-in; the hops role installs ~/podman-data/hops and hops.container under systemctl --user with a podman healthcheck gate; the caddy role appends the site blocks to the shared Caddyfile and reloads. - Drop the Docker-era network role, community.docker requirements, ansible/files templates and docker-compose.yml. - .containerignore replaces .dockerignore. - README: Podman build, rootless Quadlet deploy on emily.
This commit is contained in:
1 parent
486ed14ee9
commit
843c8ba78a
20 files changed
+618
-235
No files matched your search
@@ -34,11 +34,13 @@ hops/
|
||||
│ │ │ └── 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 # Container build ignore rules
|
||||
├── .containerignore # Build-context excludes (Podman/Buildah)
|
||||
├── .gitignore # Git repository ignore rules
|
||||
├── docker-compose.yml # Local & server orchestration with persistent storage
|
||||
├── 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
|
||||
```
|
||||
@@ -75,34 +77,90 @@ npm run dev:frontend
|
||||
|
||||
---
|
||||
|
||||
## Docker Deployment
|
||||
## Deploying
|
||||
|
||||
The application is containerized using a multi-stage Docker build that bundles both the TanStack Start Nitro SSR server and PocketBase into a single, self-contained container.
|
||||
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`.
|
||||
|
||||
### Option A: Using Docker Compose (Recommended)
|
||||
```bash
|
||||
docker compose up --build -d
|
||||
./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
|
||||
```
|
||||
- Web Application: `http://localhost:3000`
|
||||
- PocketBase Admin: `http://localhost:8090/_/`
|
||||
- Data Persistence: Automatically stored in the `pb_data` named Docker volume.
|
||||
|
||||
### Option B: Building & Running Directly with Docker
|
||||
Or run the pieces yourself:
|
||||
|
||||
```bash
|
||||
# Build the Docker image
|
||||
docker build -t hops-n-grains:latest .
|
||||
make build-production # podman build HEAD, push :<commit> + :latest
|
||||
make build-check # compile check from HEAD, no push
|
||||
|
||||
# Run container with persistent volume
|
||||
docker run -d \
|
||||
--name hops-app \
|
||||
-p 3000:3000 \
|
||||
-p 8090:8090 \
|
||||
-v hops_pb_data:/app/pb_data \
|
||||
-e PB_ADMIN_EMAIL=admin@hopsngrains.com \
|
||||
-e PB_ADMIN_PASSWORD=HopsPassword1234! \
|
||||
hops-n-grains:latest
|
||||
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:
|
||||
|
||||
```bash
|
||||
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.
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
Reference in new issue
Block a user