commit d0755b7b550c8bbb97b16c8dc82e8e523c13958a Author: tajniak81 <13187254+tajniak81@users.noreply.github.com> Date: Sun Jul 19 12:08:41 2026 +0200 Initial commit: PocketBase Docker setup (root & rootless variants) Add Dockerfiles, Compose stacks, and entrypoints for running PocketBase in both root and rootless container configurations, plus .env.example documenting version, port, superuser bootstrap, and encryption settings. Co-Authored-By: Claude Opus 4.8 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..83af26e --- /dev/null +++ b/.env.example @@ -0,0 +1,31 @@ +# Copy this file to `.env` and adjust as needed: +# cp .env.example .env +# Compose reads `.env` automatically. Keep `.env` OUT of version control. + +# ── General ─────────────────────────────────────────────────────────── +# PocketBase version to build (must match a published release tag, without the leading "v") +PB_VERSION=0.39.7 + +# Name of the running container +CONTAINER_NAME=pocketbase + +# Port PocketBase listens on (used both inside the container and for the host mapping) +PB_PORT=8080 + +# Port for the root variant (only used with `docker compose --profile root up`) +PB_PORT_ROOT=8081 + +# ── Superuser bootstrap ─────────────────────────────────────────────── +# On every start the entrypoint checks whether PB_ADMIN_EMAIL already exists: +# • exists → nothing happens (PB_ADMIN_PASSWORD is ignored/optional) +# • missing → the superuser is created using PB_ADMIN_PASSWORD (required in that case) +# No defaults. Leave blank to skip bootstrapping entirely. +PB_ADMIN_EMAIL= +PB_ADMIN_PASSWORD= + +# ── Encryption ──────────────────────────────────────────────────────── +# Encrypts the application settings stored in PocketBase's database. +# Must be EXACTLY 32 characters. Leave blank to disable. +# Generate one with: openssl rand -hex 16 +# Docs: https://pocketbase.io/docs/going-to-production/#enable-settings-encryption +PB_ENCRYPTION_KEY= diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..d3d2dd0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +# Shell scripts and Docker files run inside Linux containers — they MUST keep +# LF endings or the container fails with "no such file or directory". +*.sh text eol=lf +Dockerfile* text eol=lf +docker-compose*.yml text eol=lf +.env.example text eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7f748d5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# ── Secrets / local config ──────────────────────────────────────────── +# .env holds admin credentials and the encryption key — never commit it. +.env +.env.* +!.env.example + +# ── PocketBase runtime data ─────────────────────────────────────────── +# Local bind-mounts of the SQLite DB, uploads, and generated files. +pb_data/ +pb_public/ +pocketbase +pocketbase.exe + +# Migrations/hooks are usually versioned; ignore only local scratch copies. +# pb_migrations/ +# pb_hooks/ + +# ── Tooling ─────────────────────────────────────────────────────────── +.claude/settings.local.json + +# ── OS / editor cruft ───────────────────────────────────────────────── +.DS_Store +Thumbs.db +*.swp +*~ +.vscode/ +.idea/ diff --git a/Dockerfile.root b/Dockerfile.root new file mode 100644 index 0000000..399ed7b --- /dev/null +++ b/Dockerfile.root @@ -0,0 +1,53 @@ +# syntax=docker/dockerfile:1 + +# ---- build stage: fetch + verify + unzip PocketBase ---- +FROM alpine:3.22 AS build + +ARG PB_VERSION=0.39.7 +# provided automatically by buildx (e.g. amd64, arm64) +ARG TARGETARCH + +RUN apk add --no-cache unzip ca-certificates + +# download the release zip and its checksums file +ADD https://github.com/pocketbase/pocketbase/releases/download/v${PB_VERSION}/pocketbase_${PB_VERSION}_linux_${TARGETARCH}.zip /tmp/pb.zip +ADD https://github.com/pocketbase/pocketbase/releases/download/v${PB_VERSION}/pocketbase_${PB_VERSION}_checksums.txt /tmp/pb_checksums.txt + +# verify integrity against the published checksums, then unzip +RUN grep "pocketbase_${PB_VERSION}_linux_${TARGETARCH}.zip" /tmp/pb_checksums.txt \ + | sed "s#pocketbase_${PB_VERSION}_linux_${TARGETARCH}.zip#/tmp/pb.zip#" \ + | sha256sum -c - \ + && unzip /tmp/pb.zip -d /pb/ + +# ---- runtime stage: minimal image with just the binary ---- +# NOTE: this variant runs PocketBase as root (no dedicated user / USER directive). +FROM alpine:3.22 + +# ca-certificates: outbound TLS (e.g. OAuth, mailer) +# sqlite: used by the entrypoint to check whether the superuser already exists +RUN apk add --no-cache ca-certificates sqlite \ + && mkdir -p /pb/pb_data + +COPY --from=build /pb/pocketbase /pb/pocketbase +COPY --chmod=0755 entrypoint.root.sh /pb/entrypoint.root.sh + +# uncomment to copy the local pb_migrations dir into the image +# COPY ./pb_migrations /pb/pb_migrations + +# uncomment to copy the local pb_hooks dir into the image +# COPY ./pb_hooks /pb/pb_hooks + +# persist the SQLite database and uploaded files across container recreations +VOLUME /pb/pb_data + +# default listen port; override at runtime with -e PB_PORT=... +ENV PB_PORT=8080 +EXPOSE 8080 + +# shell form so ${PB_PORT} is expanded from the runtime environment +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ + CMD wget -qO- "http://127.0.0.1:${PB_PORT}/api/health" || exit 1 + +# runs as root (default user — no USER directive) +# entrypoint handles superuser bootstrap + encryption, then execs `serve` +ENTRYPOINT ["/pb/entrypoint.root.sh"] diff --git a/Dockerfile.rootless b/Dockerfile.rootless new file mode 100644 index 0000000..bdb44d9 --- /dev/null +++ b/Dockerfile.rootless @@ -0,0 +1,56 @@ +# syntax=docker/dockerfile:1 + +# ---- build stage: fetch + verify + unzip PocketBase ---- +FROM alpine:3.22 AS build + +ARG PB_VERSION=0.39.7 +# provided automatically by buildx (e.g. amd64, arm64) +ARG TARGETARCH + +RUN apk add --no-cache unzip ca-certificates + +# download the release zip and its checksums file +ADD https://github.com/pocketbase/pocketbase/releases/download/v${PB_VERSION}/pocketbase_${PB_VERSION}_linux_${TARGETARCH}.zip /tmp/pb.zip +ADD https://github.com/pocketbase/pocketbase/releases/download/v${PB_VERSION}/pocketbase_${PB_VERSION}_checksums.txt /tmp/pb_checksums.txt + +# verify integrity against the published checksums, then unzip +RUN grep "pocketbase_${PB_VERSION}_linux_${TARGETARCH}.zip" /tmp/pb_checksums.txt \ + | sed "s#pocketbase_${PB_VERSION}_linux_${TARGETARCH}.zip#/tmp/pb.zip#" \ + | sha256sum -c - \ + && unzip /tmp/pb.zip -d /pb/ + +# ---- runtime stage: minimal image with just the binary ---- +FROM alpine:3.22 + +# ca-certificates: outbound TLS (e.g. OAuth, mailer) +# sqlite: used by the entrypoint to check whether the superuser already exists +RUN apk add --no-cache ca-certificates sqlite \ + # non-root user to run PocketBase + && addgroup -S pb && adduser -S -G pb -H -D pb \ + && mkdir -p /pb/pb_data \ + && chown -R pb:pb /pb + +COPY --from=build --chown=pb:pb /pb/pocketbase /pb/pocketbase +COPY --chown=pb:pb --chmod=0755 entrypoint.rootless.sh /pb/entrypoint.rootless.sh + +# uncomment to copy the local pb_migrations dir into the image +# COPY --chown=pb:pb ./pb_migrations /pb/pb_migrations + +# uncomment to copy the local pb_hooks dir into the image +# COPY --chown=pb:pb ./pb_hooks /pb/pb_hooks + +# persist the SQLite database and uploaded files across container recreations +VOLUME /pb/pb_data + +USER pb + +# default listen port; override at runtime with -e PB_PORT=... +ENV PB_PORT=8080 +EXPOSE 8080 + +# shell form so ${PB_PORT} is expanded from the runtime environment +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ + CMD wget -qO- "http://127.0.0.1:${PB_PORT}/api/health" || exit 1 + +# entrypoint handles superuser bootstrap + encryption, then execs `serve` +ENTRYPOINT ["/pb/entrypoint.rootless.sh"] diff --git a/docker-compose.root.yml b/docker-compose.root.yml new file mode 100644 index 0000000..5c50edc --- /dev/null +++ b/docker-compose.root.yml @@ -0,0 +1,42 @@ +# Root PocketBase (runs as root inside the container). +# Run: docker compose -f docker-compose.root.yml up -d +# A distinct project name keeps its container/volume separate from the rootless stack. +name: pocketbase-root + +services: + pocketbase: + build: + context: . + dockerfile: Dockerfile.root + args: + # PocketBase version to bake into the image (see .env) + PB_VERSION: ${PB_VERSION:-0.39.7} + image: pocketbase:${PB_VERSION:-0.39.7}-root + container_name: ${CONTAINER_NAME:-pocketbase}-root + restart: unless-stopped + environment: + # server + PB_PORT: ${PB_PORT_ROOT:-8081} + # superuser bootstrap (checked/created by the entrypoint on every start) + PB_ADMIN_EMAIL: ${PB_ADMIN_EMAIL:-} + PB_ADMIN_PASSWORD: ${PB_ADMIN_PASSWORD:-} + # settings encryption (must be exactly 32 characters when set) + PB_ENCRYPTION_KEY: ${PB_ENCRYPTION_KEY:-} + ports: + # host:container + - "${PB_PORT_ROOT:-8081}:${PB_PORT_ROOT:-8081}" + volumes: + # persist the SQLite database and uploaded files + - pb_data:/pb/pb_data + # uncomment to develop migrations/hooks against the running instance + # - ./pb_migrations:/pb/pb_migrations + # - ./pb_hooks:/pb/pb_hooks + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:${PB_PORT_ROOT:-8081}/api/health || exit 1"] + interval: 30s + timeout: 5s + start_period: 5s + retries: 3 + +volumes: + pb_data: diff --git a/docker-compose.rootless.yml b/docker-compose.rootless.yml new file mode 100644 index 0000000..bb2b27f --- /dev/null +++ b/docker-compose.rootless.yml @@ -0,0 +1,42 @@ +# Rootless PocketBase (runs as the non-root `pb` user). +# Run: docker compose -f docker-compose.rootless.yml up -d +# For the root variant, use: docker compose -f docker-compose.root.yml up -d +name: pocketbase + +services: + pocketbase: + build: + context: . + dockerfile: Dockerfile.rootless + args: + # PocketBase version to bake into the image (see .env) + PB_VERSION: ${PB_VERSION:-0.39.7} + image: pocketbase:${PB_VERSION:-0.39.7} + container_name: ${CONTAINER_NAME:-pocketbase} + restart: unless-stopped + environment: + # server + PB_PORT: ${PB_PORT:-8080} + # superuser bootstrap (checked/created by the entrypoint on every start) + PB_ADMIN_EMAIL: ${PB_ADMIN_EMAIL:-} + PB_ADMIN_PASSWORD: ${PB_ADMIN_PASSWORD:-} + # settings encryption (must be exactly 32 characters when set) + PB_ENCRYPTION_KEY: ${PB_ENCRYPTION_KEY:-} + ports: + # host:container + - "${PB_PORT:-8080}:${PB_PORT:-8080}" + volumes: + # persist the SQLite database and uploaded files + - pb_data:/pb/pb_data + # uncomment to develop migrations/hooks against the running instance + # - ./pb_migrations:/pb/pb_migrations + # - ./pb_hooks:/pb/pb_hooks + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:${PB_PORT:-8080}/api/health || exit 1"] + interval: 30s + timeout: 5s + start_period: 5s + retries: 3 + +volumes: + pb_data: diff --git a/entrypoint.root.sh b/entrypoint.root.sh new file mode 100644 index 0000000..5ba11fa --- /dev/null +++ b/entrypoint.root.sh @@ -0,0 +1,54 @@ +#!/bin/sh +# PocketBase container entrypoint (root variant). +# Runs on every start: validates config, conditionally bootstraps the superuser, +# then execs the server. POSIX sh (busybox ash on Alpine). +set -e + +DATA_DIR="/pb/pb_data" +DB_FILE="${DATA_DIR}/data.db" +PB_PORT="${PB_PORT:-8080}" + +# ---- assemble global flags shared by every pocketbase invocation ---- +set -- --dir="${DATA_DIR}" + +if [ -n "${PB_ENCRYPTION_KEY}" ]; then + # PocketBase requires the settings-encryption key to be exactly 32 chars. + if [ "${#PB_ENCRYPTION_KEY}" -ne 32 ]; then + echo "[entrypoint] ERROR: PB_ENCRYPTION_KEY must be exactly 32 characters (got ${#PB_ENCRYPTION_KEY})." >&2 + exit 1 + fi + echo "[entrypoint] Settings encryption enabled." + set -- "$@" --encryptionEnv=PB_ENCRYPTION_KEY +fi + +GLOBAL_FLAGS="$*" + +# ---- does the requested superuser already exist? ---- +superuser_exists() { + # No DB yet => first boot => cannot exist. + [ -f "${DB_FILE}" ] || return 1 + count=$(sqlite3 "${DB_FILE}" \ + "SELECT COUNT(*) FROM _superusers WHERE email = '${PB_ADMIN_EMAIL}';" 2>/dev/null) || return 1 + [ "${count:-0}" -gt 0 ] +} + +# ---- superuser bootstrap (always evaluated, before the server starts) ---- +if [ -n "${PB_ADMIN_EMAIL}" ]; then + if superuser_exists; then + echo "[entrypoint] Superuser '${PB_ADMIN_EMAIL}' already exists — skipping creation." + else + if [ -z "${PB_ADMIN_PASSWORD}" ]; then + echo "[entrypoint] ERROR: superuser '${PB_ADMIN_EMAIL}' does not exist and PB_ADMIN_PASSWORD is not set." >&2 + exit 1 + fi + echo "[entrypoint] Creating superuser '${PB_ADMIN_EMAIL}'..." + # shellcheck disable=SC2086 + /pb/pocketbase superuser create "${PB_ADMIN_EMAIL}" "${PB_ADMIN_PASSWORD}" ${GLOBAL_FLAGS} + fi +else + echo "[entrypoint] PB_ADMIN_EMAIL not set — skipping superuser bootstrap." +fi + +echo "[entrypoint] Starting PocketBase on 0.0.0.0:${PB_PORT}" +# shellcheck disable=SC2086 +exec /pb/pocketbase serve --http="0.0.0.0:${PB_PORT}" ${GLOBAL_FLAGS} diff --git a/entrypoint.rootless.sh b/entrypoint.rootless.sh new file mode 100644 index 0000000..5650c79 --- /dev/null +++ b/entrypoint.rootless.sh @@ -0,0 +1,54 @@ +#!/bin/sh +# PocketBase container entrypoint. +# Runs on every start: validates config, conditionally bootstraps the superuser, +# then execs the server. POSIX sh (busybox ash on Alpine). +set -e + +DATA_DIR="/pb/pb_data" +DB_FILE="${DATA_DIR}/data.db" +PB_PORT="${PB_PORT:-8080}" + +# ---- assemble global flags shared by every pocketbase invocation ---- +set -- --dir="${DATA_DIR}" + +if [ -n "${PB_ENCRYPTION_KEY}" ]; then + # PocketBase requires the settings-encryption key to be exactly 32 chars. + if [ "${#PB_ENCRYPTION_KEY}" -ne 32 ]; then + echo "[entrypoint] ERROR: PB_ENCRYPTION_KEY must be exactly 32 characters (got ${#PB_ENCRYPTION_KEY})." >&2 + exit 1 + fi + echo "[entrypoint] Settings encryption enabled." + set -- "$@" --encryptionEnv=PB_ENCRYPTION_KEY +fi + +GLOBAL_FLAGS="$*" + +# ---- does the requested superuser already exist? ---- +superuser_exists() { + # No DB yet => first boot => cannot exist. + [ -f "${DB_FILE}" ] || return 1 + count=$(sqlite3 "${DB_FILE}" \ + "SELECT COUNT(*) FROM _superusers WHERE email = '${PB_ADMIN_EMAIL}';" 2>/dev/null) || return 1 + [ "${count:-0}" -gt 0 ] +} + +# ---- superuser bootstrap (always evaluated, before the server starts) ---- +if [ -n "${PB_ADMIN_EMAIL}" ]; then + if superuser_exists; then + echo "[entrypoint] Superuser '${PB_ADMIN_EMAIL}' already exists — skipping creation." + else + if [ -z "${PB_ADMIN_PASSWORD}" ]; then + echo "[entrypoint] ERROR: superuser '${PB_ADMIN_EMAIL}' does not exist and PB_ADMIN_PASSWORD is not set." >&2 + exit 1 + fi + echo "[entrypoint] Creating superuser '${PB_ADMIN_EMAIL}'..." + # shellcheck disable=SC2086 + /pb/pocketbase superuser create "${PB_ADMIN_EMAIL}" "${PB_ADMIN_PASSWORD}" ${GLOBAL_FLAGS} + fi +else + echo "[entrypoint] PB_ADMIN_EMAIL not set — skipping superuser bootstrap." +fi + +echo "[entrypoint] Starting PocketBase on 0.0.0.0:${PB_PORT}" +# shellcheck disable=SC2086 +exec /pb/pocketbase serve --http="0.0.0.0:${PB_PORT}" ${GLOBAL_FLAGS}