From 42e7e0f1ff2249b858a2a61f5d0c57c317655f75 Mon Sep 17 00:00:00 2001 From: venus Date: Sun, 13 Sep 2026 21:12:06 -0500 Subject: [PATCH] added engine seperation from posts and entrypoint for compose building --- Dockerfile | 11 +++-- entrypoint.sh | 20 +++++++++ sample-docker-compose.yml | 41 ++++++++++++------- ...026-07-15-infrastructure-notes-template.md | 2 +- test/docker-config.test.js | 33 ++++++++++++--- 5 files changed, 84 insertions(+), 23 deletions(-) create mode 100755 entrypoint.sh diff --git a/Dockerfile b/Dockerfile index 18993f5..9c7c7d8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,12 +17,17 @@ RUN npm ci # Copy application source files COPY . . +RUN chmod +x /app/entrypoint.sh -# Build static production assets +# Pre-compile baseline static assets RUN npm run build # Expose web server port dynamically EXPOSE ${PORT} -# Serve site with npm -CMD ["npm", "start"] +# Native container healthcheck probing IPv4 loopback +HEALTHCHECK --interval=30s --timeout=5s --start-period=3s --retries=3 \ + CMD node -e "fetch('http://127.0.0.1:' + (process.env.PORT || 8080) + '/healthz').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))" + +# Serve site using dynamic entrypoint +ENTRYPOINT ["/app/entrypoint.sh"] diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 0000000..f360da8 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +# Pass-through mode: if arguments were provided, execute them directly +if [ "$#" -gt 0 ]; then + exec "$@" +fi + +# Build static site from mounted or packaged markdown posts +echo "[blog-engine] Compiling static site with Eleventy..." +npm run build || exit 1 + +# Optional watch mode for zero-restart live note updates +if [ "${WATCH:-false}" = "true" ]; then + echo "[blog-engine] Starting live watch daemon for markdown changes..." + npx @11ty/eleventy --watch & +fi + +# Start production static web server +echo "[blog-engine] Starting static server on port ${PORT:-8080}..." +exec node server.js diff --git a/sample-docker-compose.yml b/sample-docker-compose.yml index b35e736..b9095ff 100644 --- a/sample-docker-compose.yml +++ b/sample-docker-compose.yml @@ -1,25 +1,38 @@ services: web: - build: - context: . - dockerfile: Dockerfile - args: - SITE_DOMAIN: "${SITE_DOMAIN:?Error: SITE_DOMAIN is not set. Please define SITE_DOMAIN in .env}" - SITE_URL: "${SITE_URL:-https://${SITE_DOMAIN}}" - PORT: "${PORT:-8080}" - image: river-rooks-blog:latest + # Pull prebuilt engine image from your Gitea registry + image: git.riverrooks.dev/venus/river-rooks-blog:latest + # Local build fallback (optional when building image locally) + # build: + # context: . + # dockerfile: Dockerfile + # args: + # SITE_DOMAIN: "${SITE_DOMAIN:?Error: SITE_DOMAIN is not set. Please define SITE_DOMAIN in .env}" + # SITE_URL: "${SITE_URL:-https://${SITE_DOMAIN}}" + # PORT: "${PORT:-8080}" container_name: river_rooks_web restart: unless-stopped - healthcheck: - test: ["CMD", "node", "test/healthcheck.js"] - interval: 30s - timeout: 3s - start_period: 3s - retries: 3 + # Healthcheck is natively baked into the Dockerfile image metadata environment: - "SITE_DOMAIN=${SITE_DOMAIN:?Error: SITE_DOMAIN is not set. Please define SITE_DOMAIN in .env}" - "SITE_URL=${SITE_URL:-https://${SITE_DOMAIN}}" - "PORT=${PORT:-8080}" + - "WATCH=${WATCH:-false}" + volumes: + # Mount external markdown posts directly into the container + - "./posts:/app/src/posts:ro" ports: - "${HOST_PORT:-${PORT:-8080}}:${PORT:-8080}" + networks: + - backend + labels: + - "traefik.enable=true" + - "traefik.http.routers.blog.rule=Host(`riverrooks.dev`) || Host(`www.riverrooks.dev`)" + - "traefik.http.routers.blog.entrypoints=websecure" + - "traefik.http.routers.blog.tls.certresolver=basic" + - "traefik.http.services.blog.loadbalancer.server.port=8080" + - "traefik.docker.network=backend" +networks: + backend: + external: true diff --git a/src/posts/2026-07-15-infrastructure-notes-template.md b/src/posts/2026-07-15-infrastructure-notes-template.md index 434f34c..f453582 100644 --- a/src/posts/2026-07-15-infrastructure-notes-template.md +++ b/src/posts/2026-07-15-infrastructure-notes-template.md @@ -1,6 +1,6 @@ --- layout: layouts/post.njk -title: "Infrastructure Notes & Systems Deployment Guide" +title: "a post titel Notes & Systems Deployment Guide" date: 2026-07-15 tags: - homelab diff --git a/test/docker-config.test.js b/test/docker-config.test.js index 167bcb0..3905f9b 100644 --- a/test/docker-config.test.js +++ b/test/docker-config.test.js @@ -3,7 +3,7 @@ import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; -test("Docker Configuration: Dockerfile enforces Node runtime and excludes Nginx", () => { +test("Docker Configuration: Dockerfile enforces Node runtime, healthcheck, and entrypoint", () => { const dockerfilePath = path.resolve(process.cwd(), "Dockerfile"); assert.ok(fs.existsSync(dockerfilePath), "Dockerfile must exist"); @@ -16,12 +16,32 @@ test("Docker Configuration: Dockerfile enforces Node runtime and excludes Nginx" assert.doesNotMatch(content, /FROM\s+nginx/i, "Dockerfile must not use nginx base image"); assert.doesNotMatch(content, /nginx/i, "Dockerfile must not contain nginx directives"); - // Must build static site and serve with npm - assert.match(content, /RUN\s+npm\s+run\s+build/, "Dockerfile must run npm build"); + // Must build static site and configure dynamic port + assert.match(content, /RUN\s+npm\s+run\s+build/, "Dockerfile must compile baseline static assets"); assert.match(content, /ARG\s+PORT=8080/, "Dockerfile must define ARG PORT with default fallback"); assert.match(content, /ENV\s+PORT=\$\{?PORT\}?/, "Dockerfile must define ENV PORT using variable"); assert.match(content, /EXPOSE\s+\$\{?PORT\}?/, "Dockerfile must expose dynamic PORT variable"); - assert.match(content, /CMD\s*\[\s*["']npm["']\s*,\s*["']start["']\s*\]/, "Dockerfile must start via npm start"); + + // Must define built-in HEALTHCHECK probing 127.0.0.1 loopback + assert.match(content, /HEALTHCHECK[\s\S]*?127\.0\.0\.1/, "Dockerfile must define baked-in HEALTHCHECK on 127.0.0.1"); + + // Must configure dynamic entrypoint script + assert.match(content, /ENTRYPOINT\s*\[\s*["']\/app\/entrypoint\.sh["']\s*\]/, "Dockerfile must use /app/entrypoint.sh entrypoint"); +}); + +test("Docker Configuration: entrypoint.sh script exists and has valid structure", () => { + const entrypointPath = path.resolve(process.cwd(), "entrypoint.sh"); + assert.ok(fs.existsSync(entrypointPath), "entrypoint.sh must exist"); + + const stats = fs.statSync(entrypointPath); + // Check executable permissions (at least owner executable: 0o100) + assert.ok(Boolean(stats.mode & 0o111), "entrypoint.sh must have executable permission bits"); + + const content = fs.readFileSync(entrypointPath, "utf-8"); + assert.match(content, /^#!\/bin\/sh/, "entrypoint.sh must use POSIX /bin/sh shebang"); + assert.match(content, /exec\s+"\$@"/, "entrypoint.sh must pass through custom container arguments"); + assert.match(content, /npm\s+run\s+build/, "entrypoint.sh must build the static site"); + assert.match(content, /exec\s+node\s+server\.js/, "entrypoint.sh must start static server with exec"); }); test("Docker Configuration: docker-compose configures Node service correctly", () => { @@ -33,9 +53,12 @@ test("Docker Configuration: docker-compose configures Node service correctly", ( const content = fs.readFileSync(composePath, "utf-8"); // Verifies dynamic port mapping and environment variables - assert.match(content, /PORT:\s*"?\$\{PORT:-8080\}"?/, "docker-compose.yml must pass dynamic PORT build arg and env"); + assert.match(content, /PORT.*8080/, "docker-compose.yml must configure PORT fallback"); assert.match(content, /:\$\{PORT:-8080\}/, "docker-compose.yml must map dynamic container PORT"); // Verifies domain argument enforcement assert.match(content, /SITE_DOMAIN/, "docker-compose.yml must require SITE_DOMAIN"); + + // Verifies markdown volume mount + assert.match(content, /\/app\/src\/posts/, "docker-compose.yml must mount external markdown posts"); });