Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
187f83259e | ||
|
|
48a55bef93 | ||
|
|
aaf6fe4d53 | ||
|
|
31c0c5e6bc | ||
|
|
33ab118bcc | ||
|
|
8a49517528 | ||
|
|
a4e581e5eb | ||
|
|
38ae0c8c89 | ||
|
|
063662b926 | ||
|
|
3f0d622db0 | ||
|
|
961226dc7f | ||
|
|
08d5c118d3 |
+19
-24
@@ -1,26 +1,21 @@
|
||||
FROM nginx:1.15.2-alpine
|
||||
# The photos admin client, built by the mesh from this repository (novox/hq ADR 0069). The same
|
||||
# shape as client/Dockerfile — see the notes there; only the source directory differs.
|
||||
#
|
||||
# The build context is the repository root; every COPY says so.
|
||||
ARG NODE_BASE
|
||||
|
||||
# Nginx config
|
||||
RUN rm -rf /etc/nginx/conf.d
|
||||
COPY ./conf /etc/nginx
|
||||
FROM ${NODE_BASE} AS build
|
||||
WORKDIR /build
|
||||
COPY admin-client/package.json admin-client/package-lock.json ./
|
||||
RUN npm ci
|
||||
COPY admin-client/public ./public
|
||||
COPY admin-client/src ./src
|
||||
COPY admin-client/.env ./.env
|
||||
RUN npm run build
|
||||
|
||||
# Static build
|
||||
COPY ./build /usr/share/nginx/html/
|
||||
|
||||
# Default port exposure
|
||||
EXPOSE 80
|
||||
|
||||
# Copy .env file and shell script to container
|
||||
WORKDIR /usr/share/nginx/html
|
||||
|
||||
COPY scripts/env.sh .
|
||||
COPY .env .
|
||||
|
||||
# Add bash
|
||||
RUN apk add --no-cache bash
|
||||
|
||||
# Make our shell script executable
|
||||
RUN chmod +x env.sh
|
||||
|
||||
# Start Nginx server
|
||||
CMD ["/bin/bash", "-c", "/usr/share/nginx/html/env.sh && nginx -g \"daemon off;\""]
|
||||
FROM ${NODE_BASE}
|
||||
ENV NODE_ENV=production
|
||||
COPY client-server/serve.mjs /serve.mjs
|
||||
COPY --from=build /build/build /site/
|
||||
COPY admin-client/.env /site/.env
|
||||
CMD ["node", "/serve.mjs"]
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
// The client sites' server: static files, an SPA fallback, and env-config.js from the
|
||||
// environment. Node built-ins only — the runtime base already carries node for the API, so a
|
||||
// second web server (and the shell its startup script needed) would be two more moving parts to
|
||||
// serve files the first one serves fine.
|
||||
//
|
||||
// What the nginx arrangement this replaces did, it does: every unknown path without an extension
|
||||
// falls back to index.html (client-side routing), nothing is cached (the site is tiny and a stale
|
||||
// index.html after a deploy is the only caching bug this app has ever had), and env-config.js is
|
||||
// regenerated from the container's environment at start — which is how one built image serves
|
||||
// under any API URL.
|
||||
import { createServer } from "node:http";
|
||||
import { readFileSync, writeFileSync, createReadStream, statSync } from "node:fs";
|
||||
import { join, resolve, extname } from "node:path";
|
||||
|
||||
const root = resolve(process.env.HTML_ROOT ?? "/site");
|
||||
const port = Number(process.env.PORT ?? 80);
|
||||
|
||||
// .env names the variables the site reads; the environment overrides their values. Written once
|
||||
// at start, exactly as the env.sh this replaces did.
|
||||
const pairs = [];
|
||||
for (const line of readFileSync(join(root, ".env"), "utf8").split("\n")) {
|
||||
const at = line.indexOf("=");
|
||||
if (at < 1) continue;
|
||||
const name = line.slice(0, at).trim();
|
||||
pairs.push(` ${name}: ${JSON.stringify(process.env[name] ?? line.slice(at + 1).trim())},`);
|
||||
}
|
||||
writeFileSync(join(root, "env-config.js"), `window._env_ = {\n${pairs.join("\n")}\n}\n`);
|
||||
|
||||
const types = {
|
||||
".html": "text/html; charset=utf-8",
|
||||
".js": "application/javascript",
|
||||
".mjs": "application/javascript",
|
||||
".css": "text/css",
|
||||
".json": "application/json",
|
||||
".map": "application/json",
|
||||
".svg": "image/svg+xml",
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".gif": "image/gif",
|
||||
".webp": "image/webp",
|
||||
".ico": "image/x-icon",
|
||||
".txt": "text/plain; charset=utf-8",
|
||||
".woff": "font/woff",
|
||||
".woff2": "font/woff2",
|
||||
".ttf": "font/ttf",
|
||||
};
|
||||
|
||||
createServer((request, response) => {
|
||||
const asked = decodeURIComponent(new URL(request.url, "http://x").pathname);
|
||||
// resolve() collapses any ".." before the prefix check, so a path cannot escape the root.
|
||||
let path = resolve(join(root, asked));
|
||||
if (!path.startsWith(root)) {
|
||||
response.writeHead(403).end();
|
||||
return;
|
||||
}
|
||||
let served;
|
||||
try {
|
||||
served = statSync(path);
|
||||
if (served.isDirectory()) {
|
||||
path = join(path, "index.html");
|
||||
served = statSync(path);
|
||||
}
|
||||
} catch {
|
||||
// Not a file: a client-side route, answered by the app itself — unless it asked for a file
|
||||
// by extension, where index.html would be a wrong answer dressed as a right one.
|
||||
if (extname(asked) !== "") {
|
||||
response.writeHead(404, { "Content-Type": "text/plain" }).end("not found\n");
|
||||
return;
|
||||
}
|
||||
path = join(root, "index.html");
|
||||
served = statSync(path);
|
||||
}
|
||||
response.writeHead(200, {
|
||||
"Content-Type": types[extname(path)] ?? "application/octet-stream",
|
||||
"Content-Length": served.size,
|
||||
"Cache-Control": "no-cache",
|
||||
});
|
||||
createReadStream(path).pipe(response);
|
||||
}).listen(port, () => console.log(`serving ${root} on :${port}`));
|
||||
+25
-24
@@ -1,26 +1,27 @@
|
||||
FROM nginx:1.15.2-alpine
|
||||
# The photos client, built by the mesh from this repository (novox/hq ADR 0069): the react build
|
||||
# happens here rather than in a CI step that copied its output in.
|
||||
#
|
||||
# One base, named rather than pinned (novox/hq issue 044), declared in module.json's `build.on`:
|
||||
# the site is compiled and served by the same node the API already needs. Serving is
|
||||
# client-server/serve.mjs — static files, the SPA fallback, and env-config.js regenerated from the
|
||||
# container's environment at start, which is how one built image serves under any API URL. The
|
||||
# nginx-and-bash arrangement this replaces did the same with two more moving parts.
|
||||
#
|
||||
# The build context is the repository root; every COPY says so.
|
||||
ARG NODE_BASE
|
||||
|
||||
# Nginx config
|
||||
RUN rm -rf /etc/nginx/conf.d
|
||||
COPY ./conf /etc/nginx
|
||||
FROM ${NODE_BASE} AS build
|
||||
WORKDIR /build
|
||||
COPY client/package.json client/package-lock.json ./
|
||||
RUN npm ci
|
||||
COPY client/public ./public
|
||||
COPY client/src ./src
|
||||
COPY client/.env ./.env
|
||||
RUN npm run build
|
||||
|
||||
# Static build
|
||||
COPY ./build /usr/share/nginx/html/
|
||||
|
||||
# Default port exposure
|
||||
EXPOSE 80
|
||||
|
||||
# Copy .env file and shell script to container
|
||||
WORKDIR /usr/share/nginx/html
|
||||
|
||||
COPY scripts/env.sh .
|
||||
COPY .env .
|
||||
|
||||
# Add bash
|
||||
RUN apk add --no-cache bash
|
||||
|
||||
# Make our shell script executable
|
||||
RUN chmod +x env.sh
|
||||
|
||||
# Start Nginx server
|
||||
CMD ["/bin/bash", "-c", "/usr/share/nginx/html/env.sh && nginx -g \"daemon off;\""]
|
||||
FROM ${NODE_BASE}
|
||||
ENV NODE_ENV=production
|
||||
COPY client-server/serve.mjs /serve.mjs
|
||||
COPY --from=build /build/build /site/
|
||||
COPY client/.env /site/.env
|
||||
CMD ["node", "/serve.mjs"]
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
REACT_APP_API_URL=https://${bound:route:name-api}
|
||||
@@ -0,0 +1,11 @@
|
||||
NODE_ENV=production
|
||||
PORT=9000
|
||||
MONGO_URL=mongodb://${bound:mongodb-database:as}:${secret:mongodb-database}@${bound:mongodb-database:at}:${bound:mongodb-database:port}/${bound:mongodb-database:as}?authSource=${bound:mongodb-database:as}
|
||||
MONGO_DB=${bound:mongodb-database:as}
|
||||
MINIO_ENDPOINT=${bound:s3-bucket:at}
|
||||
MINIO_PORT=${bound:s3-bucket:port}
|
||||
MINIO_BUCKET=${bound:s3-bucket:bucket}
|
||||
MINIO_ACCESSKEY=${bound:s3-bucket:as}
|
||||
MINIO_SECRET=${secret:s3-bucket}
|
||||
MINIO_USE_SSL=false
|
||||
SUPER_ADMIN_KEY=${secret:admin-key}
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/bin/sh
|
||||
# mesh-check-toolchain: typescript
|
||||
#
|
||||
# The photo application's own check (novox/hq ADR 0237): the second layer of a pull request's merge check,
|
||||
# `mesh/repo-check`, run by the build seat in the mesh's TypeScript toolchain — whose Node is the release
|
||||
# this module's images are built on (module.json `build.on`, NODE_BASE). The gate — the manifest through
|
||||
# the controller the mesh runs, every machine composed with it — is the build seat's first layer
|
||||
# (`mesh/merge-gate`), run because the mesh's module graph builds photos from this branch.
|
||||
#
|
||||
# What the three images' builds do, done before the merge rather than after it: the server, the client and
|
||||
# the admin client each installed from its lock file and built, and the client's file server parsed.
|
||||
#
|
||||
# **Said, never passed silently**: nothing is started here — the server wants its object and document
|
||||
# stores — and the application has no tests of its own.
|
||||
set -eu
|
||||
|
||||
for part in server client admin-client; do
|
||||
echo "--- $part: installed from its lock file and built"
|
||||
(cd "$part" && npm ci --no-audit --no-fund --loglevel=error && CI=false npm run build)
|
||||
done
|
||||
node --check client-server/serve.mjs
|
||||
# The file text (novox/hq ADR 0349 §2): no file resource keeps its text inline in module.json; each names its
|
||||
# template in files/, which the build packs into the one archive named files that the mesh reads the text from.
|
||||
node -e '
|
||||
const fs = require("fs");
|
||||
const m = JSON.parse(fs.readFileSync("module.json", "utf8"));
|
||||
const bad = [];
|
||||
for (const r of m.resources || []) {
|
||||
if (r.type !== "file") continue;
|
||||
if ("content" in r) bad.push(`resource ${r.id} keeps its text inline: keep it in files/ and name it as the template`);
|
||||
else if (typeof r.template !== "string" || !r.template.startsWith("files/") || !fs.existsSync(r.template))
|
||||
bad.push(`resource ${r.id} names no template in files/ that is there: ${r.template}`);
|
||||
}
|
||||
const packs = ((m.build || {}).artifacts || []).filter((a) => a.kind === "archive" && a.from === "files" && a.name === "files").length;
|
||||
if (packs !== 1) bad.push(`the build packs files/ into ${packs} archive(s) named files, not one`);
|
||||
if (bad.length) { for (const b of bad) console.error(b); process.exit(1); }
|
||||
console.log("every file takes its text from a template in files/");
|
||||
'
|
||||
|
||||
echo "NOT RUN HERE: the server (it wants its object and document stores); the application has no tests"
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
{
|
||||
"module": "photos",
|
||||
"version": "0.1",
|
||||
"capabilities": [
|
||||
"container-runtime"
|
||||
],
|
||||
"requires": [
|
||||
"s3-bucket",
|
||||
"mongodb-database",
|
||||
"route"
|
||||
],
|
||||
"contributes": {
|
||||
"mongodb-database": {
|
||||
"name": "photos"
|
||||
},
|
||||
"route": {
|
||||
"api": {
|
||||
"label": "photos-api",
|
||||
"endpoint": "api"
|
||||
},
|
||||
"admin": {
|
||||
"label": "photos",
|
||||
"endpoint": "admin"
|
||||
},
|
||||
"eef": {
|
||||
"label": "eef",
|
||||
"endpoint": "eef"
|
||||
},
|
||||
"filip": {
|
||||
"label": "filip",
|
||||
"endpoint": "filip"
|
||||
},
|
||||
"flip": {
|
||||
"label": "flip",
|
||||
"endpoint": "filip"
|
||||
},
|
||||
"filip-api": {
|
||||
"label": "filip-api",
|
||||
"endpoint": "api"
|
||||
}
|
||||
}
|
||||
},
|
||||
"binds": {
|
||||
"s3-bucket": "${dir:state}/store.json",
|
||||
"mongodb-database": "${dir:state}/database.json",
|
||||
"route": "${dir:state}/route.json"
|
||||
},
|
||||
"secrets": {
|
||||
"s3-bucket": "${dir:state}/store.secret",
|
||||
"mongodb-database": "${dir:state}/database.secret"
|
||||
},
|
||||
"own-secrets": {
|
||||
"admin-key": "${dir:state}/admin-key.secret"
|
||||
},
|
||||
"listens": [
|
||||
{
|
||||
"name": "api",
|
||||
"port": 9102,
|
||||
"protocol": "tcp",
|
||||
"from": "mesh",
|
||||
"why": "the photos API over http, which every client site calls; reached by the proxy here under the api and filip-api routes. The machine side of the 9102:9000 mapping"
|
||||
},
|
||||
{
|
||||
"name": "admin",
|
||||
"port": 8102,
|
||||
"protocol": "tcp",
|
||||
"from": "mesh",
|
||||
"why": "the admin client site over http; reached by the proxy here under the photos route"
|
||||
},
|
||||
{
|
||||
"name": "filip",
|
||||
"port": 8103,
|
||||
"protocol": "tcp",
|
||||
"from": "mesh",
|
||||
"why": "the filip client site over http; reached by the proxy here under the filip and flip routes"
|
||||
},
|
||||
{
|
||||
"name": "eef",
|
||||
"port": 8104,
|
||||
"protocol": "tcp",
|
||||
"from": "mesh",
|
||||
"why": "the eef client site over http; reached by the proxy here under the eef route"
|
||||
}
|
||||
],
|
||||
"data": {
|
||||
"kept-by": {
|
||||
"s3-bucket": {
|
||||
"class": "irreplaceable",
|
||||
"why": "the photos themselves, uploaded by the people the sites serve: the operator's ranking (2026-10-06) — whatever their own backups, the mesh protects what it holds"
|
||||
},
|
||||
"mongodb-database": {
|
||||
"class": "irreplaceable",
|
||||
"why": "the albums, captions and order the photos are shown in"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"id": "state",
|
||||
"type": "directory",
|
||||
"mode": "0700",
|
||||
"place": "."
|
||||
},
|
||||
{
|
||||
"id": "server-env",
|
||||
"type": "file",
|
||||
"path": "${dir:state}/server.env",
|
||||
"mode": "0600",
|
||||
"template": "files/server.env"
|
||||
},
|
||||
{
|
||||
"id": "client-env",
|
||||
"type": "file",
|
||||
"path": "${dir:state}/client.env",
|
||||
"mode": "0644",
|
||||
"template": "files/client.env"
|
||||
},
|
||||
{
|
||||
"id": "net",
|
||||
"type": "network",
|
||||
"name": "photos"
|
||||
},
|
||||
{
|
||||
"id": "server",
|
||||
"type": "container",
|
||||
"name": "photos-server",
|
||||
"artifact": "server",
|
||||
"network": "photos",
|
||||
"env-file": [
|
||||
"${dir:state}/server.env"
|
||||
],
|
||||
"ports": [
|
||||
"9102:9000"
|
||||
],
|
||||
"secrets-in-environment": "the application's own code reads MONGO_URL, MINIO_SECRET and SUPER_ADMIN_KEY from the environment (server/src/config.js); converting is this repository's change, tracked but not blocking the mesh conversion"
|
||||
},
|
||||
{
|
||||
"id": "admin",
|
||||
"type": "container",
|
||||
"name": "photos-admin",
|
||||
"artifact": "admin-client",
|
||||
"network": "photos",
|
||||
"env-file": [
|
||||
"${dir:state}/client.env"
|
||||
],
|
||||
"ports": [
|
||||
"8102:80"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "eef",
|
||||
"type": "container",
|
||||
"name": "photos-eef",
|
||||
"artifact": "client",
|
||||
"network": "photos",
|
||||
"env-file": [
|
||||
"${dir:state}/client.env"
|
||||
],
|
||||
"ports": [
|
||||
"8104:80"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "filip",
|
||||
"type": "container",
|
||||
"name": "photos-filip",
|
||||
"artifact": "client",
|
||||
"network": "photos",
|
||||
"env-file": [
|
||||
"${dir:state}/client.env"
|
||||
],
|
||||
"ports": [
|
||||
"8103:80"
|
||||
]
|
||||
}
|
||||
],
|
||||
"build": {
|
||||
"on": [
|
||||
{
|
||||
"arg": "NODE_BASE",
|
||||
"image": "node@sha256:48e4b67d85f87bd551df43704e24d252f56cc5f8e9718841aace50f19948f0f9"
|
||||
}
|
||||
],
|
||||
"artifacts": [
|
||||
{
|
||||
"name": "server",
|
||||
"kind": "image",
|
||||
"from": "server/Dockerfile"
|
||||
},
|
||||
{
|
||||
"name": "client",
|
||||
"kind": "image",
|
||||
"from": "client/Dockerfile"
|
||||
},
|
||||
{
|
||||
"name": "admin-client",
|
||||
"kind": "image",
|
||||
"from": "admin-client/Dockerfile"
|
||||
},
|
||||
{
|
||||
"name": "files",
|
||||
"kind": "archive",
|
||||
"from": "files"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+25
-8
@@ -1,10 +1,27 @@
|
||||
FROM node:21.2.0
|
||||
# The photos API, built by the mesh from this repository (novox/hq ADR 0069): the build happens
|
||||
# here rather than in a CI step that copied its output in — an image the mesh can rebuild from a
|
||||
# commit is one whose contents that commit fully determines.
|
||||
#
|
||||
# The base is named rather than pinned (novox/hq issue 044): declared in module.json's `build.on`,
|
||||
# so the copy the mesh holds answers it. One base for both stages — the runtime stage installs
|
||||
# production dependencies itself (sharp is a native module rollup cannot bundle), so it needs npm
|
||||
# exactly as the build stage does.
|
||||
#
|
||||
# The build context is the repository root; every COPY says so.
|
||||
ARG NODE_BASE
|
||||
|
||||
# Create app directory
|
||||
WORKDIR /usr/src/photos-server.novox.be
|
||||
FROM ${NODE_BASE} AS build
|
||||
WORKDIR /build
|
||||
COPY server/package.json server/package-lock.json ./
|
||||
RUN npm ci
|
||||
COPY server/rollup.config.mjs ./
|
||||
COPY server/src ./src
|
||||
RUN npm run build
|
||||
|
||||
# Copy build output
|
||||
COPY ./dist ./
|
||||
COPY ./node_modules ./node_modules
|
||||
|
||||
CMD ["node", "server.cjs", "--enable-source-maps"]
|
||||
FROM ${NODE_BASE}
|
||||
ENV NODE_ENV=production
|
||||
WORKDIR /app
|
||||
COPY server/package.json server/package-lock.json ./
|
||||
RUN npm ci --omit=dev
|
||||
COPY --from=build /build/dist ./dist
|
||||
CMD ["node", "dist/server.cjs", "--enable-source-maps"]
|
||||
|
||||
Reference in New Issue
Block a user