Feat: Local prod proxy

This commit is contained in:
MangoPig
2026-06-15 05:09:13 +01:00
parent ddd25b6eb3
commit 354dbc849b
12 changed files with 197 additions and 23 deletions

13
.dockerignore Normal file
View File

@@ -0,0 +1,13 @@
.DS_Store
.git
.gitignore
**/.pnpm-store
**/.output
**/dist
**/node_modules
Commands
Docker
Documentation
Env

View File

@@ -1,11 +1,32 @@
project_root := justfile_directory() project_root := justfile_directory()
frontend_dir := project_root + "/Frontend" proxy_bake := project_root + "/Proxy/docker-bake.hcl"
frontend_bake := project_root + "/Frontend/docker-bake.hcl" local_compose := project_root + "/Docker/docker-compose.local.prod.yaml"
# Build the Frontend production image locally. # Build the local production proxy image locally.
build: build:
cd '{{frontend_dir}}' && docker buildx bake -f '{{frontend_bake}}' prod cd '{{project_root}}' && docker buildx bake -f '{{proxy_bake}}' prod
# Rebuild the Frontend production image locally. # Start the local production stack in the background using the current image.
up:
docker compose -f '{{local_compose}}' up -d --remove-orphans --force-recreate
# Build first, then start the local production stack in the background.
start: build
docker compose -f '{{local_compose}}' up -d --remove-orphans --force-recreate
# Rebuild the local production proxy image locally.
rebuild: rebuild:
cd '{{frontend_dir}}' && docker buildx bake -f '{{frontend_bake}}' --set '*.no-cache=true' prod cd '{{project_root}}' && docker buildx bake -f '{{proxy_bake}}' --set '*.no-cache=true' prod
docker compose -f '{{local_compose}}' up -d --remove-orphans --force-recreate
# Stop and remove the local production stack.
down:
docker compose -f '{{local_compose}}' down --remove-orphans --volumes
# Follow logs for the local production stack.
logs:
docker compose -f '{{local_compose}}' logs -f
# Restart the local production stack.
restart:
docker compose -f '{{local_compose}}' restart

View File

@@ -1 +1,7 @@
# Reserved for a future local production compose stack. services:
proxy:
image: moku/work-proxy:local-prod
container_name: moku-work-proxy-local
restart: unless-stopped
ports:
- "8080:80"

View File

@@ -11,7 +11,15 @@
- [ ] Project-Structure - [ ] Project-Structure
- [ ] Stack-Decisions - [ ] Stack-Decisions
- [ ] Proxy - [ ] Proxy
- [ ] Local-Prod-NGINX-Proxy
- [ ] Static-Frontend-Serving
- [ ] Dev-and-Prod-Builds - [ ] Dev-and-Prod-Builds
- [x] Local-Dev-Just-Commands
- [x] Local-Dev-Docker-Compose
- [ ] Local-Prod-Just-Commands
- [ ] Local-Prod-Docker-Compose
- [ ] Frontend-Production-Dockerfile
- [ ] Frontend-docker-bake
#### Backend #### Backend

View File

@@ -1,5 +1,8 @@
# syntax=docker/dockerfile:1.7 # syntax=docker/dockerfile:1.7
# Frontend development image only.
# Production static serving is owned by Proxy/Local/Dockerfile.
FROM node:22-alpine AS base FROM node:22-alpine AS base
WORKDIR /app WORKDIR /app

View File

