chore: Update environment variables and documentation for ORIGIN requirement; refactor Docker setup for documentation indexing

This commit is contained in:
Raj Nandan Sharma
2026-02-23 11:15:19 +05:30
parent 3363e90007
commit 9199b807d5
15 changed files with 190 additions and 117 deletions
+1
View File
@@ -1,2 +1,3 @@
KENER_SECRET_KEY=some_secret_key_for_kener
REDIS_URL=redis://localhost:6379
ORIGIN=http://localhost:3000
+92
View File
@@ -0,0 +1,92 @@
name: Publish Main Docker Image (with Docs)
on:
push:
branches:
- main
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: main-docker-${{ github.ref }}
cancel-in-progress: true
jobs:
build-and-push-main:
name: Build and push main Docker images (with docs)
strategy:
matrix:
variant: [debian, alpine]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
steps:
- name: Check out the repo
uses: actions/checkout@v4.2.2
- 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: 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=main-with-docs,enable=${{ matrix.variant == 'debian' }}
type=raw,value=main-with-docs-alpine,enable=${{ matrix.variant == 'alpine' }}
type=sha,format=short,prefix=main-with-docs-,enable=${{ matrix.variant == 'debian' }}
type=sha,format=short,prefix=main-with-docs-alpine-,enable=${{ matrix.variant == 'alpine' }}
- 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
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}
+38 -7
View File
@@ -7,10 +7,12 @@
# Build:
# docker build -t kener . # Alpine (default)
# docker build -t kener --build-arg VARIANT=debian . # Debian Slim
# docker build -t kener --build-arg WITH_DOCS=true . # Include docs
#
# Run:
# docker run -d -p 3000:3000 \
# -e KENER_SECRET_KEY=<secret> \
# -e ORIGIN=http://localhost:3000 \
# -e REDIS_URL=redis://<host>:6379 \
# -v kener_db:/app/database \
# kener
@@ -18,6 +20,7 @@
ARG NODE_VERSION=24
ARG VARIANT=alpine
ARG WITH_DOCS=false
# =============================================================================
# STAGE 1 — BUILDER (installs deps, compiles native modules, builds app)
@@ -62,14 +65,35 @@ COPY . .
# 4. Create directories that the app expects
RUN mkdir -p database
# 5. Remove docs routes before build (avoids EXDEV rename error in overlayfs)
# and clean .svelte-kit so stale route types don't persist
RUN rm -rf src/routes/\(docs\) .svelte-kit
# 5. Conditionally remove docs routes before build
# (avoids EXDEV rename error in overlayfs; clean .svelte-kit so stale
# route types don't persist)
ARG WITH_DOCS
RUN if [ "$WITH_DOCS" != "true" ]; then \
rm -rf src/routes/\(docs\) .svelte-kit; \
fi
# 6. Build: SvelteKit (vite) + server bundle (esbuild)
RUN npm run build
# Use build-with-docs when docs are enabled
RUN if [ "$WITH_DOCS" = "true" ]; then \
npm run build-with-docs; \
else \
npm run build; \
fi
# 7. Remove devDependencies from node_modules
# 7. Stage docs runtime files for index-docs (empty dir when docs disabled)
RUN mkdir -p /docs-runtime && \
if [ "$WITH_DOCS" = "true" ]; then \
mkdir -p /docs-runtime/scripts && \
mkdir -p /docs-runtime/src/lib && \
mkdir -p "/docs-runtime/src/routes/(docs)/docs" && \
cp scripts/index-docs.ts /docs-runtime/scripts/ && \
cp src/lib/marked.ts /docs-runtime/src/lib/ && \
cp "src/routes/(docs)/docs.json" "/docs-runtime/src/routes/(docs)/" && \
cp -r "src/routes/(docs)/docs/content" "/docs-runtime/src/routes/(docs)/docs/"; \
fi
# 8. Remove devDependencies from node_modules
RUN npm prune --omit=dev
# =============================================================================
@@ -137,6 +161,13 @@ COPY --chown=node:node --from=builder /app/src/lib/server/templates/general
# Build output (SvelteKit client/server + esbuild main.js) — changes most often
COPY --chown=node:node --from=builder /app/build ./build
# Docs runtime files (index-docs script + markdown sources; empty when WITH_DOCS=false)
COPY --chown=node:node --from=builder /docs-runtime/ ./
# Entrypoint script (runs index-docs on startup when docs are bundled)
COPY --chown=node:node docker-entrypoint.sh ./docker-entrypoint.sh
RUN chmod +x docker-entrypoint.sh
# ---- Runtime configuration ----
# Switch to non-root user
@@ -148,5 +179,5 @@ EXPOSE ${PORT}
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
CMD sh -c 'curl -sf http://localhost:${PORT}${KENER_BASE_PATH}/healthcheck || exit 1'
ENTRYPOINT ["node"]
CMD ["build/main.js"]
ENTRYPOINT ["./docker-entrypoint.sh"]
CMD ["node", "build/main.js"]
+4 -2
View File
@@ -78,14 +78,14 @@ git clone https://github.com/rajnandan1/kener.git
cd kener
# Uses docker-compose.yml (includes Redis + Kener)
# Set a strong KENER_SECRET_KEY in docker-compose.yml before first run
# Set a strong KENER_SECRET_KEY and ORIGIN in docker-compose.yml before first run
docker compose up -d
```
Open `http://localhost:3000`.
> [!IMPORTANT]
> Set a strong `KENER_SECRET_KEY` before starting for the first time.
> Set a strong `KENER_SECRET_KEY` and set `ORIGIN` to your public URL before starting for the first time.
Use `docker-compose.dev.yml` when you want to build from local source instead of pulling the published image:
@@ -113,6 +113,7 @@ docker run -d \
-p 3000:3000 \
-v "$(pwd)/database:/app/database" \
-e "KENER_SECRET_KEY=replace_with_a_random_string" \
-e "ORIGIN=http://localhost:3000" \
-e "REDIS_URL=redis://host.docker.internal:6379" \
docker.io/rajnandan1/kener:latest
```
@@ -140,6 +141,7 @@ Create a `.env` with at least:
```dotenv
KENER_SECRET_KEY=replace_with_a_random_string
ORIGIN=http://localhost:3000
REDIS_URL=redis://localhost:6379
PORT=3000
```
+2 -2
View File
@@ -36,20 +36,20 @@ services:
args:
VARIANT: alpine # or "debian"
NODE_VERSION: 24
WITH_DOCS: "true"
container_name: kener-dev
environment:
KENER_SECRET_KEY: dev-secret-key-for-local-testing-only
ORIGIN: http://localhost:3000
REDIS_URL: redis://redis:6379
# DATABASE_URL: sqlite://./database/kener.sqlite.db
ports:
- "3000:3000"
volumes:
- dev_data:/app/database
- dev_uploads:/app/uploads
depends_on:
redis:
condition: service_healthy
volumes:
dev_data:
dev_uploads:
+1 -3
View File
@@ -34,6 +34,7 @@ services:
environment:
# ── Required ──
KENER_SECRET_KEY: replace_me_with_a_random_string # generate: openssl rand -base64 32
ORIGIN: http://localhost:3000 # public URL of your Kener instance (required for CSRF protection)
REDIS_URL: redis://redis:6379
# ── Database (default: SQLite) ──
@@ -59,7 +60,6 @@ services:
- "3000:3000"
volumes:
- data:/app/database
- uploads:/app/uploads
depends_on:
redis:
condition: service_healthy
@@ -107,8 +107,6 @@ services:
volumes:
data:
name: kener_db
uploads:
name: kener_uploads
redis_data:
name: kener_redis
# postgres_data:
+11
View File
@@ -0,0 +1,11 @@
#!/bin/sh
set -e
# Index documentation into Redis when docs are bundled in the image
if [ -f /app/scripts/index-docs.ts ]; then
echo "[kener] Indexing documentation into Redis..."
node --experimental-strip-types /app/scripts/index-docs.ts || \
echo "[kener] Warning: docs indexing failed (is REDIS_URL set?). Continuing..."
fi
exec "$@"
+1 -1
View File
@@ -16,7 +16,7 @@ import IORedis from "ioredis";
import dotenv from "dotenv";
import { marked } from "marked";
import plaintify from "marked-plaintify";
import { mdToText } from "../src/lib/marked";
import { mdToText } from "../src/lib/marked.ts";
dotenv.config();
-2
View File
@@ -1,5 +1,4 @@
import { handler } from "../build/handler.js";
import { apiReference } from "@scalar/express-api-reference";
import dotenv from "dotenv";
dotenv.config();
import express from "express";
@@ -7,7 +6,6 @@ import Startup from "../src/lib/server/startup.ts";
import shutdownSchedulers from "../src/lib/server/schedulers/shutdown.ts";
import shutdownQueues from "../src/lib/server/queues/shutdown.ts";
import dbInstance from "../src/lib/server/db/db.ts";
import fs from "fs-extra";
import knex from "knex";
import knexOb from "../knexfile.js";
@@ -13,14 +13,14 @@ The fastest way to get started is with Docker Compose.
git clone https://github.com/rajnandan1/kener.git
cd kener
# Update KENER_SECRET_KEY in docker-compose.yml before first run
# Update KENER_SECRET_KEY and ORIGIN in docker-compose.yml before first run
docker compose up -d
```
Kener will be available at `http://localhost:3000`.
> [!IMPORTANT]
> Set a strong value for `KENER_SECRET_KEY` in `docker-compose.yml` before starting.
> Set a strong value for `KENER_SECRET_KEY` and set `ORIGIN` to your public URL in `docker-compose.yml` before starting.
### Run pre-built image {#run-pre-built-image-docker-hub-or-ghcr}
@@ -57,6 +57,7 @@ Minimum `.env` for Docker:
```dotenv
KENER_SECRET_KEY=replace_with_a_random_string
ORIGIN=http://localhost:3000
REDIS_URL=redis://host.docker.internal:6379
PORT=3000
```
@@ -70,6 +71,7 @@ docker run -d \
-p 3000:3000 \
-v "$(pwd)/database:/app/database" \
-e "KENER_SECRET_KEY=replace_with_a_random_string" \
-e "ORIGIN=http://localhost:3000" \
-e "REDIS_URL=redis://host.docker.internal:6379" \
docker.io/rajnandan1/kener:latest
```
@@ -122,6 +124,7 @@ Create or update your `.env`:
```dotenv
KENER_SECRET_KEY=replace_with_a_random_string
ORIGIN=http://localhost:3000
REDIS_URL=redis://localhost:6379
PORT=3000
# Optional (defaults to SQLite):
@@ -44,6 +44,38 @@ node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
> [!WARNING]
> Without `KENER_SECRET_KEY`, Kener will use a default key which is **not secure for production**. You'll see warnings in the console if running without this variable.
### ORIGIN {#origin}
**Purpose**: The public-facing URL of your Kener instance. Required by SvelteKit for CSRF protection in production.
**Why It's Required**: In production builds, SvelteKit validates that POST form submissions originate from the same site. Without `ORIGIN`, all form submissions (login, signup, settings, etc.) will fail with a **"Cross-site POST form submissions are forbidden"** error.
**Requirements**:
- Must include protocol (`http://` or `https://`)
- Must match the URL users access in their browser
- No trailing slash
- Not needed during local development (`vite dev` infers it automatically)
**Examples**:
```bash
# Local Docker testing
ORIGIN=http://localhost:3000
# Production
ORIGIN=https://status.example.com
# With custom port
ORIGIN=https://status.example.com:8443
# With base path
ORIGIN=https://example.com
```
> [!CAUTION]
> Without `ORIGIN`, **all form submissions will be rejected** in production. This includes login, signup, and admin panel actions. Always set this variable when deploying.
## Optional Variables {#optional-variables}
### KENER_BASE_PATH {#kener-base-path}
@@ -1,64 +0,0 @@
<script lang="ts">
import DotsIcon from "@lucide/svelte/icons/camera";
import FolderIcon from "@lucide/svelte/icons/folder";
import Share3Icon from "@lucide/svelte/icons/share";
import TrashIcon from "@lucide/svelte/icons/trash";
import type { Component } from "svelte";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
let { items }: { items: { name: string; url: string; icon: Component }[] } = $props();
const sidebar = Sidebar.useSidebar();
</script>
<Sidebar.Group class="group-data-[collapsible=icon]:hidden">
<Sidebar.GroupLabel>Documents</Sidebar.GroupLabel>
<Sidebar.Menu>
{#each items as item (item.name)}
<Sidebar.MenuItem>
<Sidebar.MenuButton>
{#snippet child({ props })}
<a {...props} href={item.url}>
<item.icon />
<span>{item.name}</span>
</a>
{/snippet}
</Sidebar.MenuButton>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Sidebar.MenuAction {...props} showOnHover class="data-[state=open]:bg-accent rounded-sm">
<DotsIcon />
<span class="sr-only">More</span>
</Sidebar.MenuAction>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content
class="w-24 rounded-lg"
side={sidebar.isMobile ? "bottom" : "right"}
align={sidebar.isMobile ? "end" : "start"}
>
<DropdownMenu.Item>
<FolderIcon />
<span>Open</span>
</DropdownMenu.Item>
<DropdownMenu.Item>
<Share3Icon />
<span>Share</span>
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item variant="destructive">
<TrashIcon />
<span>Delete</span>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</Sidebar.MenuItem>
{/each}
<Sidebar.MenuItem>
<Sidebar.MenuButton class="text-sidebar-foreground/70">
<DotsIcon class="text-sidebar-foreground/70" />
<span>More</span>
</Sidebar.MenuButton>
</Sidebar.MenuItem>
</Sidebar.Menu>
</Sidebar.Group>
@@ -1,7 +1,4 @@
<script lang="ts">
import CirclePlusFilledIcon from "@lucide/svelte/icons/circle-plus";
import MailIcon from "@lucide/svelte/icons/mail";
import { Button } from "$lib/components/ui/button/index.js";
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
import { page } from "$app/state";
import type { Component } from "svelte";
@@ -1,30 +0,0 @@
<script lang="ts">
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
import type { WithoutChildren } from "$lib/utils.js";
import type { Component, ComponentProps } from "svelte";
let {
items,
...restProps
}: { items: { title: string; url: string; icon: Component }[] } & WithoutChildren<
ComponentProps<typeof Sidebar.Group>
> = $props();
</script>
<Sidebar.Group {...restProps}>
<Sidebar.GroupContent>
<Sidebar.Menu>
{#each items as item (item.title)}
<Sidebar.MenuItem>
<Sidebar.MenuButton>
{#snippet child({ props })}
<a href={item.url} {...props}>
<item.icon />
<span>{item.title}</span>
</a>
{/snippet}
</Sidebar.MenuButton>
</Sidebar.MenuItem>
{/each}
</Sidebar.Menu>
</Sidebar.GroupContent>
</Sidebar.Group>
@@ -20,12 +20,14 @@
href={clientResolver(resolve, "/")}
variant="secondary"
size="sm"
class="dark:text-foreground hidden sm:flex"
target="_blank"
rel="noopener noreferrer"
>
Status Page
</Button>
<Button href="https://kener.ing/docs" variant="secondary" size="sm" target="_blank" rel="noopener noreferrer">
Documentation
</Button>
</div>
</div>
</header>