Merge branch 'Features/Frontend/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()
|
||||
frontend_dir := project_root + "/Frontend"
|
||||
frontend_bake := project_root + "/Frontend/docker-bake.hcl"
|
||||
proxy_bake := project_root + "/Proxy/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:
|
||||
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:
|
||||
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
|
||||
- [ ] Stack-Decisions
|
||||
- [ ] Proxy
|
||||
- [ ] Local-Prod-NGINX-Proxy
|
||||
- [ ] Static-Frontend-Serving
|
||||
- [ ] 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
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
|
||||
# Frontend development image only.
|
||||
# Production static serving is owned by Proxy/Local/Dockerfile.
|
||||
|
||||
FROM node:22-alpine AS base
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -17,30 +17,18 @@ target "dev" {
|
||||
tags = ["moku/work-frontend:dev"]
|
||||
}
|
||||
|
||||
target "prod" {
|
||||
inherits = ["_app"]
|
||||
target = "production"
|
||||
tags = ["moku/work-frontend:prod"]
|
||||
}
|
||||
|
||||
target "dev-image" {
|
||||
inherits = ["_app"]
|
||||
target = "development"
|
||||
tags = ["${REGISTRY}/moku/work-frontend:dev-${TAG}"]
|
||||
}
|
||||
|
||||
target "prod-image" {
|
||||
inherits = ["_app"]
|
||||
target = "production"
|
||||
tags = ["${REGISTRY}/moku/work-frontend:prod-${TAG}"]
|
||||
}
|
||||
|
||||
group "local" {
|
||||
targets = ["dev", "prod"]
|
||||
targets = ["dev"]
|
||||
}
|
||||
|
||||
group "registry" {
|
||||
targets = ["dev-image", "prod-image"]
|
||||
targets = ["dev-image"]
|
||||
}
|
||||
|
||||
group "default" {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"build:static": "pnpm build && node ./scripts/render-static-index.mjs",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"start": "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
|
||||
|
||||
import { nitroV2Plugin as nitro } from "@solidjs/vite-plugin-nitro-2";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
import { solidStart } from "@solidjs/start/config";
|
||||
@@ -11,7 +10,7 @@ const extraAllowedHosts = (process.env.ALLOWED_HOSTS ?? "")
|
||||
.filter(Boolean);
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [solidStart(), nitro()],
|
||||
plugins: [solidStart({ ssr: false })],
|
||||
server: {
|
||||
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