Feat: Local prod proxy
This commit is contained in:
13
.dockerignore
Normal file
13
.dockerignore
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
.DS_Store
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
**/.pnpm-store
|
||||||
|
**/.output
|
||||||
|
**/dist
|
||||||
|
**/node_modules
|
||||||
|
|
||||||
|
Commands
|
||||||
|
Docker
|
||||||
|
Documentation
|
||||||
|
Env
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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" {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
49
Frontend/scripts/render-static-index.mjs
Normal file
49
Frontend/scripts/render-static-index.mjs
Normal 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");
|
||||||
@@ -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
27
Proxy/Local/Dockerfile
Normal 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
23
Proxy/Local/default.conf
Normal 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
36
Proxy/docker-bake.hcl
Normal 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"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user