Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 51 additions & 20 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# Stage 1: Frontend Builder
# This stage prepares the static frontend assets.
FROM alpine:3.19 AS frontend
WORKDIR /opt/frontend

Expand All @@ -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

Expand All @@ -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" ]

Expand Down
87 changes: 87 additions & 0 deletions esbuild.config.mjs
Original file line number Diff line number Diff line change
@@ -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' },
];

// copy out the 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
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/*',
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: external pattern '@remnawave/*' may not work correctly with current local library structure - consider being more specific

'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
})
));
}
Loading