diff --git a/Docker-AIO/.env.example b/Docker-AIO/.env.example index e81bbda..00041f5 100644 --- a/Docker-AIO/.env.example +++ b/Docker-AIO/.env.example @@ -9,8 +9,8 @@ # An empty value means the built-in default, given in the comment above it. # Every homeserver setting is described in docs/configuration.md. # -# Set by the compose files, not here: the database driver and connection -# string (each file is one database), the listen addresses, the signing key +# Set by the image, not here: the database driver and connection string +# (each Dockerfile is one database), the listen addresses, the signing key # and media paths inside /data, the address the admin panel and web client # reach the homeserver on, and whether the bundled PostgreSQL runs. @@ -21,8 +21,9 @@ WARP_PORT=8008 WARP_ADMIN_PORT=8009 WARP_WEB_PORT=8080 -# The *.prod.yml files pull $WARP_REGISTRY/warp-aio at this tag. Pin a -# release in production. +# The *.prod.yml files pull $WARP_REGISTRY/warp-aio (SQLite), +# warp-aio-pocketbase or warp-aio-postgresql at this tag. Pin a release in +# production. WARP_REGISTRY=ghcr.io/dariuszjaniak WARP_VERSION=latest diff --git a/Docker-AIO/Dockerfile b/Docker-AIO/Dockerfile index 0d47302..ab47a0c 100644 --- a/Docker-AIO/Dockerfile +++ b/Docker-AIO/Dockerfile @@ -1,5 +1,6 @@ # warp-aio — the homeserver, the admin panel and the web client in one -# image, with PostgreSQL bundled for those who want it inside too. +# image, on SQLite. Dockerfile.PostgreSQL and Dockerfile.PocketBase are the +# same image on the other databases. # # Build from the repository root: # @@ -7,10 +8,7 @@ # docker run -p 8008:8008 -p 8009:8009 -p 8080:8080 -v warp-data:/data \ # -e WARP_SERVER_NAME=warp.example warp-aio # -# The database is picked the same way as for the homeserver alone, with -# WARP_DB_DRIVER (sqlite, the default, pocketbase or postgres). -# WARP_AIO_EMBEDDED_POSTGRES=true starts the bundled PostgreSQL in the -# container, its data in /data/postgres; see Docker-AIO/entrypoint.sh. +# The database, signing key and media live in /data; mount a volume on it. FROM node:22-alpine AS web @@ -71,10 +69,8 @@ RUN cd apps/web && GOWORK=off go build -trimpath -ldflags "-s -w -X main.version FROM alpine:3.21 # tini is PID 1 so signals reach the entrypoint and zombies are reaped. -# PostgreSQL runs as the same unprivileged user as everything else. -RUN apk add --no-cache ca-certificates tzdata curl tini postgresql17 postgresql17-client && \ - adduser -D -u 10001 -h /data warp && \ - mkdir -p /run/postgresql && chown warp:warp /run/postgresql +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 diff --git a/Docker-AIO/Dockerfile.PocketBase b/Docker-AIO/Dockerfile.PocketBase new file mode 100644 index 0000000..8bea6ab --- /dev/null +++ b/Docker-AIO/Dockerfile.PocketBase @@ -0,0 +1,107 @@ +# warp-aio-pocketbase — the homeserver, the admin panel and the web client +# in one image, on PocketBase. PocketBase is the homeserver's embedded +# database driver: its data directory, with media in its storage, is +# /data/pb_data. Set WARP_PB_S3_* to keep media in an S3 bucket instead. +# +# Build from the repository root: +# +# docker build -t warp-aio-pocketbase -f Docker-AIO/Dockerfile.PocketBase . +# docker run -p 8008:8008 -p 8009:8009 -p 8080:8080 -v warp-data:/data \ +# -e WARP_SERVER_NAME=warp.example warp-aio-pocketbase +# +# The database, signing key and media live in /data; mount a volume on it. + +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 + +# Database, signing key and media 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_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/Dockerfile.PostgreSQL b/Docker-AIO/Dockerfile.PostgreSQL new file mode 100644 index 0000000..7b7cb87 --- /dev/null +++ b/Docker-AIO/Dockerfile.PostgreSQL @@ -0,0 +1,111 @@ +# warp-aio-postgresql — the homeserver, the admin panel, the web client +# and PostgreSQL 17 in one image. PostgreSQL listens only on a Unix socket +# inside the container, with its data in /data/postgres; see entrypoint.sh. +# To use a PostgreSQL elsewhere instead, set WARP_AIO_EMBEDDED_POSTGRES=false +# and WARP_DB_CONNECTION_STRING to its DSN. +# +# Build from the repository root: +# +# docker build -t warp-aio-postgresql -f Docker-AIO/Dockerfile.PostgreSQL . +# docker run -p 8008:8008 -p 8009:8009 -p 8080:8080 -v warp-data:/data \ +# -e WARP_SERVER_NAME=warp.example warp-aio-postgresql +# +# The database, signing key and media live in /data; mount a volume on it. + +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. +# PostgreSQL runs as the same unprivileged user as everything else. +RUN apk add --no-cache ca-certificates tzdata curl tini postgresql17 postgresql17-client && \ + adduser -D -u 10001 -h /data warp && \ + mkdir -p /run/postgresql && chown warp:warp /run/postgresql + +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 + +# Database, signing key and media 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="postgres" \ + WARP_DB_CONNECTION_STRING="postgres:///warp?host=/run/postgresql&user=warp&sslmode=disable" \ + WARP_MEDIA_BASE_PATH="/data/media" \ + 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="true" + +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 3480552..9662fe1 100644 --- a/Docker-AIO/README.md +++ b/Docker-AIO/README.md @@ -1,17 +1,22 @@ # Docker-AIO: everything in one image -`warp-aio` holds the homeserver, the admin panel, the web client and -PostgreSQL 17 in one image, built from [`Dockerfile`](Dockerfile). -[`entrypoint.sh`](entrypoint.sh) starts them in one container; if one -exits, the rest are stopped and the container's restart policy brings the -set back. +The homeserver, the admin panel and the web client in one image, one image +per database. [`entrypoint.sh`](entrypoint.sh) starts them in one +container; if one exits, the rest are stopped and the container's restart +policy brings the set back. -| File | Database | Image | +| Dockerfile | Image | Database | | --- | --- | --- | -| `docker-compose.sqlite.yml` | SQLite, `/data/warp.db` | built here | -| `docker-compose.pocketbase.yml` | PocketBase (embedded driver), `/data/pb_data` | built here | -| `docker-compose.postgresql.yml` | PostgreSQL in the container, `/data/postgres` | built here | -| `docker-compose.*.prod.yml` | as above | pulled, never built | +| [`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` | + +| Compose file | Image | +| --- | --- | +| `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.*.prod.yml` | the same images, pulled, never built | Everything lives in the `warp-data` volume, mounted on `/data`. @@ -24,8 +29,14 @@ docker compose -f docker-compose.sqlite.yml exec warp warp create-account --user Homeserver on `:8008`, admin panel on `:8009`, web client on `:8080`. -The embedded PostgreSQL listens only on a Unix socket inside the -container, so it needs no password and is not reachable from outside. +The PostgreSQL in `warp-aio-postgresql` listens only on a Unix socket +inside the container, so it needs no password and is not reachable from +outside. To point that image at a PostgreSQL elsewhere instead, set +`WARP_AIO_EMBEDDED_POSTGRES=false` and `WARP_DB_CONNECTION_STRING`. + +The three Dockerfiles share their build stages line for line; only the +header and the final stage differ. Keep the build stages in step when +changing one. ## Settings @@ -39,15 +50,19 @@ default. `.env` is not committed. On top of the settings of | `WARP_AIO_ADMIN` | `true` | `false` runs no admin panel | | `WARP_AIO_WEB` | `true` | `false` runs no web client | -`WARP_AIO_EMBEDDED_POSTGRES`, which starts the bundled PostgreSQL, is set -by `docker-compose.postgresql*.yml`. +The database driver and connection string come from the image. ## Prod files -The `.prod.yml` files pull `$WARP_REGISTRY/warp-aio:$WARP_VERSION` -(defaults `ghcr.io/dariuszjaniak` and `latest`). Push the image first: +The `.prod.yml` files pull `$WARP_REGISTRY/:$WARP_VERSION` +(defaults `ghcr.io/dariuszjaniak` and `latest`). Push the images first, +from the repository root: ```sh docker build -t $WARP_REGISTRY/warp-aio:$WARP_VERSION -f Docker-AIO/Dockerfile . -docker push $WARP_REGISTRY/warp-aio:$WARP_VERSION +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 . +for i in warp-aio warp-aio-pocketbase warp-aio-postgresql; do + docker push $WARP_REGISTRY/$i:$WARP_VERSION +done ``` diff --git a/Docker-AIO/docker-compose.pocketbase.prod.yml b/Docker-AIO/docker-compose.pocketbase.prod.yml index 5b4d4ac..d16a0d7 100644 --- a/Docker-AIO/docker-compose.pocketbase.prod.yml +++ b/Docker-AIO/docker-compose.pocketbase.prod.yml @@ -27,13 +27,10 @@ name: warp-aio-pocketbase services: warp: - image: ${WARP_REGISTRY:-ghcr.io/dariuszjaniak}/warp-aio:${WARP_VERSION:-latest} + image: ${WARP_REGISTRY:-ghcr.io/dariuszjaniak}/warp-aio-pocketbase:${WARP_VERSION:-latest} pull_policy: always restart: unless-stopped env_file: .env - environment: - WARP_DB_DRIVER: pocketbase - WARP_DB_CONNECTION_STRING: /data/pb_data volumes: - warp-data:/data ports: diff --git a/Docker-AIO/docker-compose.pocketbase.yml b/Docker-AIO/docker-compose.pocketbase.yml index 88148e5..dd4dd97 100644 --- a/Docker-AIO/docker-compose.pocketbase.yml +++ b/Docker-AIO/docker-compose.pocketbase.yml @@ -23,13 +23,10 @@ services: warp: build: context: .. - dockerfile: Docker-AIO/Dockerfile - image: warp-aio:latest + dockerfile: Docker-AIO/Dockerfile.PocketBase + image: warp-aio-pocketbase:latest restart: unless-stopped env_file: .env - environment: - WARP_DB_DRIVER: pocketbase - WARP_DB_CONNECTION_STRING: /data/pb_data volumes: - warp-data:/data ports: diff --git a/Docker-AIO/docker-compose.postgresql.prod.yml b/Docker-AIO/docker-compose.postgresql.prod.yml index fbfaea0..0073d96 100644 --- a/Docker-AIO/docker-compose.postgresql.prod.yml +++ b/Docker-AIO/docker-compose.postgresql.prod.yml @@ -25,16 +25,12 @@ name: warp-aio-postgresql services: warp: - image: ${WARP_REGISTRY:-ghcr.io/dariuszjaniak}/warp-aio:${WARP_VERSION:-latest} + image: ${WARP_REGISTRY:-ghcr.io/dariuszjaniak}/warp-aio-postgresql:${WARP_VERSION:-latest} pull_policy: always restart: unless-stopped - # PostgreSQL's fast shutdown runs after the homeserver's; give both time. + # PostgreSQL stops after the homeserver; give both time. stop_grace_period: 30s env_file: .env - environment: - WARP_AIO_EMBEDDED_POSTGRES: "true" - WARP_DB_DRIVER: postgres - WARP_DB_CONNECTION_STRING: postgres:///warp?host=/run/postgresql&user=warp&sslmode=disable volumes: - warp-data:/data ports: diff --git a/Docker-AIO/docker-compose.postgresql.yml b/Docker-AIO/docker-compose.postgresql.yml index 9402ffd..7fa370b 100644 --- a/Docker-AIO/docker-compose.postgresql.yml +++ b/Docker-AIO/docker-compose.postgresql.yml @@ -21,16 +21,12 @@ services: warp: build: context: .. - dockerfile: Docker-AIO/Dockerfile - image: warp-aio:latest + dockerfile: Docker-AIO/Dockerfile.PostgreSQL + image: warp-aio-postgresql:latest restart: unless-stopped - # PostgreSQL's fast shutdown runs after the homeserver's; give both time. + # PostgreSQL stops after the homeserver; give both time. stop_grace_period: 30s env_file: .env - environment: - WARP_AIO_EMBEDDED_POSTGRES: "true" - WARP_DB_DRIVER: postgres - WARP_DB_CONNECTION_STRING: postgres:///warp?host=/run/postgresql&user=warp&sslmode=disable volumes: - warp-data:/data ports: diff --git a/Docker-AIO/docker-compose.sqlite.prod.yml b/Docker-AIO/docker-compose.sqlite.prod.yml index f05a879..7d9ce29 100644 --- a/Docker-AIO/docker-compose.sqlite.prod.yml +++ b/Docker-AIO/docker-compose.sqlite.prod.yml @@ -27,8 +27,6 @@ services: pull_policy: always restart: unless-stopped env_file: .env - environment: - WARP_DB_DRIVER: sqlite volumes: - warp-data:/data ports: diff --git a/Docker-AIO/docker-compose.sqlite.yml b/Docker-AIO/docker-compose.sqlite.yml index 5346174..a5f834e 100644 --- a/Docker-AIO/docker-compose.sqlite.yml +++ b/Docker-AIO/docker-compose.sqlite.yml @@ -23,8 +23,6 @@ services: image: warp-aio:latest restart: unless-stopped env_file: .env - environment: - WARP_DB_DRIVER: sqlite volumes: - warp-data:/data ports: diff --git a/Docker-AIO/entrypoint.sh b/Docker-AIO/entrypoint.sh index d6fc32b..d0c56ba 100644 --- a/Docker-AIO/entrypoint.sh +++ b/Docker-AIO/entrypoint.sh @@ -11,9 +11,9 @@ # WARP_AIO_ADMIN=false no admin panel # WARP_AIO_WEB=false no web client # WARP_AIO_EMBEDDED_POSTGRES=true PostgreSQL in the container, data in -# $PGDATA (/data/postgres); point -# WARP_DB_CONNECTION_STRING at -# postgres:///warp?host=/run/postgresql&user=warp&sslmode=disable +# $PGDATA (/data/postgres); the default +# in warp-aio-postgresql, the only image +# that has PostgreSQL set -eu @@ -53,6 +53,10 @@ shutdown() { trap 'log "stopping"; shutdown 0' TERM INT if [ "$WARP_AIO_EMBEDDED_POSTGRES" = true ]; then + if ! command -v postgres >/dev/null; then + log "WARP_AIO_EMBEDDED_POSTGRES=true, but this image has no PostgreSQL; use warp-aio-postgresql" + exit 1 + fi export PGDATA="${PGDATA:-/data/postgres}" if [ ! -s "$PGDATA/PG_VERSION" ]; then log "initialising PostgreSQL in $PGDATA"