From b970c5bba180b17d81ffbf1f6da822888a6f8275 Mon Sep 17 00:00:00 2001 From: Raj Nandan Sharma Date: Tue, 24 Feb 2026 11:20:49 +0530 Subject: [PATCH] chore: Enhance Docker and documentation for subpath deployment support --- .github/workflows/create-release.yml | 97 ++++++++++++ .github/workflows/publish-release.yml | 143 ++++++++++++++++++ Dockerfile | 6 + README.md | 25 +++ docker-compose.dev.yml | 1 + docker-compose.status.yml | 47 ++++++ src/lib/components/ThemePlus.svelte | 2 +- src/routes/(docs)/docs.json | 4 + .../docs/content/v4/guides/base-path.md | 85 +++++++++++ .../docs/content/v4/guides/reverse-proxy.md | 2 + 10 files changed, 411 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/create-release.yml create mode 100644 .github/workflows/publish-release.yml create mode 100644 docker-compose.status.yml create mode 100644 src/routes/(docs)/docs/content/v4/guides/base-path.md diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml new file mode 100644 index 00000000..059fe4c1 --- /dev/null +++ b/.github/workflows/create-release.yml @@ -0,0 +1,97 @@ +name: Create Release (Deterministic) + +on: + workflow_dispatch: + inputs: + version: + description: "Release version (for example: 4.0.0)" + required: true + type: string + make_latest: + description: "Mark this release as latest" + required: true + type: boolean + default: true + prerelease: + description: "Mark as pre-release" + required: true + type: boolean + default: false + +permissions: + contents: write + +jobs: + create-release: + name: Bump version, tag, and create release + runs-on: ubuntu-latest + + steps: + - name: Check out default branch + uses: actions/checkout@v4.2.2 + with: + ref: ${{ github.event.repository.default_branch }} + fetch-depth: 0 + + - name: Validate version format + run: | + VERSION="${{ inputs.version }}" + if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then + echo "Invalid version format: $VERSION" + echo "Use semver like 4.0.0 or 4.0.0-rc.1" + exit 1 + fi + + - name: Ensure release tag does not already exist + run: | + TAG="v${{ inputs.version }}" + if git rev-parse "$TAG" >/dev/null 2>&1; then + echo "Tag $TAG already exists" + exit 1 + fi + + - name: Bump package version + run: | + VERSION="${{ inputs.version }}" + CURRENT_VERSION="$(node -p \"require('./package.json').version\")" + + if [ "$CURRENT_VERSION" != "$VERSION" ]; then + npm version "$VERSION" --no-git-tag-version --allow-same-version + else + echo "package.json already at version $VERSION" + fi + + - name: Commit version bump + run: | + VERSION="${{ inputs.version }}" + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + git add package.json + if [ -f package-lock.json ]; then + git add package-lock.json + fi + + if git diff --cached --quiet; then + echo "No changes to commit" + else + git commit -m "chore(release): bump version to $VERSION" + fi + + - name: Create and push git tag + run: | + VERSION="${{ inputs.version }}" + TAG="v$VERSION" + + git tag -a "$TAG" -m "Release $TAG" + git push origin "HEAD:${{ github.event.repository.default_branch }}" + git push origin "$TAG" + + - name: Create GitHub release + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ inputs.version }} + target_commitish: ${{ github.event.repository.default_branch }} + generate_release_notes: true + make_latest: ${{ inputs.make_latest && 'true' || 'false' }} + prerelease: ${{ inputs.prerelease }} diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml new file mode 100644 index 00000000..cd29d2d7 --- /dev/null +++ b/.github/workflows/publish-release.yml @@ -0,0 +1,143 @@ +name: Publish Release Docker Images + +on: + release: + types: + - published + workflow_dispatch: + +env: + DOCKERHUB_REGISTRY: docker.io + GITHUB_REGISTRY: ghcr.io + DOCKERHUB_IMAGE_NAME: ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }} + GITHUB_IMAGE_NAME: ${{ github.repository }} + +concurrency: + group: release-docker-${{ github.event.release.tag_name || github.ref }} + cancel-in-progress: true + +jobs: + build-and-push-release: + name: Build and push release Docker images + strategy: + matrix: + variant: [debian, alpine] + base_path: ["", "/status"] + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write + + steps: + - name: Check out release tag + uses: actions/checkout@v4.2.2 + with: + ref: refs/tags/${{ github.event.release.tag_name || github.ref_name }} + fetch-depth: 0 + + - name: Validate package version matches release tag + run: | + TAG="${{ github.event.release.tag_name || github.ref_name }}" + EXPECTED_VERSION="${TAG#v}" + PACKAGE_VERSION="$(node -p \"require('./package.json').version\")" + + if [ "$PACKAGE_VERSION" != "$EXPECTED_VERSION" ]; then + echo "package.json version mismatch" + echo "release tag: $TAG" + echo "expected package.json version: $EXPECTED_VERSION" + echo "actual package.json version: $PACKAGE_VERSION" + exit 1 + fi + + - name: Install cosign + uses: sigstore/cosign-installer@v3.8.0 + with: + cosign-release: v2.2.4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3.8.0 + + - name: Log in to Docker Hub + uses: docker/login-action@v3.3.0 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3.3.0 + with: + registry: ${{ env.GITHUB_REGISTRY }} + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Compute release tags + id: vars + run: | + TAG="${{ github.event.release.tag_name || github.ref_name }}" + NORM_TAG="${TAG#v}" + + if [ "${{ matrix.base_path }}" = "/status" ]; then + BASE_SUFFIX="-status" + else + BASE_SUFFIX="" + fi + + if [ "${{ matrix.variant }}" = "alpine" ]; then + VARIANT_SUFFIX="-alpine" + else + VARIANT_SUFFIX="" + fi + + FULL_SUFFIX="${BASE_SUFFIX}${VARIANT_SUFFIX}" + + echo "release_tag=${TAG}${FULL_SUFFIX}" >> "$GITHUB_OUTPUT" + echo "release_norm_tag=${NORM_TAG}${FULL_SUFFIX}" >> "$GITHUB_OUTPUT" + echo "latest_tag=latest${FULL_SUFFIX}" >> "$GITHUB_OUTPUT" + + if [ "${{ matrix.variant }}" = "debian" ]; then + echo "release_tag_debian_alias=${TAG}${BASE_SUFFIX}-debian" >> "$GITHUB_OUTPUT" + echo "release_norm_tag_debian_alias=${NORM_TAG}${BASE_SUFFIX}-debian" >> "$GITHUB_OUTPUT" + echo "latest_tag_debian_alias=latest${BASE_SUFFIX}-debian" >> "$GITHUB_OUTPUT" + fi + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5.6.1 + with: + images: | + ${{ env.DOCKERHUB_IMAGE_NAME }} + ${{ env.GITHUB_REGISTRY }}/${{ env.GITHUB_IMAGE_NAME }} + tags: | + type=raw,value=${{ steps.vars.outputs.latest_tag }} + type=raw,value=${{ steps.vars.outputs.release_tag }} + type=raw,value=${{ steps.vars.outputs.release_norm_tag }},enable=${{ steps.vars.outputs.release_norm_tag != steps.vars.outputs.release_tag }} + type=raw,value=${{ steps.vars.outputs.latest_tag_debian_alias }},enable=${{ matrix.variant == 'debian' }} + type=raw,value=${{ steps.vars.outputs.release_tag_debian_alias }},enable=${{ matrix.variant == 'debian' }} + type=raw,value=${{ steps.vars.outputs.release_norm_tag_debian_alias }},enable=${{ matrix.variant == 'debian' && steps.vars.outputs.release_norm_tag_debian_alias != steps.vars.outputs.release_tag_debian_alias }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3.3.0 + + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@v6.13.0 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + VARIANT=${{ matrix.variant }} + WITH_DOCS=true + KENER_BASE_PATH=${{ matrix.base_path }} + platforms: linux/amd64,linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Sign the published Docker images + env: + TAGS: ${{ steps.meta.outputs.tags }} + DIGEST: ${{ steps.build-and-push.outputs.digest }} + run: | + echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST} diff --git a/Dockerfile b/Dockerfile index 46b02e06..3f4b9d0f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,6 +21,7 @@ ARG NODE_VERSION=24 ARG VARIANT=alpine ARG WITH_DOCS=false +ARG KENER_BASE_PATH= # ============================================================================= # STAGE 1 — BUILDER (installs deps, compiles native modules, builds app) @@ -52,6 +53,9 @@ ENV NPM_CONFIG_LOGLEVEL=error WORKDIR /app +ARG KENER_BASE_PATH +ENV KENER_BASE_PATH=${KENER_BASE_PATH} + # 1. Copy package manifests first (maximises layer cache hits) COPY package*.json ./ @@ -126,9 +130,11 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ FROM final-${VARIANT} AS final ARG PORT=3000 +ARG KENER_BASE_PATH= ENV NODE_ENV=production \ PORT=${PORT} \ + KENER_BASE_PATH=${KENER_BASE_PATH} \ TZ=UTC \ # Required so Node can import .ts migration/seed files at runtime NODE_OPTIONS="--experimental-strip-types" diff --git a/README.md b/README.md index b57345a3..b908271b 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,13 @@ You can use either image: - `docker.io/rajnandan1/kener:latest` - `ghcr.io/rajnandan1/kener:latest` +For subpath deployments (`/status`), use: + +- `docker.io/rajnandan1/kener:latest-status` +- `docker.io/rajnandan1/kener:latest-status-alpine` +- `ghcr.io/rajnandan1/kener:latest-status` +- `ghcr.io/rajnandan1/kener:latest-status-alpine` + ```bash mkdir -p database docker run -d \ @@ -118,6 +125,24 @@ docker run -d \ docker.io/rajnandan1/kener:latest ``` +### Run pre-built subpath image (`/status`) + +```bash +mkdir -p database +docker run -d \ + --name kener-status \ + -p 3000:3000 \ + -v "$(pwd)/database:/app/database" \ + -e "KENER_SECRET_KEY=replace_with_a_random_string" \ + -e "ORIGIN=http://localhost:3000" \ + -e "KENER_BASE_PATH=/status" \ + -e "REDIS_URL=redis://host.docker.internal:6379" \ + docker.io/rajnandan1/kener:latest-status +``` + +> [!NOTE] +> For subpath mode, keep `ORIGIN` as the site origin (`http://localhost:3000`), not `http://localhost:3000/status`. + ### Run without Docker Requirements: diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 18016ce6..130630a2 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -37,6 +37,7 @@ services: VARIANT: alpine # or "debian" NODE_VERSION: 24 WITH_DOCS: "true" + KENER_BASE_PATH: "" container_name: kener-dev environment: KENER_SECRET_KEY: dev-secret-key-for-local-testing-only diff --git a/docker-compose.status.yml b/docker-compose.status.yml new file mode 100644 index 00000000..32488568 --- /dev/null +++ b/docker-compose.status.yml @@ -0,0 +1,47 @@ +# ============================================================================= +# Kener v4 — Production Docker Compose for /status base path +# +# Usage: +# docker compose -f docker-compose.status.yml up -d +# ============================================================================= + +services: + redis: + image: redis:7-alpine + container_name: kener-redis-status + restart: unless-stopped + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + + kener: + image: rajnandan1/kener:latest-status + # For Alpine variant use: rajnandan1/kener:latest-status-alpine + container_name: kener-status + environment: + KENER_SECRET_KEY: replace_me_with_a_random_string # generate: openssl rand -base64 32 + ORIGIN: http://localhost:3000/status + REDIS_URL: redis://redis:6379 + KENER_BASE_PATH: /status + + # DATABASE_URL: sqlite://./database/kener.sqlite.db + # DATABASE_URL: postgresql://user:password@postgres:5432/kener + # DATABASE_URL: mysql://user:password@mysql:3306/kener + ports: + - "3000:3000" + volumes: + - data:/app/database + depends_on: + redis: + condition: service_healthy + restart: unless-stopped + +volumes: + data: + name: kener_db_status + redis_data: + name: kener_redis_status diff --git a/src/lib/components/ThemePlus.svelte b/src/lib/components/ThemePlus.svelte index 845bb8ea..eb286d55 100644 --- a/src/lib/components/ThemePlus.svelte +++ b/src/lib/components/ThemePlus.svelte @@ -78,7 +78,7 @@ }); -
+
{#if page.data.isSubsEnabled && page.data.canSendEmail} diff --git a/src/routes/(docs)/docs.json b/src/routes/(docs)/docs.json index e97aa891..95e6cadb 100644 --- a/src/routes/(docs)/docs.json +++ b/src/routes/(docs)/docs.json @@ -240,6 +240,10 @@ { "title": "Reverse Proxy Setup", "content": "v4/guides/reverse-proxy" + }, + { + "title": "Base Path Deployment", + "content": "v4/guides/base-path" } ] } diff --git a/src/routes/(docs)/docs/content/v4/guides/base-path.md b/src/routes/(docs)/docs/content/v4/guides/base-path.md new file mode 100644 index 00000000..1f5d4277 --- /dev/null +++ b/src/routes/(docs)/docs/content/v4/guides/base-path.md @@ -0,0 +1,85 @@ +--- +title: Base Path Deployment +description: Deploy Kener under a subpath like /status using KENER_BASE_PATH with Docker and reverse proxies +--- + +Use this guide when Kener should be served from a subpath (for example `https://example.com/status`) instead of domain root. + +## Quick setup {#quick-setup} + +Set these values: + +| Variable | Value for subpath setup | +| :---------------- | :---------------------- | +| `KENER_BASE_PATH` | `/status` | +| `ORIGIN` | `https://example.com` | + +> [!IMPORTANT] +> `ORIGIN` must be the site origin only (scheme + host + optional port), not the full `/status` URL. + +## Prebuilt Docker images for subpath {#prebuilt-images} + +Use the prebuilt subpath tags: + +- `docker.io/rajnandan1/kener:latest-status` +- `docker.io/rajnandan1/kener:latest-status-alpine` +- `ghcr.io/rajnandan1/kener:latest-status` +- `ghcr.io/rajnandan1/kener:latest-status-alpine` + +Release tags follow the same pattern: + +- `vX.Y.Z-status` +- `vX.Y.Z-status-alpine` + +## Run with Docker {#run-docker} + +```bash +docker run -d \ + --name kener-status \ + -p 3000:3000 \ + -v "$(pwd)/database:/app/database" \ + -e "KENER_SECRET_KEY=replace_with_a_random_string" \ + -e "ORIGIN=https://example.com" \ + -e "KENER_BASE_PATH=/status" \ + -e "REDIS_URL=redis://host.docker.internal:6379" \ + docker.io/rajnandan1/kener:latest-status +``` + +## Run with Docker Compose {#run-compose} + +Use the provided `docker-compose.status.yml` file: + +```bash +docker compose -f docker-compose.status.yml up -d +``` + +This compose file already sets: + +- `image: rajnandan1/kener:latest-status` +- `KENER_BASE_PATH: /status` + +## Reverse proxy mapping {#reverse-proxy-mapping} + +Your proxy path and `KENER_BASE_PATH` must match exactly. + +For `/status`, proxy traffic to Kener on the same `/status` path. + +For full examples, see [Reverse Proxy Setup](/docs/v4/guides/reverse-proxy). + +## Verify setup {#verify-setup} + +Check these URLs: + +- `https://example.com/status` +- `https://example.com/status/healthcheck` + +If CSS/JS files 404, re-check: + +1. `KENER_BASE_PATH` is set to `/status` +2. proxy path is also `/status` +3. Kener was rebuilt/restarted after env changes + +## Related docs {#related-docs} + +- [Environment Variables](/docs/v4/setup/environment-variables) +- [Reverse Proxy Setup](/docs/v4/guides/reverse-proxy) diff --git a/src/routes/(docs)/docs/content/v4/guides/reverse-proxy.md b/src/routes/(docs)/docs/content/v4/guides/reverse-proxy.md index b3dec516..2ef20517 100644 --- a/src/routes/(docs)/docs/content/v4/guides/reverse-proxy.md +++ b/src/routes/(docs)/docs/content/v4/guides/reverse-proxy.md @@ -19,6 +19,8 @@ Running Kener behind a reverse proxy is the recommended approach for production ## Before You Begin {#before-you-begin} +If you specifically need `/status` (or any subpath) deployment, follow [Base Path Deployment](/docs/v4/guides/base-path) first, then return here for your proxy-specific config. + ### Prerequisites {#prerequisites} - Kener running on port 3000 (or custom port)