From 332916e9dbc942e644163514f401d977b3221583 Mon Sep 17 00:00:00 2001 From: tajniak81 <13187254+tajniak81@users.noreply.github.com> Date: Thu, 22 Oct 2026 20:45:01 +0200 Subject: [PATCH] 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 --- Docker-AIO/.env.example | 8 +- Docker-AIO/Dockerfile.PocketBase.s3 | 120 ++++++++++++++++++ Docker-AIO/README.md | 25 +++- .../docker-compose.pocketbase.s3.prod.yml | 60 +++++++++ Docker-AIO/docker-compose.pocketbase.s3.yml | 56 ++++++++ Docker-AIO/entrypoint.sh | 16 +++ 6 files changed, 283 insertions(+), 2 deletions(-) create mode 100644 Docker-AIO/Dockerfile.PocketBase.s3 create mode 100644 Docker-AIO/docker-compose.pocketbase.s3.prod.yml create mode 100644 Docker-AIO/docker-compose.pocketbase.s3.yml diff --git a/Docker-AIO/.env.example b/Docker-AIO/.env.example index 26852f4..ac21841 100644 --- a/Docker-AIO/.env.example +++ b/Docker-AIO/.env.example @@ -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= diff --git a/Docker-AIO/Dockerfile.PocketBase.s3 b/Docker-AIO/Dockerfile.PocketBase.s3 new file mode 100644 index 0000000..6ce331e --- /dev/null +++ b/Docker-AIO/Dockerfile.PocketBase.s3 @@ -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"] diff --git a/Docker-AIO/README.md b/Docker-AIO/README.md index 00da281..800475e 100644 --- a/Docker-AIO/README.md +++ b/Docker-AIO/README.md @@ -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 diff --git a/Docker-AIO/docker-compose.pocketbase.s3.prod.yml b/Docker-AIO/docker-compose.pocketbase.s3.prod.yml new file mode 100644 index 0000000..aa2b054 --- /dev/null +++ b/Docker-AIO/docker-compose.pocketbase.s3.prod.yml @@ -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: diff --git a/Docker-AIO/docker-compose.pocketbase.s3.yml b/Docker-AIO/docker-compose.pocketbase.s3.yml new file mode 100644 index 0000000..7557b52 --- /dev/null +++ b/Docker-AIO/docker-compose.pocketbase.s3.yml @@ -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: diff --git a/Docker-AIO/entrypoint.sh b/Docker-AIO/entrypoint.sh index ac980f2..7c61161 100644 --- a/Docker-AIO/entrypoint.sh +++ b/Docker-AIO/entrypoint.sh @@ -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