feat: add Workshop staging deployment
This commit is contained in:
1 parent
c41a3d427e
commit
3ebcedec26
11 files changed
+207
-4
No files matched your search
@@ -23,8 +23,9 @@ jobs:
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm --filter @homarr/workshop typecheck
|
||||
- run: pnpm --filter @homarr/docs typecheck
|
||||
- run: docker compose --env-file apps/workshop/.env.staging.example -f apps/workshop/docker-compose.staging.yml config --quiet
|
||||
- run: pnpm test:workshop
|
||||
- run: docker build --target production -f apps/workshop/Dockerfile .
|
||||
- run: pnpm test:workshop-image
|
||||
|
||||
publish:
|
||||
if: github.event_name == 'push' && (github.ref_name == 'v2' || github.ref_name == 'main')
|
||||
|
||||
@@ -112,8 +112,8 @@ The advanced deployments environment variables should only be used if you know w
|
||||
| Environment Variable | Description |
|
||||
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `DB_MIGRATIONS_DISABLED` | Disable db migrations. For example for helm charts. |
|
||||
| `WORKSHOP_PUBLIC_ORIGIN` | Origin used in PocketBase-generated links and email. It must be set explicitly in production. |
|
||||
| `PB_ALLOWED_ORIGINS` | Browser origins accepted by PocketBase. The central public community defaults to `*` so self-hosted Homarr instances can use OAuth. |
|
||||
| `WORKSHOP_PUBLIC_ORIGIN` | Origin used in PocketBase-generated links and email. It must be set explicitly in production and must match the externally reachable Workshop origin. |
|
||||
| `PB_ALLOWED_ORIGINS` | Browser origins accepted by PocketBase. Use `*` for the central public community so arbitrary self-hosted Homarr instances can use the API and OAuth; private staging may use a comma-separated origin allowlist. |
|
||||
|
||||
All public URL variables reject non-HTTP protocols, embedded credentials, query strings, and fragments, and normalize
|
||||
trailing slashes when Homarr or Docusaurus starts. The Docusaurus-only `WORKSHOP_URL` name is accepted as a deprecated alias for
|
||||
|
||||
@@ -112,6 +112,42 @@ The deprecated docs-only `WORKSHOP_URL` alias remains for one release; `WORKSHOP
|
||||
combined Workshop image writes these public URLs into its browser runtime configuration whenever the container starts;
|
||||
the Docker build arguments remain fallbacks. Restart the container after changing these values, without rebuilding it.
|
||||
|
||||
### Staging and demo deployment
|
||||
|
||||
Use `apps/workshop/docker-compose.staging.yml` with the published `ghcr.io/homarr-labs/workshop:v2` image for a v2
|
||||
preview. Copy `apps/workshop/.env.staging.example` to `.env.staging`; its default public contract is:
|
||||
|
||||
```dotenv
|
||||
HOMARR_WEBSITE_URL=https://v2.preview.homarr.dev
|
||||
WORKSHOP_API_URL=https://v2.preview.homarr.dev
|
||||
WORKSHOP_WEB_URL=https://v2.preview.homarr.dev/workshop
|
||||
WORKSHOP_PUBLIC_ORIGIN=https://v2.preview.homarr.dev
|
||||
PB_ALLOWED_ORIGINS=*
|
||||
```
|
||||
|
||||
Start or update it from the repository root:
|
||||
|
||||
```sh
|
||||
docker compose --env-file apps/workshop/.env.staging \
|
||||
-f apps/workshop/docker-compose.staging.yml pull
|
||||
docker compose --env-file apps/workshop/.env.staging \
|
||||
-f apps/workshop/docker-compose.staging.yml up --detach
|
||||
curl --fail http://127.0.0.1:8090/api/health
|
||||
```
|
||||
|
||||
The service binds to `127.0.0.1:8090`. Terminate TLS at a reverse proxy and forward the **entire hostname**, including
|
||||
`/api`, `/_/`, and `/workshop-runtime-config.js`; forwarding only `/workshop` breaks PocketBase and runtime URL loading.
|
||||
This combined image serves both the documentation site and PocketBase, so use a dedicated Workshop hostname if the
|
||||
chosen hostname already serves Homarr itself. In that case, replace the hostname in all four URL values and keep
|
||||
`/workshop` only on `WORKSHOP_WEB_URL`.
|
||||
|
||||
The Compose volume preserves PocketBase data across pulls and container recreation. The mutable `v2` tag is suitable
|
||||
for previews; use the `sha-<commit>` tag for promotion. For the public central Workshop, `PB_ALLOWED_ORIGINS=*` allows
|
||||
self-hosted Homarr origins to use the API and OAuth. A private demo may use a comma-separated origin allowlist instead.
|
||||
GitHub OAuth requires both credentials and the callback
|
||||
`https://v2.preview.homarr.dev/api/oauth2-redirect`. Restart after changing environment values, then verify the health
|
||||
endpoint, `/workshop`, a public listing request, GitHub sign-in, and an installation from the Homarr preview.
|
||||
|
||||
Back up application and PocketBase data before deployment. Publish the combined image by immutable digest, test the
|
||||
exact digest in staging, rehearse application rollback and PocketBase restore, then promote that same digest.
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
WORKSHOP_IMAGE=ghcr.io/homarr-labs/workshop:v2
|
||||
WORKSHOP_EXPOSE_PORT=8090
|
||||
|
||||
HOMARR_WEBSITE_URL=https://v2.preview.homarr.dev
|
||||
WORKSHOP_API_URL=https://v2.preview.homarr.dev
|
||||
WORKSHOP_WEB_URL=https://v2.preview.homarr.dev/workshop
|
||||
WORKSHOP_PUBLIC_ORIGIN=https://v2.preview.homarr.dev
|
||||
PB_ALLOWED_ORIGINS=*
|
||||
|
||||
# Optional GitHub OAuth application for the preview hostname.
|
||||
# GITHUB_CLIENT_ID=
|
||||
# GITHUB_CLIENT_SECRET=
|
||||
@@ -162,6 +162,45 @@ WORKSHOP_PUBLIC_ORIGIN=https://homarr.dev
|
||||
PB_ALLOWED_ORIGINS=*
|
||||
```
|
||||
|
||||
### Deploy a v2 staging or demo instance
|
||||
|
||||
The staging Compose file runs the published `ghcr.io/homarr-labs/workshop:v2` image, persists `/pb_data`, binds only to
|
||||
the host loopback interface, and expects TLS to terminate at a reverse proxy. It does not need a repository checkout to
|
||||
run; copy `docker-compose.staging.yml` and `.env.staging.example` to the deployment host, then:
|
||||
|
||||
```sh
|
||||
cp .env.staging.example .env.staging
|
||||
docker compose --env-file .env.staging -f docker-compose.staging.yml pull
|
||||
docker compose --env-file .env.staging -f docker-compose.staging.yml up --detach
|
||||
docker compose --env-file .env.staging -f docker-compose.staging.yml ps
|
||||
curl --fail http://127.0.0.1:8090/api/health
|
||||
```
|
||||
|
||||
The checked-in example is ready for `https://v2.preview.homarr.dev`:
|
||||
|
||||
```dotenv
|
||||
HOMARR_WEBSITE_URL=https://v2.preview.homarr.dev
|
||||
WORKSHOP_API_URL=https://v2.preview.homarr.dev
|
||||
WORKSHOP_WEB_URL=https://v2.preview.homarr.dev/workshop
|
||||
WORKSHOP_PUBLIC_ORIGIN=https://v2.preview.homarr.dev
|
||||
PB_ALLOWED_ORIGINS=*
|
||||
```
|
||||
|
||||
Proxy the entire `v2.preview.homarr.dev` hostname to `http://127.0.0.1:8090`, including `/api`, `/_/`, and
|
||||
`/workshop-runtime-config.js`; do not proxy only `/workshop`. The combined image owns both the Docusaurus pages and
|
||||
PocketBase routes. If the hostname already serves the Homarr application, use a dedicated Workshop hostname instead
|
||||
and set all four public URL variables to that hostname (with `/workshop` only on `WORKSHOP_WEB_URL`).
|
||||
|
||||
The `v2` image tag is intentionally convenient and mutable for previews. Run `pull` followed by `up --detach` to test a
|
||||
new v2 image while retaining the named PocketBase volume. Use the immutable `sha-<commit>` image tag when promoting a
|
||||
tested build. The public central Workshop needs `PB_ALLOWED_ORIGINS=*` because Homarr instances on arbitrary origins
|
||||
call the API and open the OAuth flow; a private demo can instead use a comma-separated allowlist of its browser origins.
|
||||
|
||||
After startup, open `https://v2.preview.homarr.dev/_/` to create the first PocketBase superuser. If GitHub OAuth is
|
||||
enabled, configure its callback as `https://v2.preview.homarr.dev/api/oauth2-redirect`, set both GitHub credentials, and
|
||||
restart the service. Verify `/workshop`, `/api/health`, listing requests, sign-in, and one installation from the Homarr
|
||||
preview before sharing the demo.
|
||||
|
||||
Build and smoke-test the combined image locally:
|
||||
|
||||
```sh
|
||||
@@ -172,6 +211,10 @@ curl --fail http://127.0.0.1:3003/workshop
|
||||
docker compose -f apps/workshop/docker-compose.yml --profile docs stop docs
|
||||
```
|
||||
|
||||
`pnpm test:workshop-image` builds the production target, starts it with the preview URL contract, and proves that the
|
||||
browser runtime configuration, Workshop page, health endpoint, migrations, public listing endpoint, and cross-origin
|
||||
browser access all work.
|
||||
|
||||
Normal `up`, `start`, `stop`, and `down` operations retain the named volume. The following command is an intentional,
|
||||
irreversible local reset:
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
name: homarr-workshop-staging
|
||||
|
||||
services:
|
||||
workshop:
|
||||
image: ${WORKSHOP_IMAGE:-ghcr.io/homarr-labs/workshop:v2}
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
HOMARR_WEBSITE_URL: ${HOMARR_WEBSITE_URL:?Set HOMARR_WEBSITE_URL in the Workshop environment file}
|
||||
PB_ALLOWED_ORIGINS: ${PB_ALLOWED_ORIGINS:-*}
|
||||
PORT: 8090
|
||||
WORKSHOP_API_URL: ${WORKSHOP_API_URL:?Set WORKSHOP_API_URL in the Workshop environment file}
|
||||
WORKSHOP_PUBLIC_ORIGIN: ${WORKSHOP_PUBLIC_ORIGIN:?Set WORKSHOP_PUBLIC_ORIGIN in the Workshop environment file}
|
||||
WORKSHOP_WEB_URL: ${WORKSHOP_WEB_URL:?Set WORKSHOP_WEB_URL in the Workshop environment file}
|
||||
GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID:-}
|
||||
GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET:-}
|
||||
ports:
|
||||
- "127.0.0.1:${WORKSHOP_EXPOSE_PORT:-8090}:8090"
|
||||
volumes:
|
||||
- pb_data:/pb_data
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
stop_grace_period: 30s
|
||||
|
||||
volumes:
|
||||
pb_data:
|
||||
@@ -8,8 +8,10 @@ services:
|
||||
target: development
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
HOMARR_WEBSITE_URL: ${HOMARR_WEBSITE_URL:-http://localhost:3003}
|
||||
PB_ALLOWED_ORIGINS: ${PB_ALLOWED_ORIGINS:-*}
|
||||
PORT: ${PB_EXPOSE_PORT:-8090}
|
||||
WORKSHOP_API_URL: ${WORKSHOP_API_URL:-http://localhost:8090}
|
||||
WORKSHOP_WEB_URL: ${WORKSHOP_WEB_URL:-http://localhost:3003/workshop}
|
||||
GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID:-}
|
||||
GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET:-}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
WORKSHOP_IMAGE_TEST_PORT=${WORKSHOP_IMAGE_TEST_PORT:-18091}
|
||||
WORKSHOP_IMAGE_TEST_NAME="homarr-workshop-image-test-$$"
|
||||
WORKSHOP_IMAGE_TEST_TAG="homarr-workshop:test-$$"
|
||||
PREVIEW_ORIGIN=https://v2.preview.homarr.dev
|
||||
|
||||
cleanup() {
|
||||
docker rm --force "$WORKSHOP_IMAGE_TEST_NAME" >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
docker build --target production -f apps/workshop/Dockerfile -t "$WORKSHOP_IMAGE_TEST_TAG" .
|
||||
docker run --detach --name "$WORKSHOP_IMAGE_TEST_NAME" \
|
||||
--publish "127.0.0.1:$WORKSHOP_IMAGE_TEST_PORT:8090" \
|
||||
--env HOMARR_WEBSITE_URL="$PREVIEW_ORIGIN" \
|
||||
--env WORKSHOP_API_URL="$PREVIEW_ORIGIN" \
|
||||
--env WORKSHOP_WEB_URL="$PREVIEW_ORIGIN/workshop" \
|
||||
--env WORKSHOP_PUBLIC_ORIGIN="$PREVIEW_ORIGIN" \
|
||||
--env PB_ALLOWED_ORIGINS='*' \
|
||||
"$WORKSHOP_IMAGE_TEST_TAG" >/dev/null
|
||||
|
||||
for attempt in $(seq 1 60); do
|
||||
if curl --fail --silent "http://127.0.0.1:$WORKSHOP_IMAGE_TEST_PORT/api/health" >/dev/null; then
|
||||
break
|
||||
fi
|
||||
if [ "$attempt" -eq 60 ]; then
|
||||
docker logs "$WORKSHOP_IMAGE_TEST_NAME"
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
WORKSHOP_TEST_URL="http://127.0.0.1:$WORKSHOP_IMAGE_TEST_PORT" \
|
||||
EXPECTED_HOMARR_WEBSITE_URL="$PREVIEW_ORIGIN" \
|
||||
EXPECTED_WORKSHOP_API_URL="$PREVIEW_ORIGIN" \
|
||||
EXPECTED_WORKSHOP_WEB_URL="$PREVIEW_ORIGIN/workshop" \
|
||||
node apps/workshop/tests/runtime-config.integration.mjs
|
||||
|
||||
curl --fail --location --silent "http://127.0.0.1:$WORKSHOP_IMAGE_TEST_PORT/workshop" >/dev/null
|
||||
curl --fail --silent \
|
||||
"http://127.0.0.1:$WORKSHOP_IMAGE_TEST_PORT/api/collections/workshop_listings/records?perPage=1" >/dev/null
|
||||
curl --fail --silent --dump-header - --output /dev/null \
|
||||
--header 'Origin: https://homarr.example' \
|
||||
"http://127.0.0.1:$WORKSHOP_IMAGE_TEST_PORT/api/collections/workshop_listings/records?perPage=1" \
|
||||
| tr -d '\r' \
|
||||
| grep -qi '^Access-Control-Allow-Origin: \*$'
|
||||
|
||||
echo "Workshop production image passed"
|
||||
+10
-1
@@ -14,7 +14,10 @@ cleanup() {
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
PB_EXPOSE_PORT="$WORKSHOP_TEST_PORT" docker compose -p "$WORKSHOP_TEST_PROJECT" \
|
||||
HOMARR_WEBSITE_URL=https://preview.example.invalid \
|
||||
WORKSHOP_API_URL=https://api.preview.example.invalid \
|
||||
WORKSHOP_WEB_URL=https://preview.example.invalid/workshop \
|
||||
PB_EXPOSE_PORT="$WORKSHOP_TEST_PORT" docker compose -p "$WORKSHOP_TEST_PROJECT" \
|
||||
-f apps/workshop/docker-compose.yml up --build -d
|
||||
|
||||
for attempt in $(seq 1 60); do
|
||||
@@ -29,6 +32,12 @@ for attempt in $(seq 1 60); do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
WORKSHOP_TEST_URL="http://127.0.0.1:$WORKSHOP_TEST_PORT" \
|
||||
EXPECTED_HOMARR_WEBSITE_URL=https://preview.example.invalid \
|
||||
EXPECTED_WORKSHOP_API_URL=https://api.preview.example.invalid \
|
||||
EXPECTED_WORKSHOP_WEB_URL=https://preview.example.invalid/workshop \
|
||||
node apps/workshop/tests/runtime-config.integration.mjs
|
||||
|
||||
docker compose -p "$WORKSHOP_TEST_PROJECT" -f apps/workshop/docker-compose.yml exec -T workshop \
|
||||
pocketbase superuser create \
|
||||
workshop-test@example.invalid 'WorkshopLocalTest123!' --dir=/pb_data
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import vm from "node:vm";
|
||||
|
||||
const baseUrl = process.env.WORKSHOP_TEST_URL;
|
||||
if (!baseUrl) throw new Error("WORKSHOP_TEST_URL is required");
|
||||
|
||||
const response = await fetch(`${baseUrl}/workshop-runtime-config.js`);
|
||||
if (!response.ok) throw new Error(`Runtime configuration returned ${response.status}`);
|
||||
|
||||
const context = { TextDecoder, Uint8Array, atob };
|
||||
context.globalThis = context;
|
||||
vm.runInNewContext(await response.text(), context);
|
||||
|
||||
const actual = JSON.parse(JSON.stringify(context.homarrRuntimeConfig));
|
||||
const expected = {
|
||||
homarrWebsiteUrl: process.env.EXPECTED_HOMARR_WEBSITE_URL,
|
||||
workshopApiUrl: process.env.EXPECTED_WORKSHOP_API_URL,
|
||||
workshopWebUrl: process.env.EXPECTED_WORKSHOP_WEB_URL,
|
||||
};
|
||||
|
||||
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
||||
throw new Error(`Unexpected Workshop runtime configuration: ${JSON.stringify(actual)}`);
|
||||
}
|
||||
|
||||
console.log("Workshop runtime configuration passed");
|
||||
@@ -49,6 +49,7 @@
|
||||
"test": "cross-env NODE_ENV=development CI=true vitest run --project \"!e2e\" --coverage.enabled ",
|
||||
"test:e2e": "cross-env NODE_ENV=development CI=true vitest e2e",
|
||||
"test:workshop": "sh apps/workshop/test.sh",
|
||||
"test:workshop-image": "sh apps/workshop/test-image.sh",
|
||||
"test:ui": "cross-env NODE_ENV=development CI=true vitest --project \"!e2e\" --ui --coverage.enabled",
|
||||
"typecheck": "turbo typecheck",
|
||||
"with-env": "dotenv -e .env --"
|
||||
|
||||
Reference in new issue
Block a user