@@ -17,30 +17,18 @@ target "dev" {
tags = ["moku/work-frontend:dev"] tags = ["moku/work-frontend:dev"]
} }
target "prod" {
inherits = ["_app"]
target = "production"
tags = ["moku/work-frontend:prod"]
}
target "dev-image" { target "dev-image" {
inherits = ["_app"] inherits = ["_app"]
target = "development" target = "development"
tags = ["${REGISTRY}/moku/work-frontend:dev-${TAG}"] tags = ["${REGISTRY}/moku/work-frontend:dev-${TAG}"]
} }
target "prod-image" {
inherits = ["_app"]
target = "production"
tags = ["${REGISTRY}/moku/work-frontend:prod-${TAG}"]
}
group "local" { group "local" {
targets = ["dev", "prod"] targets = ["dev"]
} }
group "registry" { group "registry" {
targets = ["dev-image", "prod-image"] targets = ["dev-image"]
} }
group "default" { group "default" {

View File

@@ -11,6 +11,7 @@
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
"build": "vite build", "build": "vite build",
"build:static": "pnpm build && node ./scripts/render-static-index.mjs",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"start": "vite preview", "start": "vite preview",
"preview": "vite preview" "preview": "vite preview"

View File

@@ -0,0 +1,49 @@
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
const manifestPath = resolve("dist/client/.vite/manifest.json");
const outputPath = resolve("dist/client/index.html");
const themeBootstrapScript = `
(() => {
try {
const storageKey = "theme";
const stored = localStorage.getItem(storageKey);
const theme = stored === "light" || stored === "dark"
? stored
: (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
document.documentElement.setAttribute("data-theme", theme);
} catch {
document.documentElement.setAttribute("data-theme", "light");
}
})();
`.trim();
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
const entry = manifest["src/entry-client.tsx"];
if (!entry?.file) {
throw new Error("Could not find src/entry-client.tsx in the client manifest.");
}
const cssLinks = Array.isArray(entry.css) ? entry.css.map((href) => ` <link rel="stylesheet" href="/${href}">`).join("\n") : "";
const html = `<!doctype html>
<html lang="en" data-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="/favicon.ico">
<script>${themeBootstrapScript}</script>
${cssLinks}
</head>
<body>
<div id="app"></div>
<script type="module" src="/${entry.file}"></script>
</body>
</html>
`;
await mkdir(dirname(outputPath), { recursive: true });
await writeFile(outputPath, html, "utf8");

View File

@@ -1,6 +1,5 @@
// Path: Frontend/vite.config.ts // Path: Frontend/vite.config.ts
import { nitroV2Plugin as nitro } from "@solidjs/vite-plugin-nitro-2";
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import { solidStart } from "@solidjs/start/config"; import { solidStart } from "@solidjs/start/config";
@@ -11,7 +10,7 @@ const extraAllowedHosts = (process.env.ALLOWED_HOSTS ?? "")
.filter(Boolean); .filter(Boolean);
export default defineConfig({ export default defineConfig({
plugins: [solidStart(), nitro()], plugins: [solidStart({ ssr: false })],
server: { server: {
allowedHosts: ["localhost", ...extraAllowedHosts], allowedHosts: ["localhost", ...extraAllowedHosts],
}, },

27
Proxy/Local/Dockerfile Normal file
View File

@@ -0,0 +1,27 @@
# syntax=docker/dockerfile:1.7
FROM node:22-alpine AS frontend-dependencies
WORKDIR /workspace/Frontend
RUN corepack enable && corepack prepare pnpm@9.0.0 --activate
COPY Frontend/package.json Frontend/pnpm-lock.yaml Frontend/tsconfig.json Frontend/vite.config.ts ./
RUN pnpm install --frozen-lockfile
FROM frontend-dependencies AS frontend-build
ENV NODE_ENV=production
COPY Frontend/ ./
RUN pnpm build:static
FROM nginx:1.29-alpine AS production
COPY Proxy/Local/default.conf /etc/nginx/conf.d/default.conf
COPY --from=frontend-build /workspace/Frontend/dist/client /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

23
Proxy/Local/default.conf Normal file
View File

@@ -0,0 +1,23 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /favicon.ico {
try_files $uri =404;
access_log off;
log_not_found off;
}
location /_build/ {
access_log off;
add_header Cache-Control "public, max-age=31536000, immutable";
try_files $uri =404;
}
}

36
Proxy/docker-bake.hcl Normal file
View File

@@ -0,0 +1,36 @@
variable "REGISTRY" {
default = "registry.example.com"
}
variable "TAG" {
default = "latest"
}
target "_proxy" {
context = "."
dockerfile = "Proxy/Local/Dockerfile"
}
target "prod" {
inherits = ["_proxy"]
target = "production"
tags = ["moku/work-proxy:local-prod"]
}
target "prod-image" {
inherits = ["_proxy"]
target = "production"
tags = ["${REGISTRY}/moku/work-proxy:prod-${TAG}"]
}
group "local" {
targets = ["prod"]
}
group "registry" {
targets = ["prod-image"]
}
group "default" {
targets = ["prod"]
}