diff --git a/Dockerfile b/Dockerfile index 5346e8bb..a529e66d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,5 @@ +# Stage 1: Frontend Builder +# This stage prepares the static frontend assets. FROM alpine:3.19 AS frontend WORKDIR /opt/frontend @@ -21,30 +23,37 @@ RUN if [ "$BRANCH" = "dev" ]; then \ && curl -L https://remnawave.github.io/xray-monaco-editor/xray.schema.cn.json -o frontend_crowdin_temp/dist/xray.schema.cn.json \ && curl -L https://remnawave.github.io/xray-monaco-editor/main.wasm -o frontend_crowdin_temp/dist/main.wasm; \ else \ - mkdir -p frontend_crowdin_temp/dist; \ + mkdir -p frontend_crowdin_temp/dist; \ fi +# Stage 2: Backend Builder +# This stage installs all dependencies, generates prisma client, and builds the app bundle. FROM node:22 AS backend-build WORKDIR /opt/app -ENV PRISMA_CLI_BINARY_TARGETS=linux-musl-openssl-3.0.x,linux-musl-arm64-openssl-3.0.x - COPY package*.json ./ -COPY prisma ./prisma - +COPY prisma ./prisma/ -RUN npm ci +RUN npm ci && npx prisma generate +# Copy the rest of the source code COPY . . +# Run migrations and build the application RUN npm run migrate:generate - -RUN npm run build - -RUN npm cache clean --force - -RUN npm prune --omit=dev - +RUN npm run build && node esbuild.config.mjs --build + +# This creates a clean, minimal node_modules with only production dependencies +RUN \ + TARGET_BINARY="linux-musl-openssl-3.0.x"; \ + if [ "$TARGETARCH" = "arm64" ]; then \ + TARGET_BINARY="linux-musl-arm64-openssl-3.0.x"; \ + fi \ + && node esbuild.config.mjs --create-deps \ + && PRISMA_CLI_BINARY_TARGETS=$TARGET_BINARY npm install --prefix /opt/app/prod_modules + +# Stage 3: Production Image +# This is the final, lean image that will be deployed. FROM node:22-alpine WORKDIR /opt/app @@ -58,22 +67,44 @@ ENV REMNAWAVE_BRANCH=${BRANCH} ENV PRISMA_HIDE_UPDATE_MESSAGE=true ENV PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING=1 -COPY --from=backend-build /opt/app/dist ./dist +# Copy the cleanly installed production modules from the build stage +COPY --from=backend-build /opt/app/prod_modules/node_modules /opt/app/node_modules + +# --- Copy Artifacts from Builder Stages --- +# Copy frontend assets COPY --from=frontend /opt/frontend/frontend_temp/dist ./frontend COPY --from=frontend /opt/frontend/frontend_crowdin_temp/dist ./frontend-crowdin -COPY --from=backend-build /opt/app/prisma ./prisma -COPY --from=backend-build /opt/app/node_modules ./node_modules +# Copy prisma schema (required by the client at runtime) +COPY --from=backend-build /opt/app/prisma/schema.prisma ./prisma/schema.prisma +# Copy the pre-generated prisma client and its engine binary from build node_modules. +COPY --from=backend-build /opt/app/node_modules/.prisma /opt/app/node_modules/.prisma +COPY --from=backend-build /opt/app/node_modules/@remnawave /opt/app/node_modules/@remnawave + +# Copy other necessary files COPY configs /var/lib/remnawave/configs COPY package*.json ./ COPY libs ./libs COPY ecosystem.config.js ./ COPY docker-entrypoint.sh ./ - -RUN npm install pm2 -g \ - && npm link - +COPY esbuild.config.mjs ./ + +# Copy bundled application code +RUN --mount=type=bind,from=backend-build,source=/opt/app/bundle,target=/opt/app/bundle \ + node esbuild.config.mjs --copy-bundled-executables + +# Clean up prisma libraries +RUN \ + if [ "$TARGETARCH" = "arm64" ]; then \ + find /opt/app/node_modules/.prisma -name "libquery_engine-*.so.node" ! -name "*arm64*" -delete; \ + else \ + find /opt/app/node_modules/.prisma -name "libquery_engine-*.so.node" \( -name "*arm64*" -o -name "*debian*" \) -delete; \ + fi && \ + find /opt/app/node_modules/.prisma/client -name "libquery_engine-*.so.node" -exec ln -fs {} /opt/app/node_modules/@prisma/engines/ \; + +# Install pm2 globally +RUN npm install pm2 -g --no-fund ENTRYPOINT [ "/bin/sh", "docker-entrypoint.sh" ] diff --git a/esbuild.config.mjs b/esbuild.config.mjs new file mode 100644 index 00000000..3b035bdb --- /dev/null +++ b/esbuild.config.mjs @@ -0,0 +1,87 @@ +import fs from 'fs'; +import path from 'path'; + +// list of node_modules that are shipped inside the container +const prodDeps = ['@bull-board/ui', '@prisma/client', 'class-transformer', 'class-validator', 'prisma', 'zod']; + +const targets = [ + { entry: 'dist/src/main.js', out: 'bundle/main.js' }, + { entry: 'dist/src/bin/cli/cli.js', out: 'bundle/cli.js' }, + { entry: 'dist/src/bin/processors/processors.js', out: 'bundle/processors.js' }, + { entry: 'dist/src/bin/scheduler/scheduler.js', out: 'bundle/scheduler.js' }, + { entry: 'prisma/seed/config.seed.js', out: 'bundle/config.seed.js', dest: 'dist/prisma/seed/config.seed.js' }, +]; + +// create minimal package.json with the same version as currently set only for the packages that had trouble being bundled +if (process.argv.includes('--create-deps')) { + const prodModulesDir = '/opt/app/prod_modules'; + fs.mkdirSync(prodModulesDir, { recursive: true }); + + const mainPkg = JSON.parse(fs.readFileSync('./package.json', 'utf8')); + const allDeps = { ...mainPkg.dependencies, ...mainPkg.devDependencies }; + const pkg = { + type: 'module', + dependencies: Object.fromEntries(prodDeps.filter(k => allDeps[k]).map(k => [k, allDeps[k]])) + }; + + fs.writeFileSync(path.join(prodModulesDir, 'package.json'), JSON.stringify(pkg, null, 2)); + process.exit(0); +} + +// copy out the bundled files to their previous locations +if (process.argv.includes('--copy-bundled-executables')) { + const bundleDir = '/opt/app/bundle'; + targets.forEach(({ entry, out, dest }) => { + const srcFile = path.join(bundleDir, path.basename(out)); + const destFile = dest || entry; + const destDir = path.dirname(destFile); + + fs.mkdirSync(destDir, { recursive: true }); + fs.copyFileSync(srcFile, destFile); + }); + process.exit(0); +} + +const commonOptions = { + bundle: true, + platform: 'node', + target: 'node22', + format: 'cjs', + minify: true, + keepNames: true, + minifySyntax: false, + treeShaking: true, + define: { + 'process.env.NODE_ENV': '"production"' + }, + metafile: true, + logLevel: 'info', + external: [ + ...prodDeps, + '@fastify/static', + '@mikro-orm/core', + '@nestjs/microservices', + '@nestjs/microservices/microservices-module', + '@nestjs/mongoose', + '@nestjs/sequelize/dist/common/sequelize.utils', + '@nestjs/typeorm/dist/common/typeorm.utils', + '@nestjs/websockets/socket-module', + '@remnawave/*', + 'blessed', + 'pm2-deploy', + 'pty.js', + 'term.js', + ] +}; + +if (process.argv.includes('--build')) { + const { build } = await import('esbuild'); + + await Promise.all(targets.map(({ entry, out }) => + build({ + ...commonOptions, + entryPoints: [entry], + outfile: out + }) + )); +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index c4f37792..cfa9ee5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,19 @@ { "name": "@remnawave/backend", - "version": "2.1.3", + "version": "2.1.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@remnawave/backend", - "version": "2.1.3", + "version": "2.1.4", "license": "AGPL-3.0-only", "dependencies": { "@arthurfiorette/prisma-kysely": "^2.1.0", "@bull-board/api": "^6.12.0", "@bull-board/express": "^6.12.0", "@bull-board/nestjs": "^6.12.0", + "@bull-board/ui": "^6.12.0", "@exact-team/telegram-oauth2": "0.0.9", "@grammyjs/parse-mode": "1.11.1", "@kastov/grammy-nestjs": "0.4.2", @@ -116,6 +117,7 @@ "@types/semver": "^7.7.0", "@typescript-eslint/eslint-plugin": "^8.39.1", "@typescript-eslint/parser": "^8.39.1", + "esbuild": "^0.25.9", "eslint": "9.33.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-paths": "^1.1.0", @@ -451,6 +453,23 @@ "tslib": "^2.4.0" } }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", + "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", @@ -1409,58 +1428,6 @@ "node": ">=16" } }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", - "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", - "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", - "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", - "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", @@ -1474,19 +1441,6 @@ "linux" ] }, - "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", - "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.3.tgz", @@ -5690,6 +5644,48 @@ "node": ">= 0.4" } }, + "node_modules/esbuild": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", + "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.9", + "@esbuild/android-arm": "0.25.9", + "@esbuild/android-arm64": "0.25.9", + "@esbuild/android-x64": "0.25.9", + "@esbuild/darwin-arm64": "0.25.9", + "@esbuild/darwin-x64": "0.25.9", + "@esbuild/freebsd-arm64": "0.25.9", + "@esbuild/freebsd-x64": "0.25.9", + "@esbuild/linux-arm": "0.25.9", + "@esbuild/linux-arm64": "0.25.9", + "@esbuild/linux-ia32": "0.25.9", + "@esbuild/linux-loong64": "0.25.9", + "@esbuild/linux-mips64el": "0.25.9", + "@esbuild/linux-ppc64": "0.25.9", + "@esbuild/linux-riscv64": "0.25.9", + "@esbuild/linux-s390x": "0.25.9", + "@esbuild/linux-x64": "0.25.9", + "@esbuild/netbsd-arm64": "0.25.9", + "@esbuild/netbsd-x64": "0.25.9", + "@esbuild/openbsd-arm64": "0.25.9", + "@esbuild/openbsd-x64": "0.25.9", + "@esbuild/openharmony-arm64": "0.25.9", + "@esbuild/sunos-x64": "0.25.9", + "@esbuild/win32-arm64": "0.25.9", + "@esbuild/win32-ia32": "0.25.9", + "@esbuild/win32-x64": "0.25.9" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -6721,20 +6717,6 @@ "dev": true, "license": "Unlicense" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", diff --git a/package.json b/package.json index 35488756..ead51185 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@bull-board/api": "^6.12.0", "@bull-board/express": "^6.12.0", "@bull-board/nestjs": "^6.12.0", + "@bull-board/ui": "^6.12.0", "@exact-team/telegram-oauth2": "0.0.9", "@grammyjs/parse-mode": "1.11.1", "@kastov/grammy-nestjs": "0.4.2", @@ -150,6 +151,7 @@ "@types/semver": "^7.7.0", "@typescript-eslint/eslint-plugin": "^8.39.1", "@typescript-eslint/parser": "^8.39.1", + "esbuild": "^0.25.9", "eslint": "9.33.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-paths": "^1.1.0", diff --git a/src/modules/subscription-template/render-templates.service.ts b/src/modules/subscription-template/render-templates.service.ts index c73eef40..65c31846 100644 --- a/src/modules/subscription-template/render-templates.service.ts +++ b/src/modules/subscription-template/render-templates.service.ts @@ -157,14 +157,6 @@ export class RenderTemplatesService { contentType: SUBSCRIPTION_CONFIG_TYPES.SING_BOX.CONTENT_TYPE, }; - case 'SINGBOX': - return { - sub: await this.singBoxGeneratorService.generateConfig( - formattedHosts, - '1.11.1', - ), - contentType: SUBSCRIPTION_CONFIG_TYPES.SING_BOX.CONTENT_TYPE, - }; case 'SINGBOX_LEGACY': return { sub: await this.singBoxGeneratorService.generateConfig(