Replace Strapi CMS with PocketBase
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.
This commit is contained in:
@@ -1,15 +1,15 @@
|
||||
# Blog
|
||||
|
||||
Blog platform: a public website built with Nuxt, and a private Strapi CMS where the
|
||||
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.
|
||||
|
||||
```text
|
||||
Browser → Caddy ─┬─ /admin and Strapi's plugin paths → Strapi (CMS) → PostgreSQL
|
||||
└─ everything else → Nuxt (website)
|
||||
Browser → Caddy ─┬─ /_/*, /api/* → PocketBase (CMS)
|
||||
└─ everything else → Nuxt (website), which also redirects /admin → /_/
|
||||
```
|
||||
|
||||
There are no front-end accounts: sign-up is disabled and only administrators write
|
||||
content. An article is a title, a Markdown body, a cover image and a category.
|
||||
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
|
||||
|
||||
@@ -22,37 +22,32 @@ through Caddy. Requires Docker.
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
**2. Generate the secrets.** Every `change-me` must become a different random value —
|
||||
Strapi refuses to start otherwise. This fills them all:
|
||||
**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.
|
||||
|
||||
```bash
|
||||
for var in POSTGRES_PASSWORD API_TOKEN_SALT ADMIN_JWT_SECRET TRANSFER_TOKEN_SALT JWT_SECRET ENCRYPTION_KEY; do
|
||||
sed -i "s|^$var=.*|$var=$(openssl rand -base64 32)|" .env
|
||||
done
|
||||
sed -i "s|^APP_KEYS=.*|APP_KEYS=$(openssl rand -base64 32),$(openssl rand -base64 32)|" .env
|
||||
chmod 600 .env
|
||||
```
|
||||
|
||||
`grep change-me .env` must print nothing. The domain and URL variables can stay as they
|
||||
are for local use.
|
||||
|
||||
**3. Start the stack**
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build database cms frontend
|
||||
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. Create the administrator account** at http://localhost:1337/admin (dev bypasses Caddy,
|
||||
so the CMS is reached directly on its port). This is the first
|
||||
run, so the form creates the account — pick your own credentials.
|
||||
**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 panel: create a **Category**, then an **Article**
|
||||
(the body field is Markdown), then press **Publish** — the website only shows published
|
||||
content.
|
||||
**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>`.
|
||||
@@ -60,16 +55,15 @@ content.
|
||||
Useful commands:
|
||||
|
||||
```bash
|
||||
docker compose logs -f cms # follow the CMS logs
|
||||
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 database and media
|
||||
docker compose down -v # stop and WIPE the CMS database and media
|
||||
```
|
||||
|
||||
To iterate on the code without rebuilding an image every time, run a package directly —
|
||||
`cd frontend && npm run dev`, or `cd cms && npm run develop`. The frontend defaults to
|
||||
`http://localhost:1337` for Strapi, so it works against the containerised CMS as is;
|
||||
override it with `NUXT_STRAPI_URL` if needed. Strapi reads its own `cms/.env`.
|
||||
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
|
||||
|
||||
@@ -78,14 +72,16 @@ public IP of the machine:
|
||||
|
||||
| Record | Purpose |
|
||||
|---|---|
|
||||
| `example.com` | the website and, at `/admin`, the Strapi admin panel |
|
||||
| `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. PostgreSQL, Strapi and Nuxt are only
|
||||
reachable inside the Docker network — do not publish their ports.
|
||||
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:
|
||||
|
||||
@@ -94,16 +90,15 @@ reachable inside the Docker network — do not publish their ports.
|
||||
| `PUBLIC_DOMAIN` | `example.com` |
|
||||
| `ACME_EMAIL` | a mailbox you read — Let's Encrypt sends expiry warnings there |
|
||||
| `PUBLIC_SITE_URL` | `https://example.com` |
|
||||
| `PUBLIC_STRAPI_URL` | `https://example.com` — same origin, Caddy proxies `/admin` and Strapi's other plugin paths there (see `caddy/Caddyfile`) |
|
||||
| `STRAPI_URL` | leave it as `http://cms:1337` — internal address, never public |
|
||||
| `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 |
|
||||
|
||||
Then generate **fresh** secrets on that machine with the same loop as in development —
|
||||
different values from the ones you use locally. Keep `.env` out of version control; it is
|
||||
already ignored.
|
||||
Keep `.env` out of version control; it is already ignored.
|
||||
|
||||
> Changing `APP_KEYS`, `ADMIN_JWT_SECRET` or `JWT_SECRET` later logs everyone out.
|
||||
> Changing `ENCRYPTION_KEY` after content exists makes already-encrypted values
|
||||
> unreadable. Set them once, then back up the file somewhere safe.
|
||||
> Changing `POCKETBASE_ADMIN_PASSWORD` later 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**
|
||||
|
||||
@@ -111,24 +106,22 @@ already ignored.
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
This time Caddy is included: it serves both the website and, under `/admin` and Strapi's
|
||||
other plugin paths, the CMS on `PUBLIC_DOMAIN`, obtains and renews the TLS certificate on
|
||||
its own, and adds HSTS and the other security headers.
|
||||
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. Create the administrator account** at `https://example.com/admin`, immediately,
|
||||
before anyone else finds the URL — the first visitor to that form is the one who gets the
|
||||
account. Then publish as in development.
|
||||
**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 `postgres-data` volume (all content) and the
|
||||
`cms-uploads` volume (all images). Nothing else on the server holds state.
|
||||
**6. Back up what is not in git**: the `pocketbase-data` volume (database and uploaded
|
||||
media together). Nothing else on the server holds state.
|
||||
|
||||
```bash
|
||||
docker compose exec -T database pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" > backup.sql
|
||||
docker run --rm -v blog_cms-uploads:/data -v "$PWD:/out" alpine tar czf /out/uploads.tar.gz -C /data .
|
||||
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`.
|
||||
Strapi applies its own schema changes at startup; take a backup first.
|
||||
PocketBase applies new `pb_migrations/` files at startup; take a backup first.
|
||||
|
||||
Architecture, conventions and constraints are documented in [CLAUDE.md](CLAUDE.md).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user