Compare commits
1
Commits
server
...
42e7e0f1ff
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42e7e0f1ff |
+8
-3
@@ -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"]
|
||||
|
||||
Executable
+20
@@ -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
|
||||
+27
-14
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user