From 12dfead151653edfa23e91a3f4193b2d3f36e955 Mon Sep 17 00:00:00 2001 From: Zhijie He Date: Sun, 29 Sep 2024 11:31:55 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=91=B7=20build:=20optimize=20image=20size?= =?UTF-8?q?=20under=20glibc=20env=20(#4176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 86 ++++++------ Dockerfile.database | 90 ++++++------- scripts/serverLauncher/startServer.js | 181 ++++++++++++++++++++++++++ 3 files changed, 255 insertions(+), 102 deletions(-) create mode 100644 scripts/serverLauncher/startServer.js diff --git a/Dockerfile b/Dockerfile index 5b102c2ad16f..2a9b15210886 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -## Base image for all the stages +## Base image for all building stages FROM node:20-slim AS base ARG USE_CN_MIRROR @@ -10,19 +10,22 @@ RUN \ if [ "${USE_CN_MIRROR:-false}" = "true" ]; then \ sed -i "s/deb.debian.org/mirrors.ustc.edu.cn/g" "/etc/apt/sources.list.d/debian.sources"; \ fi \ - # Add required package & update base package + # Add required package && apt update \ - && apt install busybox proxychains-ng -qy \ - && apt full-upgrade -qy \ - && apt autoremove -qy --purge \ - && apt clean -qy \ - # Configure BusyBox - && busybox --install -s \ - # Add nextjs:nodejs to run the app - && addgroup --system --gid 1001 nodejs \ - && adduser --system --home "/app" --gid 1001 -uid 1001 nextjs \ - # Set permission for nextjs:nodejs - && chown -R nextjs:nodejs "/etc/proxychains4.conf" \ + && apt install ca-certificates proxychains-ng -qy \ + # Prepare required package to distroless + && mkdir -p /distroless/bin /distroless/etc /distroless/etc/ssl/certs /distroless/lib \ + # Copy proxychains to distroless + && cp /usr/lib/$(arch)-linux-gnu/libproxychains.so.4 /distroless/lib/libproxychains.so.4 \ + && cp /usr/lib/$(arch)-linux-gnu/libdl.so.2 /distroless/lib/libdl.so.2 \ + && cp /usr/bin/proxychains4 /distroless/bin/proxychains \ + && cp /etc/proxychains4.conf /distroless/etc/proxychains4.conf \ + # Copy node to distroless + && cp /usr/lib/$(arch)-linux-gnu/libstdc++.so.6 /distroless/lib/libstdc++.so.6 \ + && cp /usr/lib/$(arch)-linux-gnu/libgcc_s.so.1 /distroless/lib/libgcc_s.so.1 \ + && cp /usr/local/bin/node /distroless/bin/node \ + # Copy CA certificates to distroless + && cp /etc/ssl/certs/ca-certificates.crt /distroless/etc/ssl/certs/ca-certificates.crt \ # Cleanup temp files && rm -rf /tmp/* /var/lib/apt/lists/* /var/tmp/* @@ -61,6 +64,7 @@ RUN \ if [ "${USE_CN_MIRROR:-false}" = "true" ]; then \ export SENTRYCLI_CDNURL="https://npmmirror.com/mirrors/sentry-cli"; \ npm config set registry "https://registry.npmmirror.com/"; \ + echo 'canvas_binary_host_mirror=https://npmmirror.com/mirrors/canvas' >> .npmrc; \ fi \ # Set the registry for corepack && export COREPACK_NPM_REGISTRY=$(npm config get registry | sed 's/\/$//') \ @@ -80,7 +84,9 @@ COPY . . RUN npm run build:docker ## Application image, copy all the files for production -FROM scratch AS app +FROM busybox:latest AS app + +COPY --from=base /distroless/ / COPY --from=builder /app/public /app/public @@ -90,13 +96,25 @@ COPY --from=builder /app/.next/standalone /app/ COPY --from=builder /app/.next/static /app/.next/static COPY --from=builder /deps/node_modules/.pnpm /app/node_modules/.pnpm +# Copy server launcher +COPY --from=builder /app/scripts/serverLauncher/startServer.js /app/startServer.js + +RUN \ + # Add nextjs:nodejs to run the app + addgroup -S -g 1001 nodejs \ + && adduser -D -G nodejs -H -S -h /app -u 1001 nextjs \ + # Set permission for nextjs:nodejs + && chown -R nextjs:nodejs /app /etc/proxychains4.conf + ## Production image, copy all the files and run next -FROM base +FROM scratch # Copy all the files from app, set the correct permission for prerender cache -COPY --from=app --chown=nextjs:nodejs /app /app +COPY --from=app / / ENV NODE_ENV="production" \ + NODE_OPTIONS="--use-openssl-ca" \ + NODE_EXTRA_CA_CERTS="/etc/ssl/certs/ca-certificates.crt" \ NODE_TLS_REJECT_UNAUTHORIZED="" # set hostname to localhost @@ -176,36 +194,6 @@ USER nextjs EXPOSE 3210/tcp -CMD \ - if [ -n "$PROXY_URL" ]; then \ - # Set regex for IPv4 - IP_REGEX="^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$"; \ - # Set proxychains command - PROXYCHAINS="proxychains -q"; \ - # Parse the proxy URL - host_with_port="${PROXY_URL#*//}"; \ - host="${host_with_port%%:*}"; \ - port="${PROXY_URL##*:}"; \ - protocol="${PROXY_URL%%://*}"; \ - # Resolve to IP address if the host is a domain - if ! [[ "$host" =~ "$IP_REGEX" ]]; then \ - nslookup=$(nslookup -q="A" "$host" | tail -n +3 | grep 'Address:'); \ - if [ -n "$nslookup" ]; then \ - host=$(echo "$nslookup" | tail -n 1 | awk '{print $2}'); \ - fi; \ - fi; \ - # Generate proxychains configuration file - printf "%s\n" \ - 'localnet 127.0.0.0/255.0.0.0' \ - 'localnet ::1/128' \ - 'proxy_dns' \ - 'remote_dns_subnet 224' \ - 'strict_chain' \ - 'tcp_connect_time_out 8000' \ - 'tcp_read_time_out 15000' \ - '[ProxyList]' \ - "$protocol $host $port" \ - > "/etc/proxychains4.conf"; \ - fi; \ - # Run the server - ${PROXYCHAINS} node "/app/server.js"; +ENTRYPOINT ["/bin/node"] + +CMD ["/app/startServer.js"] diff --git a/Dockerfile.database b/Dockerfile.database index c8e033c12664..1aa9e411a2eb 100644 --- a/Dockerfile.database +++ b/Dockerfile.database @@ -1,4 +1,4 @@ -## Base image for all the stages +## Base image for all building stages FROM node:20-slim AS base ARG USE_CN_MIRROR @@ -10,19 +10,22 @@ RUN \ if [ "${USE_CN_MIRROR:-false}" = "true" ]; then \ sed -i "s/deb.debian.org/mirrors.ustc.edu.cn/g" "/etc/apt/sources.list.d/debian.sources"; \ fi \ - # Add required package & update base package + # Add required package && apt update \ - && apt install busybox proxychains-ng -qy \ - && apt full-upgrade -qy \ - && apt autoremove -qy --purge \ - && apt clean -qy \ - # Configure BusyBox - && busybox --install -s \ - # Add nextjs:nodejs to run the app - && addgroup --system --gid 1001 nodejs \ - && adduser --system --home "/app" --gid 1001 -uid 1001 nextjs \ - # Set permission for nextjs:nodejs - && chown -R nextjs:nodejs "/etc/proxychains4.conf" \ + && apt install ca-certificates proxychains-ng -qy \ + # Prepare required package to distroless + && mkdir -p /distroless/bin /distroless/etc /distroless/etc/ssl/certs /distroless/lib \ + # Copy proxychains to distroless + && cp /usr/lib/$(arch)-linux-gnu/libproxychains.so.4 /distroless/lib/libproxychains.so.4 \ + && cp /usr/lib/$(arch)-linux-gnu/libdl.so.2 /distroless/lib/libdl.so.2 \ + && cp /usr/bin/proxychains4 /distroless/bin/proxychains \ + && cp /etc/proxychains4.conf /distroless/etc/proxychains4.conf \ + # Copy node to distroless + && cp /usr/lib/$(arch)-linux-gnu/libstdc++.so.6 /distroless/lib/libstdc++.so.6 \ + && cp /usr/lib/$(arch)-linux-gnu/libgcc_s.so.1 /distroless/lib/libgcc_s.so.1 \ + && cp /usr/local/bin/node /distroless/bin/node \ + # Copy CA certificates to distroless + && cp /etc/ssl/certs/ca-certificates.crt /distroless/etc/ssl/certs/ca-certificates.crt \ # Cleanup temp files && rm -rf /tmp/* /var/lib/apt/lists/* /var/tmp/* @@ -65,6 +68,7 @@ RUN \ if [ "${USE_CN_MIRROR:-false}" = "true" ]; then \ export SENTRYCLI_CDNURL="https://npmmirror.com/mirrors/sentry-cli"; \ npm config set registry "https://registry.npmmirror.com/"; \ + echo 'canvas_binary_host_mirror=https://npmmirror.com/mirrors/canvas' >> .npmrc; \ fi \ # Set the registry for corepack && export COREPACK_NPM_REGISTRY=$(npm config get registry | sed 's/\/$//') \ @@ -84,7 +88,9 @@ COPY . . RUN npm run build:docker ## Application image, copy all the files for production -FROM scratch AS app +FROM busybox:latest AS app + +COPY --from=base /distroless/ / COPY --from=builder /app/public /app/public @@ -103,13 +109,25 @@ COPY --from=builder /app/src/database/server/migrations /app/migrations COPY --from=builder /app/scripts/migrateServerDB/docker.cjs /app/docker.cjs COPY --from=builder /app/scripts/migrateServerDB/errorHint.js /app/errorHint.js +# Copy server launcher +COPY --from=builder /app/scripts/serverLauncher/startServer.js /app/startServer.js + +RUN \ + # Add nextjs:nodejs to run the app + addgroup -S -g 1001 nodejs \ + && adduser -D -G nodejs -H -S -h /app -u 1001 nextjs \ + # Set permission for nextjs:nodejs + && chown -R nextjs:nodejs /app /etc/proxychains4.conf + ## Production image, copy all the files and run next -FROM base +FROM scratch # Copy all the files from app, set the correct permission for prerender cache -COPY --from=app --chown=nextjs:nodejs /app /app +COPY --from=app / / ENV NODE_ENV="production" \ + NODE_OPTIONS="--use-openssl-ca" \ + NODE_EXTRA_CA_CERTS="/etc/ssl/certs/ca-certificates.crt" \ NODE_TLS_REJECT_UNAUTHORIZED="" # set hostname to localhost @@ -208,40 +226,6 @@ USER nextjs EXPOSE 3210/tcp -CMD \ - if [ -n "$PROXY_URL" ]; then \ - # Set regex for IPv4 - IP_REGEX="^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$"; \ - # Set proxychains command - PROXYCHAINS="proxychains -q"; \ - # Parse the proxy URL - host_with_port="${PROXY_URL#*//}"; \ - host="${host_with_port%%:*}"; \ - port="${PROXY_URL##*:}"; \ - protocol="${PROXY_URL%%://*}"; \ - # Resolve to IP address if the host is a domain - if ! [[ "$host" =~ "$IP_REGEX" ]]; then \ - nslookup=$(nslookup -q="A" "$host" | tail -n +3 | grep 'Address:'); \ - if [ -n "$nslookup" ]; then \ - host=$(echo "$nslookup" | tail -n 1 | awk '{print $2}'); \ - fi; \ - fi; \ - # Generate proxychains configuration file - printf "%s\n" \ - 'localnet 127.0.0.0/255.0.0.0' \ - 'localnet ::1/128' \ - 'proxy_dns' \ - 'remote_dns_subnet 224' \ - 'strict_chain' \ - 'tcp_connect_time_out 8000' \ - 'tcp_read_time_out 15000' \ - '[ProxyList]' \ - "$protocol $host $port" \ - > "/etc/proxychains4.conf"; \ - fi; \ - # Run migration - node "/app/docker.cjs"; \ - if [ "$?" -eq "0" ]; then \ - # Run the server - ${PROXYCHAINS} node "/app/server.js"; \ - fi; +ENTRYPOINT ["/bin/node"] + +CMD ["/app/startServer.js"] diff --git a/scripts/serverLauncher/startServer.js b/scripts/serverLauncher/startServer.js new file mode 100644 index 000000000000..d980702ee7d2 --- /dev/null +++ b/scripts/serverLauncher/startServer.js @@ -0,0 +1,181 @@ +const dns = require('dns').promises; +const fs = require('fs'); +const tls = require('tls'); +const { spawn } = require('child_process'); + +// Set file paths +const DB_MIGRATION_SCRIPT_PATH = '/app/docker.cjs'; +const SERVER_SCRIPT_PATH = '/app/server.js'; +const PROXYCHAINS_CONF_PATH = '/etc/proxychains4.conf'; + +// Function to check if a string is a valid IP address +const isValidIP = (ip, version = 4) => { + const ipv4Regex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/; + const ipv6Regex = /^(([0-9a-f]{1,4}:){7,7}[0-9a-f]{1,4}|([0-9a-f]{1,4}:){1,7}:|([0-9a-f]{1,4}:){1,6}:[0-9a-f]{1,4}|([0-9a-f]{1,4}:){1,5}(:[0-9a-f]{1,4}){1,2}|([0-9a-f]{1,4}:){1,4}(:[0-9a-f]{1,4}){1,3}|([0-9a-f]{1,4}:){1,3}(:[0-9a-f]{1,4}){1,4}|([0-9a-f]{1,4}:){1,2}(:[0-9a-f]{1,4}){1,5}|[0-9a-f]{1,4}:((:[0-9a-f]{1,4}){1,6})|:((:[0-9a-f]{1,4}){1,7}|:)|fe80:(:[0-9a-f]{0,4}){0,4}%[0-9a-z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-f]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/; + + switch (version) { + case 4: + return ipv4Regex.test(ip); + case 6: + return ipv6Regex.test(ip); + default: + return ipv4Regex.test(ip) || ipv6Regex.test(ip); + } +}; + +// Function to check TLS validity of a URL +const isValidTLS = (url = '') => { + if (!url) { + console.log('⚠️ TLS Check: No URL provided. Skipping TLS check. Ensure correct setting ENV.'); + console.log('-------------------------------------'); + return Promise.resolve(); + } + + const { protocol, host, port } = parseUrl(url); + if (protocol !== 'https') { + console.log(`⚠️ TLS Check: Non-HTTPS protocol (${protocol}). Skipping TLS check for ${url}.`); + console.log('-------------------------------------'); + return Promise.resolve(); + } + + const options = { host, port, servername: host }; + return new Promise((resolve, reject) => { + const socket = tls.connect(options, () => { + if (socket.authorized) { + console.log(`✅ TLS Check: Valid certificate for ${host}:${port}.`); + console.log('-------------------------------------'); + resolve(); + } + socket.end(); + }); + + socket.on('error', (err) => { + const errMsg = `❌ TLS Check: Error for ${host}:${port}. Details:`; + switch (err.code) { + case 'CERT_HAS_EXPIRED': + case 'DEPTH_ZERO_SELF_SIGNED_CERT': + console.error(`${errMsg} Certificate is not valid. Consider setting NODE_TLS_REJECT_UNAUTHORIZED="0" or mapping /etc/ssl/certs/ca-certificates.crt.`); + break; + case 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY': + console.error(`${errMsg} Unable to verify issuer. Ensure correct mapping of /etc/ssl/certs/ca-certificates.crt.`); + break; + default: + console.error(`${errMsg} Network issue. Check firewall or DNS.`); + break; + } + reject(err); + }); + }); +}; + +// Function to check TLS connections for OSS and Auth Issuer +const checkTLSConnections = async () => { + await Promise.all([ + isValidTLS(process.env.S3_ENDPOINT), + isValidTLS(process.env.S3_PUBLIC_DOMAIN), + isValidTLS(getEnvVarsByKeyword('_ISSUER')), + ]); +}; + +// Function to get environment variable by keyword +const getEnvVarsByKeyword = (keyword) => { + return Object.entries(process.env) + .filter(([key, value]) => key.includes(keyword) && value) + .map(([, value]) => value)[0] || null; +}; + +// Function to parse protocol, host and port from a URL +const parseUrl = (url) => { + const { protocol, hostname: host, port } = new URL(url); + return { protocol: protocol.replace(':', ''), host, port: port || 443 }; +}; + +// Function to resolve host IP via DNS +const resolveHostIP = async (host, version = 4) => { + try { + const { address } = await dns.lookup(host, { family: version }); + + if (!isValidIP(address, version)) { + console.error(`❌ DNS Error: Invalid resolved IP: ${address}. IP address must be IPv${version}.`); + process.exit(1); + } + + return address; + } catch (err) { + console.error(`❌ DNS Error: Could not resolve ${host}. Check DNS server.`, err); + process.exit(1); + } +}; + +// Function to generate proxychains configuration +const runProxyChainsConfGenerator = async (url) => { + const { protocol, host, port } = parseUrl(url); + + if (!['http', 'socks4', 'socks5'].includes(protocol)) { + console.error(`❌ ProxyChains: Invalid protocol (${protocol}). Protocol must be 'http', 'socks4' and 'socks5'.`); + process.exit(1); + } + + const validPort = parseInt(port, 10); + if (isNaN(validPort) || validPort <= 0 || validPort > 65535) { + console.error(`❌ ProxyChains: Invalid port (${port}). Port must be a number between 1 and 65535.`); + process.exit(1); + } + + let ip = isValidIP(host, 4) ? host : await resolveHostIP(host, 4); + + const configContent = ` +localnet 127.0.0.0/255.0.0.0 +localnet ::1/128 +proxy_dns +remote_dns_subnet 224 +strict_chain +tcp_connect_time_out 8000 +tcp_read_time_out 15000 +[ProxyList] +${protocol} ${ip} ${port} +`.trim(); + + fs.writeFileSync(PROXYCHAINS_CONF_PATH, configContent); + console.log(`✅ ProxyChains: All outgoing traffic routed via ${protocol}://${ip}:${port}.`); + console.log('-------------------------------------'); +}; + +// Function to execute a script with child process spawn +const runScript = (scriptPath, useProxy = false) => { + const command = useProxy ? ['/bin/proxychains', '-q', '/bin/node', scriptPath] : ['/bin/node', scriptPath]; + return new Promise((resolve, reject) => { + const process = spawn(command.shift(), command, { stdio: 'inherit' }); + process.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`🔴 Process exited with code ${code}`)))); + }); +}; + +// Main function to run the server with optional proxy +const runServer = async () => { + const PROXY_URL = process.env.PROXY_URL || ''; // Default empty string to avoid undefined errors + + if (PROXY_URL) { + await runProxyChainsConfGenerator(PROXY_URL); + return runScript(SERVER_SCRIPT_PATH, true); + } + return runScript(SERVER_SCRIPT_PATH); +}; + +// Main execution block +(async () => { + console.log('🌐 DNS Server:', dns.getServers()); + console.log('-------------------------------------'); + + if (process.env.DATABASE_DRIVER) { + try { + await runScript(DB_MIGRATION_SCRIPT_PATH); + await checkTLSConnections(); + } catch (err) { + console.error('❌ Error during DB migration or TLS connection check:', err); + process.exit(1); + } + } + + // Run the server in either database or non-database mode + await runServer(); +})();