Drop bundled PocketBase: app-only deployment via POCKETBASE_URL
The external PocketBase instance is referenced by the POCKETBASE_URL env variable (compose and Dockerfile build packs both supported). The schema export stays at pb_schema.json as a reference for provisioning.
This commit is contained in:
@@ -30,20 +30,6 @@ Node >= 22.12.
|
|||||||
| ---------------- | -------------------------------------------- | ------------------------------------ |
|
| ---------------- | -------------------------------------------- | ------------------------------------ |
|
||||||
| `POCKETBASE_URL` | URL of the PocketBase backend (auth + data) | `https://pokeshare.namarusaja.me` |
|
| `POCKETBASE_URL` | URL of the PocketBase backend (auth + data) | `https://pokeshare.namarusaja.me` |
|
||||||
|
|
||||||
PocketBase container (compose only):
|
|
||||||
|
|
||||||
| Variable | Description |
|
|
||||||
| ------------------------------------ | ------------------------------------------------- |
|
|
||||||
| `PB_SUPERUSER_EMAIL` / `_PASSWORD` | Create/update the admin account on boot |
|
|
||||||
| `PB_APP_URL` | Public app URL used in transactional email links |
|
|
||||||
| `SMTP_HOST` / `SMTP_PORT` | Mail server (mail disabled if `SMTP_HOST` empty) |
|
|
||||||
| `SMTP_USERNAME` / `SMTP_PASSWORD` | Mail server credentials |
|
|
||||||
| `SMTP_TLS` | `true` (default) / `false` |
|
|
||||||
| `SMTP_SENDER_NAME` / `_ADDRESS` | From header of transactional emails |
|
|
||||||
|
|
||||||
SMTP settings are applied on every boot by `pocketbase/pb_hooks/main.pb.js`
|
|
||||||
from these env vars, so they can be changed without touching the admin UI.
|
|
||||||
|
|
||||||
Read at **runtime** (`process.env`), so the same Docker image can be deployed
|
Read at **runtime** (`process.env`), so the same Docker image can be deployed
|
||||||
against different PocketBase instances. For local dev, copy `.env.example` to
|
against different PocketBase instances. For local dev, copy `.env.example` to
|
||||||
`.env`. `HOST` and `PORT` are also honored by the standalone server.
|
`.env`. `HOST` and `PORT` are also honored by the standalone server.
|
||||||
@@ -59,24 +45,18 @@ docker run -p 4321:4321 -e POCKETBASE_URL=https://pb.example.com pokeshare
|
|||||||
|
|
||||||
## Full stack (app + PocketBase)
|
## Full stack (app + PocketBase)
|
||||||
|
|
||||||
`docker-compose.yml` runs the app together with a PocketBase container. On
|
`docker-compose.yml` runs just the app; it expects an external PocketBase
|
||||||
first boot, PocketBase applies `pocketbase/pb_migrations/`, which creates the
|
instance referenced by `POCKETBASE_URL` (see above). A schema export of the
|
||||||
`users` profile fields, the `public_users` view, and the `mycards` and
|
expected collections is kept at `pb_schema.json` for reference when setting
|
||||||
`friendships` collections with their API rules — no manual setup needed.
|
up a new PocketBase instance (admin UI → Settings → Import collections).
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
PB_SUPERUSER_EMAIL=admin@example.com PB_SUPERUSER_PASSWORD=changeme123 \
|
POCKETBASE_URL=https://pb.example.com docker compose up --build
|
||||||
docker compose up --build
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- The app listens on `4321`; PocketBase listens on `8090` (internal network,
|
- The app listens on `4321`; expose it on your domain (Coolify: expose the
|
||||||
admin UI at `/_/` if you expose it).
|
`app` service, or use the plain Dockerfile build pack).
|
||||||
- In Coolify, deploy as a **Docker Compose** resource and expose the `app`
|
- Accounts are managed from the PocketBase admin UI (`/_/`).
|
||||||
service on your domain. Set `PB_SUPERUSER_*` in the environment variables.
|
|
||||||
- Accounts are created from the PocketBase admin UI (public registration is
|
|
||||||
disabled by the migration).
|
|
||||||
- To move existing data, use the PocketBase admin UI backups (Settings →
|
|
||||||
Backups) on the old instance and restore into `pb_data`.
|
|
||||||
|
|
||||||
## Structure
|
## Structure
|
||||||
|
|
||||||
|
|||||||
+5
-41
@@ -1,46 +1,13 @@
|
|||||||
# Full PokeShare stack: the Astro app and its PocketBase backend.
|
# PokeShare app, pointing at an external PocketBase instance.
|
||||||
# In Coolify, deploy this file as a "Docker Compose" resource.
|
# In Coolify, deploy this file as a "Docker Compose" resource and expose the
|
||||||
# The `app` service is the one to expose on your public domain.
|
# `app` service on your public domain.
|
||||||
# `pocketbase` stays on the internal network — expose it on a subdomain
|
|
||||||
# only if you want to reach the admin UI (/_/) from outside.
|
|
||||||
services:
|
services:
|
||||||
pocketbase:
|
|
||||||
build: ./pocketbase
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
# Optional: create/update the admin account on boot (min 8 chars)
|
|
||||||
PB_SUPERUSER_EMAIL: ${PB_SUPERUSER_EMAIL:-}
|
|
||||||
PB_SUPERUSER_PASSWORD: ${PB_SUPERUSER_PASSWORD:-}
|
|
||||||
# Optional: app URL used in transactional emails (reset links...)
|
|
||||||
PB_APP_URL: ${PB_APP_URL:-}
|
|
||||||
# Optional: SMTP for transactional emails. If SMTP_HOST is empty,
|
|
||||||
# mail stays disabled (the rest is ignored).
|
|
||||||
SMTP_HOST: ${SMTP_HOST:-}
|
|
||||||
SMTP_PORT: ${SMTP_PORT:-587}
|
|
||||||
SMTP_USERNAME: ${SMTP_USERNAME:-}
|
|
||||||
SMTP_PASSWORD: ${SMTP_PASSWORD:-}
|
|
||||||
SMTP_TLS: ${SMTP_TLS:-true}
|
|
||||||
SMTP_SENDER_NAME: ${SMTP_SENDER_NAME:-PokeShare}
|
|
||||||
SMTP_SENDER_ADDRESS: ${SMTP_SENDER_ADDRESS:-}
|
|
||||||
volumes:
|
|
||||||
- pb_data:/pb/pb_data
|
|
||||||
healthcheck:
|
|
||||||
test: "curl -fsS http://localhost:8090/api/health || exit 1"
|
|
||||||
interval: 30s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
start_period: 15s
|
|
||||||
|
|
||||||
app:
|
app:
|
||||||
build: .
|
build: .
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
# Internal service URL — the browser never talks to PocketBase
|
# Full URL of the PocketBase instance to use (required).
|
||||||
# directly, everything goes through the Astro server.
|
POCKETBASE_URL: ${POCKETBASE_URL:-https://pokeshare.namarusaja.me}
|
||||||
POCKETBASE_URL: http://pocketbase:8090
|
|
||||||
depends_on:
|
|
||||||
pocketbase:
|
|
||||||
condition: service_healthy
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
# / redirects to /login (302) — check a 200 endpoint instead
|
# / redirects to /login (302) — check a 200 endpoint instead
|
||||||
test: "wget -q -O /dev/null http://localhost:4321/login || exit 1"
|
test: "wget -q -O /dev/null http://localhost:4321/login || exit 1"
|
||||||
@@ -48,6 +15,3 @@ services:
|
|||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
start_period: 15s
|
start_period: 15s
|
||||||
|
|
||||||
volumes:
|
|
||||||
pb_data:
|
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
# PocketBase backend with auto-applied migrations (collections + rules).
|
|
||||||
# Migrations in ./pb_migrations run automatically when the server starts.
|
|
||||||
FROM alpine:3.21
|
|
||||||
|
|
||||||
ARG PB_VERSION=0.39.9
|
|
||||||
|
|
||||||
RUN apk add --no-cache ca-certificates curl unzip
|
|
||||||
|
|
||||||
WORKDIR /pb
|
|
||||||
RUN curl -fsSL "https://github.com/pocketbase/pocketbase/releases/download/v${PB_VERSION}/pocketbase_${PB_VERSION}_linux_amd64.zip" -o pb.zip \
|
|
||||||
&& unzip pb.zip \
|
|
||||||
&& rm pb.zip
|
|
||||||
|
|
||||||
COPY pb_migrations ./pb_migrations
|
|
||||||
COPY pb_hooks ./pb_hooks
|
|
||||||
COPY entrypoint.sh ./
|
|
||||||
RUN chmod +x entrypoint.sh pocketbase
|
|
||||||
|
|
||||||
EXPOSE 8090
|
|
||||||
CMD ["./entrypoint.sh"]
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Optionally create/update the admin (superuser) account on boot.
|
|
||||||
# Set PB_SUPERUSER_EMAIL and PB_SUPERUSER_PASSWORD as env vars to enable.
|
|
||||||
# The password must be at least 8 characters.
|
|
||||||
if [ -n "$PB_SUPERUSER_EMAIL" ] && [ -n "$PB_SUPERUSER_PASSWORD" ]; then
|
|
||||||
./pocketbase superuser upsert "$PB_SUPERUSER_EMAIL" "$PB_SUPERUSER_PASSWORD" || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# `serve` automatically applies pending migrations from ./pb_migrations
|
|
||||||
exec ./pocketbase serve --http=0.0.0.0:8090
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
/**
|
|
||||||
* Apply app identity and SMTP settings from environment variables on every
|
|
||||||
* boot, so mail config stays declarative (docker-compose / Coolify env vars).
|
|
||||||
* These values win over the admin UI (Settings → Mail) — change them via env.
|
|
||||||
*/
|
|
||||||
onBootstrap((e) => {
|
|
||||||
e.next();
|
|
||||||
|
|
||||||
const settings = $app.settings();
|
|
||||||
|
|
||||||
// App identity, used in transactional emails (verification, reset, etc.)
|
|
||||||
settings.meta.appName = "PokeShare";
|
|
||||||
const appUrl = $os.getenv("PB_APP_URL");
|
|
||||||
if (appUrl) settings.meta.appUrl = appUrl;
|
|
||||||
|
|
||||||
const host = $os.getenv("SMTP_HOST");
|
|
||||||
if (!host) {
|
|
||||||
// No SMTP configured — save identity settings and keep mail disabled
|
|
||||||
$app.save(settings);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const senderAddress = $os.getenv("SMTP_SENDER_ADDRESS");
|
|
||||||
const senderName = $os.getenv("SMTP_SENDER_NAME");
|
|
||||||
if (senderAddress) settings.meta.senderAddress = senderAddress;
|
|
||||||
if (senderName) settings.meta.senderName = senderName;
|
|
||||||
|
|
||||||
settings.smtp.enabled = true;
|
|
||||||
settings.smtp.host = host;
|
|
||||||
settings.smtp.port = parseInt($os.getenv("SMTP_PORT") || "587", 10);
|
|
||||||
settings.smtp.username = $os.getenv("SMTP_USERNAME") || "";
|
|
||||||
settings.smtp.password = $os.getenv("SMTP_PASSWORD") || "";
|
|
||||||
settings.smtp.tls = ($os.getenv("SMTP_TLS") || "true") === "true";
|
|
||||||
|
|
||||||
$app.save(settings);
|
|
||||||
});
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
/**
|
|
||||||
* Initial PokeShare schema: extends the default `users` auth collection and
|
|
||||||
* creates `public_users`, `mycards` and `friendships` with their API rules.
|
|
||||||
* Mirrors the production schema (pb_schema.json export).
|
|
||||||
* Applied automatically by `pocketbase serve` on first boot.
|
|
||||||
*/
|
|
||||||
migrate((app) => {
|
|
||||||
// --- users (auth collection): extra profile fields, self-only access ---
|
|
||||||
const users = app.findCollectionByNameOrId("users");
|
|
||||||
users.fields.add(
|
|
||||||
{ name: "username", type: "text", required: true, min: 3, max: 20 },
|
|
||||||
{
|
|
||||||
name: "theme_preference",
|
|
||||||
type: "select",
|
|
||||||
values: ["System", "Light", "Dark"],
|
|
||||||
maxSelect: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "visibility",
|
|
||||||
type: "select",
|
|
||||||
values: ["Public", "Friends", "Private"],
|
|
||||||
maxSelect: 1,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
users.indexes.push(
|
|
||||||
"CREATE UNIQUE INDEX idx_users_username ON users (username COLLATE NOCASE)",
|
|
||||||
);
|
|
||||||
users.listRule = "id = @request.auth.id";
|
|
||||||
users.viewRule = "id = @request.auth.id";
|
|
||||||
users.updateRule = "id = @request.auth.id";
|
|
||||||
// No public registration — accounts are created from the admin UI (/_/)
|
|
||||||
users.createRule = null;
|
|
||||||
app.save(users);
|
|
||||||
|
|
||||||
// --- public_users: read-only view used by the friend search ---
|
|
||||||
const publicUsers = new Collection({
|
|
||||||
name: "public_users",
|
|
||||||
type: "view",
|
|
||||||
viewQuery: `SELECT id, username, name, visibility FROM users WHERE visibility != 'Private'`,
|
|
||||||
listRule: '@request.auth.id != ""',
|
|
||||||
viewRule: null,
|
|
||||||
});
|
|
||||||
app.save(publicUsers);
|
|
||||||
|
|
||||||
// --- mycards: one row per owned card ---
|
|
||||||
// listRule enforces collection visibility at the database level:
|
|
||||||
// owner, or anyone if the owner is Public, or accepted friends if Friends.
|
|
||||||
const mycards = new Collection({
|
|
||||||
name: "mycards",
|
|
||||||
type: "base",
|
|
||||||
fields: [
|
|
||||||
{ name: "card_id", type: "text" },
|
|
||||||
{ name: "card_name", type: "text" },
|
|
||||||
{
|
|
||||||
name: "user",
|
|
||||||
type: "relation",
|
|
||||||
collectionId: "_pb_users_auth_",
|
|
||||||
maxSelect: 1,
|
|
||||||
cascadeDelete: true,
|
|
||||||
},
|
|
||||||
{ name: "card_image", type: "url" },
|
|
||||||
{ name: "variant", type: "text" },
|
|
||||||
{ name: "quantity", type: "number" },
|
|
||||||
{ name: "set_name", type: "text" },
|
|
||||||
{ name: "notes", type: "text" },
|
|
||||||
{ name: "condition", type: "text" },
|
|
||||||
],
|
|
||||||
listRule:
|
|
||||||
"@request.auth.id = user || (user.visibility = 'Public') || (user.visibility = 'Friends' && ((@collection.friendships.requester = user && @collection.friendships.addressee = @request.auth.id && @collection.friendships.status = 'accepted') || (@collection.friendships.addressee = user && @collection.friendships.requester = @request.auth.id && @collection.friendships.status = 'accepted')))",
|
|
||||||
viewRule:
|
|
||||||
"@request.auth.id = user || (@collection.friendships.requester = user && @collection.friendships.addressee = @request.auth.id && @collection.friendships.status = 'accepted') || (@collection.friendships.addressee = user && @collection.friendships.requester = @request.auth.id && @collection.friendships.status = 'accepted')",
|
|
||||||
createRule: '@request.auth.id != "" && user = @request.auth.id',
|
|
||||||
updateRule: '@request.auth.id != "" && user = @request.auth.id',
|
|
||||||
deleteRule: '@request.auth.id != "" && user = @request.auth.id',
|
|
||||||
});
|
|
||||||
app.save(mycards);
|
|
||||||
|
|
||||||
// --- friendships ---
|
|
||||||
const friendships = new Collection({
|
|
||||||
name: "friendships",
|
|
||||||
type: "base",
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
name: "requester",
|
|
||||||
type: "relation",
|
|
||||||
collectionId: "_pb_users_auth_",
|
|
||||||
maxSelect: 1,
|
|
||||||
cascadeDelete: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "addressee",
|
|
||||||
type: "relation",
|
|
||||||
collectionId: "_pb_users_auth_",
|
|
||||||
maxSelect: 1,
|
|
||||||
cascadeDelete: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "status",
|
|
||||||
type: "select",
|
|
||||||
values: ["accepted", "pending", "declined"],
|
|
||||||
maxSelect: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
listRule: "@request.auth.id = requester || @request.auth.id = addressee",
|
|
||||||
viewRule: "@request.auth.id = requester || @request.auth.id = addressee",
|
|
||||||
createRule: "@request.auth.id = requester && addressee != requester",
|
|
||||||
updateRule: "@request.auth.id = addressee || @request.auth.id = requester",
|
|
||||||
deleteRule: "@request.auth.id = addressee || @request.auth.id = requester",
|
|
||||||
});
|
|
||||||
app.save(friendships);
|
|
||||||
}, (app) => {
|
|
||||||
// Down: drop the custom collections. The added `users` fields are left in
|
|
||||||
// place — removing them would destroy data and they are harmless.
|
|
||||||
for (const name of ["friendships", "mycards", "public_users"]) {
|
|
||||||
try {
|
|
||||||
app.delete(app.findCollectionByNameOrId(name));
|
|
||||||
} catch {
|
|
||||||
// already missing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user