Strapi + Postgres are gone in favor of PocketBase: a single Go binary with embedded SQLite, built-in admin UI and per-collection API rules. No content existed yet, so this is a clean swap with no data migration. Collections and rules are defined as code in pocketbase/pb_migrations/ and applied automatically on first boot. Draft & Publish has no native PocketBase equivalent, so it's reproduced with a nullable `publishedAt` field enforced by listRule/viewRule, matching the old Strapi semantics. Routing flips: PocketBase's admin UI and REST/file API are hardwired to `/_/` and `/api/*` at the domain root (its own dashboard assets and API calls reference those paths directly, so a stripped path prefix like `/admin/*` would break them). `/api` is therefore reserved for PocketBase now, and the frontend's Nitro endpoints move to `/content/*` (frontend/server/routes/content/, not server/api/). A `/admin` vanity route in Nitro (not Caddy) redirects to `/_/`, so it works the same in dev, where Caddy isn't part of the stack, and in production. frontend/server/utils/strapi.ts becomes pocketbase.ts; queries.ts is rewritten for PocketBase's filter/sort/fields/expand query syntax. StrapiImage becomes MediaImage (no width/height — PocketBase file fields don't store dimensions, and the cover images already reserve their aspect ratio via CSS, so this is not a regression). docs/*.md, CLAUDE.md and README.md are updated in the same commit.
Blog
Blog platform: a public website built with Nuxt, and a private PocketBase CMS where the articles are written. Everything runs behind Caddy via Docker Compose.
Browser → Caddy ─┬─ /_/*, /api/* → PocketBase (CMS)
└─ everything else → Nuxt (website), which also redirects /admin → /_/
There are no front-end accounts: only the superuser writes content. An article is a title, a Markdown body, a cover image and a category.
Development
No domain and no TLS needed — the services are published on localhost instead of going through Caddy. Requires Docker.
1. Create the environment file
cp .env.example .env
2. Set the admin credentials. Replace POCKETBASE_ADMIN_EMAIL and
POCKETBASE_ADMIN_PASSWORD in .env with your own — this is the superuser account
PocketBase creates (or updates) on every start. The domain and URL variables can stay as
they are for local use.
chmod 600 .env
3. Start the stack
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build pocketbase frontend
The first build takes a few minutes. docker-compose.dev.yml publishes the ports on
127.0.0.1 and leaves Caddy out; it must always be passed explicitly, so it can never be
picked up by accident in production.
4. Open the admin UI at http://localhost:3000/admin (redirects to PocketBase's dashboard
— this works in dev too, without Caddy, since the redirect is handled by the frontend itself)
and log in with the credentials from .env.
5. Write something. In the admin UI: create a categories record, then an
articles record (the content field is Markdown), then set publishedAt — the
website only shows articles whose publishedAt is set and not in the future.
6. Open the website at http://localhost:3000 — home, /blog, /blog/<slug> and
/category/<slug>.
Useful commands:
docker compose logs -f pocketbase # follow the CMS logs
docker compose -f docker-compose.yml -f docker-compose.dev.yml restart frontend
docker compose down # stop, keep the data
docker compose down -v # stop and WIPE the CMS database and media
To iterate on the frontend without rebuilding an image every time: cd frontend && npm run dev. It defaults to http://localhost:8090 for PocketBase, so it works against the
containerised CMS as is; override it with NUXT_POCKETBASE_URL if needed.
Production
1. Point the DNS at the server. One A record (and AAAA if you have IPv6) on the
public IP of the machine:
| Record | Purpose |
|---|---|
example.com |
the website, and, at /admin, the PocketBase admin UI |
Wait for the record to resolve before starting the stack — Caddy requests the certificate on the first boot and a failed challenge means a retry delay.
2. Open the firewall for ports 80 and 443 only. Port 80 is required: Caddy uses
it for the ACME challenge and to redirect to HTTPS. The admin UI shares port 443 with the
site (reachable by anyone who knows /admin, protected only by the superuser login, so
keep the password strong); PocketBase itself is never published directly, only Caddy's
proxy to it.
3. Configure the environment. Copy .env.example to .env on the server and set:
| Variable | Value |
|---|---|
PUBLIC_DOMAIN |
example.com |
ACME_EMAIL |
a mailbox you read — Let's Encrypt sends expiry warnings there |
PUBLIC_SITE_URL |
https://example.com |
PUBLIC_POCKETBASE_URL |
https://example.com — same origin, Caddy proxies /_/ and /api/ there |
POCKETBASE_URL |
leave it as http://pocketbase:8090 — internal address, never public |
POCKETBASE_ADMIN_EMAIL / POCKETBASE_ADMIN_PASSWORD |
your real superuser credentials |
Keep .env out of version control; it is already ignored.
Changing
POCKETBASE_ADMIN_PASSWORDlater and restarting rotates the superuser password immediately (the entrypoint upserts it on every boot) — a credential change, so treat it with the same care as any production credential rotation.
4. Start everything
docker compose up -d --build
This time Caddy is included: it serves both the website and, under /admin, the CMS
admin UI on PUBLIC_DOMAIN, obtains and renews the TLS certificate on its own, and adds
HSTS and the other security headers.
5. Log into the admin UI at https://example.com/admin with the credentials from
.env, then publish as in development.
6. Back up what is not in git: the pocketbase-data volume (database and uploaded
media together). Nothing else on the server holds state.
docker run --rm -v blog_pocketbase-data:/data -v "$PWD:/out" alpine tar czf /out/pocketbase-data.tar.gz -C /data .
Updating a running site: pull the new code, then docker compose up -d --build.
PocketBase applies new pb_migrations/ files at startup; take a backup first.
Architecture, conventions and constraints are documented in CLAUDE.md.
License
Proprietary — Copyright (c) 2026 Davide Grilli. All rights reserved. See LICENSE.