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 <noreply@anthropic.com>
This commit is contained in:
tajniak81
2026-07-19 12:08:41 +02:00
co-authored by Claude Opus 4.8
commit d0755b7b55
9 changed files with 365 additions and 0 deletions
+31
View File
@@ -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=
+6
View File
@@ -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
+27
View File
@@ -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/
+53
View File
@@ -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"]
+56
View File
@@ -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"]
+42
View File
@@ -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:
+42
View File
@@ -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:
+54
View File
@@ -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}
+54
View File
@@ -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}