Docker-AIO: PocketBase image with media in an external S3 bucket

Dockerfile.PocketBase.s3 is the PocketBase all-in-one image with S3
turned on: media goes to an existing bucket on WARP_PB_S3_ENDPOINT
(AWS, MinIO, or a SeaweedFS elsewhere), and the database stays in
/data/pb_data. The compose files require the endpoint and key pair,
default the bucket to warp, the region to us-east-1 and path style
to true, and add host.docker.internal so the endpoint can be on the
Docker host. Modelled on DriverVault's external-S3 compose files.

The entrypoint now waits up to 30 seconds for an external S3 endpoint
to answer and stops if it doesn't, instead of starting a homeserver
that would fail on its first upload. WARP_PB_S3_FORCE_PATH_STYLE is
left empty in .env.example so the S3 file's default applies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
tajniak81andClaude Opus 5.5 committed 2026-10-22 20:45:01 +02:00
1 parent 36d83551aa
commit 332916e9db
6 files changed
+283 -2

No files matched your search

+7 -1
View File
@@ -119,13 +119,19 @@ WARP_DB_AUTO_MIGRATE=
# SeaweedFS in the container; there only the access key and secret are
# needed (required: they are the gateway's credentials too), and the bucket
# defaults to warp.
#
# The pocketbase.s3 files turn it on against a bucket elsewhere: the
# endpoint, access key and secret are required, the bucket (default warp)
# must exist, the region defaults to us-east-1 and path style to true
# (SeaweedFS, MinIO; set false for AWS S3). For a gateway on the Docker
# host, use http://host.docker.internal:8333.
WARP_PB_S3_ENABLED=false
WARP_PB_S3_BUCKET=
WARP_PB_S3_REGION=
WARP_PB_S3_ENDPOINT=
WARP_PB_S3_ACCESS_KEY=
WARP_PB_S3_SECRET=
WARP_PB_S3_FORCE_PATH_STYLE=false
WARP_PB_S3_FORCE_PATH_STYLE=
# Client API. Defaults *, false, 0s (never expire), 0s, true.
WARP_CORS_ALLOWED_ORIGINS=
+120
View File
@@ -0,0 +1,120 @@
# warp-aio-pocketbase-s3 — the homeserver, the admin panel and the web client
# in one image, on PocketBase, with media in an S3 bucket that already exists
# somewhere else: AWS, MinIO, a SeaweedFS on another host. The database stays
# in /data/pb_data. For a SeaweedFS that comes up inside the container, see
# Dockerfile.PocketBase.SeaweedFS.
#
# Build from the repository root:
#
# docker build -t warp-aio-pocketbase-s3 -f Docker-AIO/Dockerfile.PocketBase.s3 .
# docker run -p 8008:8008 -p 8009:8009 -p 8080:8080 -v warp-data:/data \
# -e WARP_SERVER_NAME=warp.example \
# -e WARP_PB_S3_ENDPOINT=https://s3.example.org -e WARP_PB_S3_BUCKET=warp \
# -e WARP_PB_S3_ACCESS_KEY=... -e WARP_PB_S3_SECRET=... \
# warp-aio-pocketbase-s3
#
# WARP_PB_S3_ENDPOINT, WARP_PB_S3_ACCESS_KEY and WARP_PB_S3_SECRET are
# required; the bucket (default warp) must exist, since nothing here creates
# it. Path-style addressing is on, which SeaweedFS and MinIO need; set
# WARP_PB_S3_FORCE_PATH_STYLE=false for AWS S3 proper. The container waits up
# to 30 seconds for the endpoint to answer before it starts the homeserver.
FROM node:22-alpine AS web
WORKDIR /src
RUN corepack enable
# Lockfile and manifests first, so the package store survives source changes.
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./
COPY apps/ui/package.json ./apps/ui/
COPY apps/admin/package.json ./apps/admin/
COPY apps/web/package.json ./apps/web/
RUN pnpm install --frozen-lockfile --filter @warp/admin... --filter @warp/web...
COPY apps/ui ./apps/ui
COPY apps/admin ./apps/admin
COPY apps/web ./apps/web
RUN pnpm --filter @warp/admin build && pnpm --filter @warp/web build
FROM golang:1.26-alpine AS build
ARG VERSION=0.0.0-dev
ARG COMMIT=unknown
ARG BUILD_DATE=unknown
ENV CGO_ENABLED=0
WORKDIR /src
# The homeserver. Dependencies first, so the module cache survives source
# changes.
COPY server/go.mod server/go.sum ./server/
RUN cd server && go mod download
COPY server ./server
RUN cd server && go build -trimpath \
-ldflags "-s -w \
-X github.com/dariuszjaniak/warp/server/internal/buildinfo.Version=${VERSION} \
-X github.com/dariuszjaniak/warp/server/internal/buildinfo.Commit=${COMMIT} \
-X github.com/dariuszjaniak/warp/server/internal/buildinfo.BuildDate=${BUILD_DATE}" \
-o /out/warp ./cmd/warp
# The admin panel and the web client, each with its frontend embedded.
COPY apps/admin/go.mod ./apps/admin/
COPY apps/admin/cmd ./apps/admin/cmd
COPY apps/admin/internal ./apps/admin/internal
COPY --from=web /src/apps/admin/internal/panel/build/dist ./apps/admin/internal/panel/build/dist
RUN cd apps/admin && GOWORK=off go build -trimpath -ldflags "-s -w -X main.version=${VERSION}" \
-o /out/warp-admin ./cmd/warp-admin
COPY apps/web/go.mod ./apps/web/
COPY apps/web/cmd ./apps/web/cmd
COPY apps/web/host ./apps/web/host
COPY apps/web/client/client.go ./apps/web/client/
COPY --from=web /src/apps/web/client/build/dist ./apps/web/client/build/dist
RUN cd apps/web && GOWORK=off go build -trimpath -ldflags "-s -w -X main.version=${VERSION}" \
-o /out/warp-web ./cmd/warp-web
FROM alpine:3.21
# tini is PID 1 so signals reach the entrypoint and zombies are reaped.
RUN apk add --no-cache ca-certificates tzdata curl tini && \
adduser -D -u 10001 -h /data warp
COPY --from=build /out/warp /out/warp-admin /out/warp-web /usr/local/bin/
COPY warp.yaml.example /etc/warp/warp.yaml.example
COPY --chmod=755 Docker-AIO/entrypoint.sh /usr/local/bin/warp-aio
# The database, signing key and anything stored before S3 was on live
# here; mount a volume on it.
WORKDIR /data
RUN chown warp:warp /data
USER warp
ENV WARP_LISTEN_ADDRESS=":8008" \
WARP_PRIVATE_KEY="/data/signing.key" \
WARP_DB_DRIVER="pocketbase" \
WARP_DB_CONNECTION_STRING="/data/pb_data" \
WARP_PB_S3_ENABLED="true" \
WARP_PB_S3_BUCKET="warp" \
WARP_PB_S3_REGION="us-east-1" \
WARP_PB_S3_FORCE_PATH_STYLE="true" \
WARP_LOG_FORMAT="json" \
WARP_ADMIN_LISTEN=":8009" \
WARP_ADMIN_HOMESERVER="http://127.0.0.1:8008" \
WARP_ADMIN_LOG_FORMAT="json" \
WARP_WEB_LISTEN=":8080" \
WARP_WEB_HOMESERVER="http://127.0.0.1:8008" \
WARP_WEB_LOG_FORMAT="json" \
WARP_AIO_ADMIN="true" \
WARP_AIO_WEB="true" \
WARP_AIO_EMBEDDED_POSTGRES="false"
EXPOSE 8008 8009 8080
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
CMD curl -fsS http://127.0.0.1:8008/healthz && \
{ [ "$WARP_AIO_ADMIN" != true ] || curl -fsS http://127.0.0.1:8009/healthz; } && \
{ [ "$WARP_AIO_WEB" != true ] || curl -fsS http://127.0.0.1:8080/healthz; } || exit 1
ENTRYPOINT ["/sbin/tini", "--", "warp-aio"]
+24 -1
View File
@@ -10,6 +10,7 @@ policy brings the set back.
| [`Dockerfile`](Dockerfile) | `warp-aio` | SQLite, `/data/warp.db` |
| [`Dockerfile.PocketBase`](Dockerfile.PocketBase) | `warp-aio-pocketbase` | PocketBase (embedded driver), `/data/pb_data` |
| [`Dockerfile.PostgreSQL`](Dockerfile.PostgreSQL) | `warp-aio-postgresql` | PostgreSQL 17 in the container, `/data/postgres` |
| [`Dockerfile.PocketBase.s3`](Dockerfile.PocketBase.s3) | `warp-aio-pocketbase-s3` | PocketBase, media in an existing S3 bucket elsewhere |
| [`Dockerfile.PocketBase.SeaweedFS`](Dockerfile.PocketBase.SeaweedFS) | `warp-aio-pocketbase-seaweedfs` | PocketBase, media in SeaweedFS (`weed server -s3`) in the container |
| [`Dockerfile.PocketBase.SeaweedFS.split`](Dockerfile.PocketBase.SeaweedFS.split) | `warp-aio-pocketbase-seaweedfs-split` | PocketBase, media in SeaweedFS split into its roles, with its admin UI |
@@ -18,6 +19,7 @@ policy brings the set back.
| `docker-compose.sqlite.yml` | `warp-aio`, built here |
| `docker-compose.pocketbase.yml` | `warp-aio-pocketbase`, built here |
| `docker-compose.postgresql.yml` | `warp-aio-postgresql`, built here |
| `docker-compose.pocketbase.s3.yml` | `warp-aio-pocketbase-s3`, built here |
| `docker-compose.pocketbase.seaweedfs.yml` | `warp-aio-pocketbase-seaweedfs`, built here |
| `docker-compose.pocketbase.seaweedfs.split.yml` | `warp-aio-pocketbase-seaweedfs-split`, built here |
| `docker-compose.*.prod.yml` | the same images, pulled, never built |
@@ -44,6 +46,26 @@ The Dockerfiles share their build stages line for line; only the header,
the SeaweedFS stage and the final stage differ. Keep the build stages in
step when changing one.
## S3 elsewhere
`Dockerfile.PocketBase.s3` keeps the PocketBase database in
`/data/pb_data` and puts media in a bucket on an S3 endpoint that already
exists: AWS, MinIO, or a SeaweedFS on another host. Nothing in the
container runs a gateway or creates the bucket.
```sh
cp .env.example .env # set WARP_PB_S3_ENDPOINT, WARP_PB_S3_ACCESS_KEY,
# WARP_PB_S3_SECRET (and WARP_PB_S3_BUCKET)
docker compose -f docker-compose.pocketbase.s3.yml up -d --build
```
Path-style addressing is on by default, which SeaweedFS and MinIO need;
set `WARP_PB_S3_FORCE_PATH_STYLE=false` for AWS S3. For a gateway on the
Docker host, use `http://host.docker.internal:8333`: the compose files add
that host entry. The container waits up to 30 seconds for the endpoint to
answer and stops if it does not, rather than starting a homeserver that
would fail on its first upload.
## SeaweedFS
The two SeaweedFS images keep the PocketBase database in `/data/pb_data`
@@ -107,9 +129,10 @@ from the repository root:
docker build -t $WARP_REGISTRY/warp-aio:$WARP_VERSION -f Docker-AIO/Dockerfile .
docker build -t $WARP_REGISTRY/warp-aio-pocketbase:$WARP_VERSION -f Docker-AIO/Dockerfile.PocketBase .
docker build -t $WARP_REGISTRY/warp-aio-postgresql:$WARP_VERSION -f Docker-AIO/Dockerfile.PostgreSQL .
docker build -t $WARP_REGISTRY/warp-aio-pocketbase-s3:$WARP_VERSION -f Docker-AIO/Dockerfile.PocketBase.s3 .
docker build -t $WARP_REGISTRY/warp-aio-pocketbase-seaweedfs:$WARP_VERSION -f Docker-AIO/Dockerfile.PocketBase.SeaweedFS .
docker build -t $WARP_REGISTRY/warp-aio-pocketbase-seaweedfs-split:$WARP_VERSION -f Docker-AIO/Dockerfile.PocketBase.SeaweedFS.split .
for i in warp-aio warp-aio-pocketbase warp-aio-postgresql \
for i in warp-aio warp-aio-pocketbase warp-aio-postgresql warp-aio-pocketbase-s3 \
warp-aio-pocketbase-seaweedfs warp-aio-pocketbase-seaweedfs-split; do
docker push $WARP_REGISTRY/$i:$WARP_VERSION
done
@@ -0,0 +1,60 @@
# Warp all-in-one on PocketBase with media in an S3 bucket elsewhere: the
# homeserver, the admin panel and the web client in one container pulled
# from a registry (the image is built from Dockerfile.PocketBase.s3).
#
# cd Docker-AIO
# cp .env.example .env # then edit it; set WARP_PB_S3_ENDPOINT,
# # WARP_PB_S3_ACCESS_KEY and WARP_PB_S3_SECRET
# docker compose -f docker-compose.pocketbase.s3.prod.yml up -d
#
# The database stays in /data/pb_data (warp-data); media goes to the bucket
# WARP_PB_S3_BUCKET (default warp) on WARP_PB_S3_ENDPOINT, which must already
# exist: nothing here runs a gateway or creates the bucket. For a gateway on
# this Docker host, use http://host.docker.internal:8333; the host entry below
# makes that name resolve. For a SeaweedFS inside the container instead, see
# docker-compose.pocketbase.seaweedfs.prod.yml. Media stored before S3 was on
# is not moved into the bucket.
#
# Homeserver http://localhost:8008, admin panel http://localhost:8009, web
# client http://localhost:8080.
#
# Create the first administrator once the server is up:
#
# docker compose -f docker-compose.pocketbase.s3.prod.yml exec warp warp create-account --username admin --admin
#
# Nothing is built here: the image comes from WARP_REGISTRY (default
# ghcr.io/dariuszjaniak) at tag WARP_VERSION (default latest). Pin
# WARP_VERSION to a release in production, and update with
#
# docker compose -f docker-compose.pocketbase.s3.prod.yml pull && docker compose -f docker-compose.pocketbase.s3.prod.yml up -d
name: warp-aio-pocketbase-s3
services:
warp:
image: ${WARP_REGISTRY:-ghcr.io/dariuszjaniak}/warp-aio-pocketbase-s3:${WARP_VERSION:-latest}
pull_policy: always
restart: unless-stopped
extra_hosts:
# Lets WARP_PB_S3_ENDPOINT name the Docker host as host.docker.internal.
- "host.docker.internal:host-gateway"
env_file: .env
environment:
WARP_PB_S3_ENABLED: "true"
WARP_PB_S3_ENDPOINT: ${WARP_PB_S3_ENDPOINT:?set WARP_PB_S3_ENDPOINT in .env}
WARP_PB_S3_BUCKET: ${WARP_PB_S3_BUCKET:-warp}
# SeaweedFS and MinIO ignore the region; the client insists on one.
WARP_PB_S3_REGION: ${WARP_PB_S3_REGION:-us-east-1}
WARP_PB_S3_ACCESS_KEY: ${WARP_PB_S3_ACCESS_KEY:?set WARP_PB_S3_ACCESS_KEY in .env}
WARP_PB_S3_SECRET: ${WARP_PB_S3_SECRET:?set WARP_PB_S3_SECRET in .env}
# true for SeaweedFS and MinIO, false for AWS S3 proper.
WARP_PB_S3_FORCE_PATH_STYLE: ${WARP_PB_S3_FORCE_PATH_STYLE:-true}
volumes:
- warp-data:/data
ports:
- "${WARP_PORT:-8008}:8008"
- "${WARP_ADMIN_PORT:-8009}:8009"
- "${WARP_WEB_PORT:-8080}:8080"
volumes:
warp-data:
@@ -0,0 +1,56 @@
# Warp all-in-one on PocketBase with media in an S3 bucket elsewhere: the
# homeserver, the admin panel and the web client in one container built from
# this checkout (Dockerfile.PocketBase.s3).
#
# cd Docker-AIO
# cp .env.example .env # then edit it; set WARP_PB_S3_ENDPOINT,
# # WARP_PB_S3_ACCESS_KEY and WARP_PB_S3_SECRET
# docker compose -f docker-compose.pocketbase.s3.yml up -d --build
#
# The database stays in /data/pb_data (warp-data); media goes to the bucket
# WARP_PB_S3_BUCKET (default warp) on WARP_PB_S3_ENDPOINT, which must already
# exist: nothing here runs a gateway or creates the bucket. For a gateway on
# this Docker host, use http://host.docker.internal:8333; the host entry below
# makes that name resolve. For a SeaweedFS inside the container instead, see
# docker-compose.pocketbase.seaweedfs.yml. Media stored before S3 was on is
# not moved into the bucket.
#
# Homeserver http://localhost:8008, admin panel http://localhost:8009, web
# client http://localhost:8080.
#
# Create the first administrator once the server is up:
#
# docker compose -f docker-compose.pocketbase.s3.yml exec warp warp create-account --username admin --admin
name: warp-aio-pocketbase-s3
services:
warp:
build:
context: ..
dockerfile: Docker-AIO/Dockerfile.PocketBase.s3
image: warp-aio-pocketbase-s3:latest
restart: unless-stopped
extra_hosts:
# Lets WARP_PB_S3_ENDPOINT name the Docker host as host.docker.internal.
- "host.docker.internal:host-gateway"
env_file: .env
environment:
WARP_PB_S3_ENABLED: "true"
WARP_PB_S3_ENDPOINT: ${WARP_PB_S3_ENDPOINT:?set WARP_PB_S3_ENDPOINT in .env}
WARP_PB_S3_BUCKET: ${WARP_PB_S3_BUCKET:-warp}
# SeaweedFS and MinIO ignore the region; the client insists on one.
WARP_PB_S3_REGION: ${WARP_PB_S3_REGION:-us-east-1}
WARP_PB_S3_ACCESS_KEY: ${WARP_PB_S3_ACCESS_KEY:?set WARP_PB_S3_ACCESS_KEY in .env}
WARP_PB_S3_SECRET: ${WARP_PB_S3_SECRET:?set WARP_PB_S3_SECRET in .env}
# true for SeaweedFS and MinIO, false for AWS S3 proper.
WARP_PB_S3_FORCE_PATH_STYLE: ${WARP_PB_S3_FORCE_PATH_STYLE:-true}
volumes:
- warp-data:/data
ports:
- "${WARP_PORT:-8008}:8008"
- "${WARP_ADMIN_PORT:-8009}:8009"
- "${WARP_WEB_PORT:-8080}:8080"
volumes:
warp-data:
+16
View File
@@ -179,6 +179,22 @@ split)
;;
esac
# An S3 bucket elsewhere: the homeserver starts without checking it, and
# would only fail on the first upload. Wait for the endpoint to answer at
# all (any HTTP status will do: without credentials it may well refuse).
if [ "${WARP_PB_S3_ENABLED:-false}" = true ] && [ -z "$seaweedfs" ] && [ -n "${WARP_PB_S3_ENDPOINT:-}" ]; then
tries=0
until curl -s -o /dev/null --max-time 5 "$WARP_PB_S3_ENDPOINT"; do
tries=$((tries + 1))
if [ "$tries" -ge 30 ]; then
log "S3 endpoint $WARP_PB_S3_ENDPOINT does not answer; stopping"
shutdown 1
fi
log "waiting for S3 endpoint $WARP_PB_S3_ENDPOINT"
sleep 1
done
fi
# The signing key is created on first start and kept in the volume: losing
# it means other servers no longer recognise this one.
if [ ! -f "$WARP_PRIVATE_KEY" ]; then