chore: Enhance Docker and documentation for subpath deployment support

This commit is contained in:
Raj Nandan Sharma
2026-02-24 11:20:49 +05:30
parent 7e7c0eb429
commit b970c5bba1
10 changed files with 411 additions and 1 deletions
+97
View File
@@ -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 }}
+143
View File
@@ -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}
+6
View File
@@ -21,6 +21,7 @@
ARG NODE_VERSION=24 ARG NODE_VERSION=24
ARG VARIANT=alpine ARG VARIANT=alpine
ARG WITH_DOCS=false ARG WITH_DOCS=false
ARG KENER_BASE_PATH=
# ============================================================================= # =============================================================================
# STAGE 1 — BUILDER (installs deps, compiles native modules, builds app) # STAGE 1 — BUILDER (installs deps, compiles native modules, builds app)
@@ -52,6 +53,9 @@ ENV NPM_CONFIG_LOGLEVEL=error
WORKDIR /app WORKDIR /app
ARG KENER_BASE_PATH
ENV KENER_BASE_PATH=${KENER_BASE_PATH}
# 1. Copy package manifests first (maximises layer cache hits) # 1. Copy package manifests first (maximises layer cache hits)
COPY package*.json ./ COPY package*.json ./
@@ -126,9 +130,11 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
FROM final-${VARIANT} AS final FROM final-${VARIANT} AS final
ARG PORT=3000 ARG PORT=3000
ARG KENER_BASE_PATH=
ENV NODE_ENV=production \ ENV NODE_ENV=production \
PORT=${PORT} \ PORT=${PORT} \
KENER_BASE_PATH=${KENER_BASE_PATH} \
TZ=UTC \ TZ=UTC \
# Required so Node can import .ts migration/seed files at runtime # Required so Node can import .ts migration/seed files at runtime
NODE_OPTIONS="--experimental-strip-types" NODE_OPTIONS="--experimental-strip-types"
+25
View File
@@ -106,6 +106,13 @@ You can use either image:
- `docker.io/rajnandan1/kener:latest` - `docker.io/rajnandan1/kener:latest`
- `ghcr.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 ```bash
mkdir -p database mkdir -p database
docker run -d \ docker run -d \
@@ -118,6 +125,24 @@ docker run -d \
docker.io/rajnandan1/kener:latest 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 ### Run without Docker
Requirements: Requirements:
+1
View File
@@ -37,6 +37,7 @@ services:
VARIANT: alpine # or "debian" VARIANT: alpine # or "debian"
NODE_VERSION: 24 NODE_VERSION: 24
WITH_DOCS: "true" WITH_DOCS: "true"
KENER_BASE_PATH: ""
container_name: kener-dev container_name: kener-dev
environment: environment:
KENER_SECRET_KEY: dev-secret-key-for-local-testing-only KENER_SECRET_KEY: dev-secret-key-for-local-testing-only
+47
View File
@@ -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
+1 -1
View File
@@ -78,7 +78,7 @@
}); });
</script> </script>
<div class="theme-plus-bar sticky top-18 z-20 flex w-full items-center gap-2 overflow-x-auto rounded py-2"> <div class="theme-plus-bar scrollbar-hidden sticky top-18 z-20 flex w-full items-center gap-2 rounded py-2">
<PageSelector /> <PageSelector />
<div class="ml-auto flex shrink-0 items-center gap-2"> <div class="ml-auto flex shrink-0 items-center gap-2">
{#if page.data.isSubsEnabled && page.data.canSendEmail} {#if page.data.isSubsEnabled && page.data.canSendEmail}
+4
View File
@@ -240,6 +240,10 @@
{ {
"title": "Reverse Proxy Setup", "title": "Reverse Proxy Setup",
"content": "v4/guides/reverse-proxy" "content": "v4/guides/reverse-proxy"
},
{
"title": "Base Path Deployment",
"content": "v4/guides/base-path"
} }
] ]
} }
@@ -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)
@@ -19,6 +19,8 @@ Running Kener behind a reverse proxy is the recommended approach for production
## Before You Begin {#before-you-begin} ## 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} ### Prerequisites {#prerequisites}
- Kener running on port 3000 (or custom port) - Kener running on port 3000 (or custom port)