build: reduce docker image layer churn
This commit is contained in:
@@ -17,6 +17,7 @@ docs-site/node_modules/
|
|||||||
docs-site/.astro/
|
docs-site/.astro/
|
||||||
docs-site/dist/
|
docs-site/dist/
|
||||||
docs-site/src/content/docs/
|
docs-site/src/content/docs/
|
||||||
|
frontend-nginx-dist/
|
||||||
deploy/compose/docker-compose-dev.yml
|
deploy/compose/docker-compose-dev.yml
|
||||||
data/*
|
data/*
|
||||||
!data/tariffs.example.json
|
!data/tariffs.example.json
|
||||||
@@ -51,6 +52,11 @@ bot/app/web/templates/subscription_webapp_admin.min.*.js.br
|
|||||||
bot/app/web/templates/subscription_webapp_admin.min.*.js.gz
|
bot/app/web/templates/subscription_webapp_admin.min.*.js.gz
|
||||||
bot/app/web/templates/subscription_webapp_admin.*.css.br
|
bot/app/web/templates/subscription_webapp_admin.*.css.br
|
||||||
bot/app/web/templates/subscription_webapp_admin.*.css.gz
|
bot/app/web/templates/subscription_webapp_admin.*.css.gz
|
||||||
|
bot/app/web/templates/subscription_webapp_docs_demo.css
|
||||||
|
bot/app/web/templates/subscription_webapp_docs_demo.js
|
||||||
|
bot/app/web/templates/subscription_webapp_docs_demo.*.css
|
||||||
|
bot/app/web/templates/subscription_webapp_docs_demo.*.css.br
|
||||||
|
bot/app/web/templates/subscription_webapp_docs_demo.*.css.gz
|
||||||
backend/bot/app/web/templates/subscription_webapp.css
|
backend/bot/app/web/templates/subscription_webapp.css
|
||||||
backend/bot/app/web/templates/subscription_webapp.js
|
backend/bot/app/web/templates/subscription_webapp.js
|
||||||
backend/bot/app/web/templates/subscription_webapp.min.*.js
|
backend/bot/app/web/templates/subscription_webapp.min.*.js
|
||||||
@@ -67,6 +73,11 @@ backend/bot/app/web/templates/subscription_webapp_admin.min.*.js.br
|
|||||||
backend/bot/app/web/templates/subscription_webapp_admin.min.*.js.gz
|
backend/bot/app/web/templates/subscription_webapp_admin.min.*.js.gz
|
||||||
backend/bot/app/web/templates/subscription_webapp_admin.*.css.br
|
backend/bot/app/web/templates/subscription_webapp_admin.*.css.br
|
||||||
backend/bot/app/web/templates/subscription_webapp_admin.*.css.gz
|
backend/bot/app/web/templates/subscription_webapp_admin.*.css.gz
|
||||||
|
backend/bot/app/web/templates/subscription_webapp_docs_demo.css
|
||||||
|
backend/bot/app/web/templates/subscription_webapp_docs_demo.js
|
||||||
|
backend/bot/app/web/templates/subscription_webapp_docs_demo.*.css
|
||||||
|
backend/bot/app/web/templates/subscription_webapp_docs_demo.*.css.br
|
||||||
|
backend/bot/app/web/templates/subscription_webapp_docs_demo.*.css.gz
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
**/__pycache__/
|
**/__pycache__/
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ docs-site/.astro/
|
|||||||
docs-site/dist/
|
docs-site/dist/
|
||||||
docs-site/public/demo/runtime/
|
docs-site/public/demo/runtime/
|
||||||
docs-site/src/content/docs/
|
docs-site/src/content/docs/
|
||||||
|
frontend-nginx-dist/
|
||||||
|
|
||||||
# WebApp build artifacts (regenerated by `npm run build:webapp` / Docker build)
|
# WebApp build artifacts (regenerated by `npm run build:webapp` / Docker build)
|
||||||
bot/app/web/templates/subscription_webapp.css
|
bot/app/web/templates/subscription_webapp.css
|
||||||
|
|||||||
@@ -119,6 +119,8 @@ COPY frontend ./
|
|||||||
RUN mkdir -p ../backend/bot/app/web/templates
|
RUN mkdir -p ../backend/bot/app/web/templates
|
||||||
|
|
||||||
RUN npm run build:webapp
|
RUN npm run build:webapp
|
||||||
|
COPY backend/bot/app/web/templates/subscription_webapp.html ../backend/bot/app/web/templates/subscription_webapp.html
|
||||||
|
RUN node ./scripts/prepare_nginx_assets.mjs --out ../frontend-nginx-dist
|
||||||
|
|
||||||
|
|
||||||
FROM nginx:1.27-alpine AS frontend
|
FROM nginx:1.27-alpine AS frontend
|
||||||
@@ -131,57 +133,10 @@ LABEL org.opencontainers.image.source="https://github.com/3252a8/remnawave-minis
|
|||||||
COPY deploy/docker/frontend/nginx.conf /etc/nginx/conf.d/default.conf
|
COPY deploy/docker/frontend/nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
COPY deploy/docker/frontend/00-startup-banner.sh /docker-entrypoint.d/00-startup-banner.sh
|
COPY deploy/docker/frontend/00-startup-banner.sh /docker-entrypoint.d/00-startup-banner.sh
|
||||||
COPY deploy/docker/frontend/robots.txt /usr/share/nginx/html/robots.txt
|
COPY deploy/docker/frontend/robots.txt /usr/share/nginx/html/robots.txt
|
||||||
COPY backend/bot/app/web/templates/subscription_webapp.html /usr/share/nginx/html/index.html
|
|
||||||
COPY --from=frontend-builder /app/backend/bot/app/web/templates/subscription_webapp.css /usr/share/nginx/html/subscription_webapp.css
|
|
||||||
COPY --from=frontend-builder /app/backend/bot/app/web/templates/subscription_webapp.*.css /usr/share/nginx/html/
|
|
||||||
COPY --from=frontend-builder /app/backend/bot/app/web/templates/subscription_webapp.*.css.gz /usr/share/nginx/html/
|
|
||||||
COPY --from=frontend-builder /app/backend/bot/app/web/templates/subscription_webapp_admin.css /usr/share/nginx/html/subscription_webapp_admin.css
|
|
||||||
COPY --from=frontend-builder /app/backend/bot/app/web/templates/subscription_webapp_admin.*.css /usr/share/nginx/html/
|
|
||||||
COPY --from=frontend-builder /app/backend/bot/app/web/templates/subscription_webapp_admin.*.css.gz /usr/share/nginx/html/
|
|
||||||
COPY --from=frontend-builder /app/backend/bot/app/web/templates/subscription_webapp.js /usr/share/nginx/html/subscription_webapp.js
|
|
||||||
COPY --from=frontend-builder /app/backend/bot/app/web/templates/subscription_webapp.min.*.js /usr/share/nginx/html/
|
|
||||||
COPY --from=frontend-builder /app/backend/bot/app/web/templates/subscription_webapp.min.*.js.gz /usr/share/nginx/html/
|
|
||||||
COPY --from=frontend-builder /app/backend/bot/app/web/templates/subscription_webapp_admin.js /usr/share/nginx/html/subscription_webapp_admin.js
|
|
||||||
COPY --from=frontend-builder /app/backend/bot/app/web/templates/subscription_webapp_admin.min.*.js /usr/share/nginx/html/
|
|
||||||
COPY --from=frontend-builder /app/backend/bot/app/web/templates/subscription_webapp_admin.min.*.js.gz /usr/share/nginx/html/
|
|
||||||
|
|
||||||
RUN set -eu; \
|
RUN set -eu; \
|
||||||
find /docker-entrypoint.d -type f -name '*.sh' -exec sed -i 's/\r$//' {} +; \
|
find /docker-entrypoint.d -type f -name '*.sh' -exec sed -i 's/\r$//' {} +; \
|
||||||
HASHED=$(ls /usr/share/nginx/html/subscription_webapp.min.*.js 2>/dev/null | sort | tail -n1 | xargs -n1 basename || true); \
|
|
||||||
JS_NAME="${HASHED:-subscription_webapp.js}"; \
|
|
||||||
CSS_NAME="subscription_webapp.css"; \
|
|
||||||
ADMIN_HASHED=$(ls /usr/share/nginx/html/subscription_webapp_admin.min.*.js 2>/dev/null | sort | tail -n1 | xargs -n1 basename || true); \
|
|
||||||
ADMIN_CSS_NAME="subscription_webapp_admin.css"; \
|
|
||||||
for candidate in /usr/share/nginx/html/subscription_webapp.*.css; do \
|
|
||||||
name="$(basename "$candidate")"; \
|
|
||||||
case "$name" in \
|
|
||||||
subscription_webapp.[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f].css) CSS_NAME="$name" ;; \
|
|
||||||
esac; \
|
|
||||||
done; \
|
|
||||||
for candidate in /usr/share/nginx/html/subscription_webapp_admin.*.css; do \
|
|
||||||
name="$(basename "$candidate")"; \
|
|
||||||
case "$name" in \
|
|
||||||
subscription_webapp_admin.[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f].css) ADMIN_CSS_NAME="$name" ;; \
|
|
||||||
esac; \
|
|
||||||
done; \
|
|
||||||
if [ -n "$ADMIN_HASHED" ]; then \
|
|
||||||
cp "/usr/share/nginx/html/${ADMIN_HASHED}" /usr/share/nginx/html/subscription_webapp_admin.js; \
|
|
||||||
if [ -f "/usr/share/nginx/html/${ADMIN_HASHED}.gz" ]; then cp "/usr/share/nginx/html/${ADMIN_HASHED}.gz" /usr/share/nginx/html/subscription_webapp_admin.js.gz; fi; \
|
|
||||||
fi; \
|
|
||||||
if [ "$ADMIN_CSS_NAME" != "subscription_webapp_admin.css" ]; then \
|
|
||||||
cp "/usr/share/nginx/html/${ADMIN_CSS_NAME}" /usr/share/nginx/html/subscription_webapp_admin.css; \
|
|
||||||
if [ -f "/usr/share/nginx/html/${ADMIN_CSS_NAME}.gz" ]; then cp "/usr/share/nginx/html/${ADMIN_CSS_NAME}.gz" /usr/share/nginx/html/subscription_webapp_admin.css.gz; fi; \
|
|
||||||
fi; \
|
|
||||||
sed -i \
|
|
||||||
-e '/WEBAPP_I18N_SCRIPT/d' \
|
|
||||||
-e '/WEBAPP_CONFIG_SCRIPT/d' \
|
|
||||||
-e "s|href=\"/subscription_webapp.css\"|href=\"/${CSS_NAME}\"|" \
|
|
||||||
-e "/WEBAPP_JS_SCRIPT/c\\ <script src=\"/${JS_NAME}\" type=\"module\"></script>" \
|
|
||||||
-e '/WEBAPP_DEV_MOCK_START/d' \
|
|
||||||
-e '/WEBAPP_DEV_MOCK_END/d' \
|
|
||||||
-e '/subscription_webapp.js" defer/d' \
|
|
||||||
/usr/share/nginx/html/index.html; \
|
|
||||||
chmod +x /docker-entrypoint.d/00-startup-banner.sh
|
chmod +x /docker-entrypoint.d/00-startup-banner.sh
|
||||||
|
COPY --from=frontend-builder /app/frontend-nginx-dist/ /usr/share/nginx/html/
|
||||||
COPY --from=version-builder /build-tag /build-tag
|
COPY --from=version-builder /build-tag /build-tag
|
||||||
COPY --from=version-builder /build-commit /build-commit
|
COPY --from=version-builder /build-commit /build-commit
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import {
|
||||||
|
copyFile,
|
||||||
|
lstat,
|
||||||
|
mkdir,
|
||||||
|
readdir,
|
||||||
|
readFile,
|
||||||
|
rm,
|
||||||
|
symlink,
|
||||||
|
writeFile,
|
||||||
|
} from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const repoRoot = path.resolve(__dirname, "..", "..");
|
||||||
|
const templatesDir = path.join(repoRoot, "backend", "bot", "app", "web", "templates");
|
||||||
|
const DEFAULT_OUT_DIR = path.join(repoRoot, "frontend-nginx-dist");
|
||||||
|
const outDir = resolveOutDir();
|
||||||
|
|
||||||
|
function resolveOutDir() {
|
||||||
|
const argIndex = process.argv.indexOf("--out");
|
||||||
|
if (argIndex === -1) {
|
||||||
|
return DEFAULT_OUT_DIR;
|
||||||
|
}
|
||||||
|
const rawValue = process.argv[argIndex + 1];
|
||||||
|
if (!rawValue) {
|
||||||
|
throw new Error("--out requires a directory path");
|
||||||
|
}
|
||||||
|
return path.resolve(process.cwd(), rawValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pathExists(filePath) {
|
||||||
|
try {
|
||||||
|
await lstat(filePath);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
if (error?.code === "ENOENT") return false;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyIfExists(sourceName, targetName = sourceName) {
|
||||||
|
const sourcePath = path.join(templatesDir, sourceName);
|
||||||
|
if (!(await pathExists(sourcePath))) return false;
|
||||||
|
await copyFile(sourcePath, path.join(outDir, targetName));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function linkOrCopy(targetName, aliasName) {
|
||||||
|
const aliasPath = path.join(outDir, aliasName);
|
||||||
|
try {
|
||||||
|
await symlink(targetName, aliasPath);
|
||||||
|
} catch (error) {
|
||||||
|
if (!["EPERM", "EINVAL", "ENOSYS"].includes(error?.code)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
await copyFile(path.join(outDir, targetName), aliasPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function latestMatching(entries, pattern, fallbackName) {
|
||||||
|
const matches = entries.filter((name) => pattern.test(name)).sort();
|
||||||
|
return matches.at(-1) || fallbackName;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyRuntimeAsset({ hashedName, stableName }) {
|
||||||
|
const copied = await copyIfExists(hashedName);
|
||||||
|
if (copied && hashedName !== stableName) {
|
||||||
|
await linkOrCopy(hashedName, stableName);
|
||||||
|
} else if (!copied) {
|
||||||
|
await copyIfExists(stableName);
|
||||||
|
}
|
||||||
|
|
||||||
|
const gzipName = `${hashedName}.gz`;
|
||||||
|
const gzipCopied = await copyIfExists(gzipName);
|
||||||
|
if (gzipCopied && hashedName !== stableName) {
|
||||||
|
await linkOrCopy(gzipName, `${stableName}.gz`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareIndexHtml(rawHtml, { cssName, jsName }) {
|
||||||
|
const html = rawHtml
|
||||||
|
.replace(/\r\n/g, "\n")
|
||||||
|
.replace('href="/subscription_webapp.css"', `href="/${cssName}"`);
|
||||||
|
const lines = html.split("\n");
|
||||||
|
const output = lines
|
||||||
|
.map((line) =>
|
||||||
|
line.includes("WEBAPP_JS_SCRIPT")
|
||||||
|
? ` <script src="/${jsName}" type="module"></script>`
|
||||||
|
: line
|
||||||
|
)
|
||||||
|
.filter(
|
||||||
|
(line) =>
|
||||||
|
!line.includes("WEBAPP_I18N_SCRIPT") &&
|
||||||
|
!line.includes("WEBAPP_CONFIG_SCRIPT") &&
|
||||||
|
!line.includes("WEBAPP_DEV_MOCK_START") &&
|
||||||
|
!line.includes("WEBAPP_DEV_MOCK_END") &&
|
||||||
|
!line.includes('subscription_webapp.js" defer')
|
||||||
|
)
|
||||||
|
.join("\n");
|
||||||
|
return output.endsWith("\n") ? output : `${output}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const entries = await readdir(templatesDir);
|
||||||
|
const mainJsName = latestMatching(
|
||||||
|
entries,
|
||||||
|
/^subscription_webapp\.min\.[0-9a-f]{8}\.js$/,
|
||||||
|
"subscription_webapp.js"
|
||||||
|
);
|
||||||
|
const mainCssName = latestMatching(
|
||||||
|
entries,
|
||||||
|
/^subscription_webapp\.[0-9a-f]{8}\.css$/,
|
||||||
|
"subscription_webapp.css"
|
||||||
|
);
|
||||||
|
const adminJsName = latestMatching(
|
||||||
|
entries,
|
||||||
|
/^subscription_webapp_admin\.min\.[0-9a-f]{8}\.js$/,
|
||||||
|
"subscription_webapp_admin.js"
|
||||||
|
);
|
||||||
|
const adminCssName = latestMatching(
|
||||||
|
entries,
|
||||||
|
/^subscription_webapp_admin\.[0-9a-f]{8}\.css$/,
|
||||||
|
"subscription_webapp_admin.css"
|
||||||
|
);
|
||||||
|
|
||||||
|
await rm(outDir, { recursive: true, force: true });
|
||||||
|
await mkdir(outDir, { recursive: true });
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
copyRuntimeAsset({ hashedName: mainJsName, stableName: "subscription_webapp.js" }),
|
||||||
|
copyRuntimeAsset({ hashedName: mainCssName, stableName: "subscription_webapp.css" }),
|
||||||
|
copyRuntimeAsset({ hashedName: adminJsName, stableName: "subscription_webapp_admin.js" }),
|
||||||
|
copyRuntimeAsset({ hashedName: adminCssName, stableName: "subscription_webapp_admin.css" }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const indexTemplate = await readFile(path.join(templatesDir, "subscription_webapp.html"), "utf8");
|
||||||
|
await writeFile(
|
||||||
|
path.join(outDir, "index.html"),
|
||||||
|
prepareIndexHtml(indexTemplate, { cssName: mainCssName, jsName: mainJsName }),
|
||||||
|
"utf8"
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Prepared nginx assets in ${path.relative(repoRoot, outDir)}: ${mainJsName}, ${mainCssName}, ${adminJsName}, ${adminCssName}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await main();
|
||||||
Reference in New Issue
Block a user