diff --git a/.dockerignore b/.dockerignore index 6a47c2536..d60c2cb95 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,10 @@ backend/node_modules -frontend/node_modules \ No newline at end of file +frontend/node_modules +backend/frontend-build +**/node_modules +**/.next +.dockerignore +.git +README.md +.dockerignore +**/Dockerfile diff --git a/.gitignore b/.gitignore index da6eb5142..24dc274ba 100644 --- a/.gitignore +++ b/.gitignore @@ -33,7 +33,7 @@ reports junit.xml # next.js -/.next/ +.next/ /out/ # production @@ -59,4 +59,6 @@ yarn-error.log* .infisical.json # Editor specific -.vscode/* \ No newline at end of file +.vscode/* + +frontend-build \ No newline at end of file diff --git a/Dockerfile.standalone-infisical b/Dockerfile.standalone-infisical index 8770bb234..db316987b 100644 --- a/Dockerfile.standalone-infisical +++ b/Dockerfile.standalone-infisical @@ -1,7 +1,12 @@ ARG POSTHOG_HOST=https://app.posthog.com ARG POSTHOG_API_KEY=posthog-api-key -FROM node:16-alpine AS frontend-dependencies +FROM node:16-alpine AS base + +FROM base AS frontend-dependencies + +# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. +RUN apk add --no-cache libc6-compat WORKDIR /app @@ -11,7 +16,7 @@ COPY frontend/package.json frontend/package-lock.json frontend/next.config.js ./ RUN npm ci --only-production --ignore-scripts # Rebuild the source code only when needed -FROM node:16-alpine AS frontend-builder +FROM base AS frontend-builder WORKDIR /app # Copy dependencies @@ -32,13 +37,13 @@ ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID RUN npm run build # Production image -FROM node:16-alpine AS frontend-runner +FROM base AS frontend-runner WORKDIR /app RUN addgroup --system --gid 1001 nodejs -RUN adduser --system --uid 1001 nextjs +RUN adduser --system --uid 1001 non-root-user -RUN mkdir -p /app/.next/cache/images && chown nextjs:nodejs /app/.next/cache/images +RUN mkdir -p /app/.next/cache/images && chown non-root-user:nodejs /app/.next/cache/images VOLUME /app/.next/cache/images ARG POSTHOG_API_KEY @@ -48,20 +53,22 @@ ARG INTERCOM_ID ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \ BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID -COPY --chown=nextjs:nodejs --chmod=555 frontend/scripts ./scripts +COPY --chown=non-root-user:nodejs --chmod=555 frontend/scripts ./scripts COPY --from=frontend-builder /app/public ./public -RUN chown nextjs:nodejs ./public/data -COPY --from=frontend-builder --chown=nextjs:nodejs /app/.next/standalone ./ -COPY --from=frontend-builder --chown=nextjs:nodejs /app/.next/static ./.next/static +RUN chown non-root-user:nodejs ./public/data +COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/standalone ./ +COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/static ./.next/static -USER nextjs +USER non-root-user ENV NEXT_TELEMETRY_DISABLED 1 ## ## BACKEND ## -FROM node:16-alpine AS backend-build +FROM base AS backend-build +RUN addgroup --system --gid 1001 nodejs \ + && adduser --system --uid 1001 non-root-user WORKDIR /app @@ -69,10 +76,11 @@ COPY backend/package*.json ./ RUN npm ci --only-production COPY /backend . +COPY --chown=non-root-user:nodejs standalone-entrypoint.sh standalone-entrypoint.sh RUN npm run build # Production stage -FROM node:16-alpine AS backend-runner +FROM base AS backend-runner WORKDIR /app @@ -81,27 +89,36 @@ RUN npm ci --only-production COPY --from=backend-build /app . +RUN mkdir frontend-build + # Production stage -FROM node:16-alpine AS production +FROM base AS production +RUN addgroup --system --gid 1001 nodejs \ + && adduser --system --uid 1001 non-root-user WORKDIR / -# Install PM2 -RUN npm install -g pm2 -# Copy ecosystem.config.js -COPY ecosystem.config.js . - -RUN apk add --no-cache nginx - -COPY nginx/default-stand-alone-docker.conf /etc/nginx/nginx.conf - COPY --from=backend-runner /app /backend -COPY --from=frontend-runner /app/ /app/ +COPY --from=frontend-runner /app ./backend/frontend-build -EXPOSE 80 +ENV PORT 8080 ENV HTTPS_ENABLED false +ENV NODE_ENV production +ENV STANDALONE_BUILD true + +WORKDIR /backend + +ENV TELEMETRY_ENABLED true + +HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \ + CMD node healthcheck.js + +EXPOSE 8080 + +USER non-root-user + +CMD ["./standalone-entrypoint.sh"] -CMD ["pm2-runtime", "start", "ecosystem.config.js"] diff --git a/backend/src/helpers/database.ts b/backend/src/helpers/database.ts index 4780be346..611e941f5 100644 --- a/backend/src/helpers/database.ts +++ b/backend/src/helpers/database.ts @@ -14,7 +14,7 @@ export const initDatabaseHelper = async ({ }) => { try { await mongoose.connect(mongoURL); - + // allow empty strings to pass the required validator mongoose.Schema.Types.String.checkRequired(v => typeof v === "string"); @@ -31,14 +31,10 @@ export const initDatabaseHelper = async ({ * Close database conection */ export const closeDatabaseHelper = async () => { - return Promise.all([ - new Promise((resolve) => { - if (mongoose.connection && mongoose.connection.readyState == 1) { - mongoose.connection.close() - .then(() => resolve("Database connection closed")); - } else { - resolve("Database connection already closed"); - } - }), - ]); -} \ No newline at end of file + if (mongoose.connection && mongoose.connection.readyState === 1) { + await mongoose.connection.close(); + return "Database connection closed"; + } else { + return "Database connection already closed"; + } +}; \ No newline at end of file diff --git a/backend/src/index.ts b/backend/src/index.ts index 08b159d42..1276a90cc 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -87,6 +87,9 @@ import { setup } from "./utils/setup"; import { syncSecretsToThirdPartyServices } from "./queues/integrations/syncSecretsToThirdPartyServices"; import { githubPushEventSecretScan } from "./queues/secret-scanning/githubScanPushEvent"; const SmeeClient = require("smee-client"); // eslint-disable-line +import path from "path"; + +let handler: null | any = null; const main = async () => { await setup(); @@ -147,6 +150,27 @@ const main = async () => { next(); }); + if ((await getNodeEnv()) === "production" && process.env.STANDALONE_BUILD === "true") { + const nextJsBuildPath = path.join(__dirname, "../frontend-build"); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/no-var-requires + const conf = require("../frontend-build/.next/required-server-files.json").config; + const NextServer = + // eslint-disable-next-line @typescript-eslint/no-var-requires + require("../frontend-build/node_modules/next/dist/server/next-server").default; + const nextApp = new NextServer({ + dev: false, + dir: nextJsBuildPath, + port: await getPort(), + conf, + hostname: "local", + customServer: false + }); + + handler = nextApp.getRequestHandler(); + } + // (EE) routes app.use("/api/v1/secret", eeSecretRouter); app.use("/api/v1/secret-snapshot", eeSecretSnapshotRouter); @@ -209,6 +233,12 @@ const main = async () => { // server status app.use("/api", healthCheck); + if (handler) { + app.all("*", (req, res) => { + return handler(req, res); + }); + } + //* Handle unrouted requests and respond with proper error message as well as status code app.use((req, res, next) => { if (res.headersSent) return next(); diff --git a/backend/src/queues/integrations/syncSecretsToThirdPartyServices.ts b/backend/src/queues/integrations/syncSecretsToThirdPartyServices.ts index b18d7bccc..490b31c10 100644 --- a/backend/src/queues/integrations/syncSecretsToThirdPartyServices.ts +++ b/backend/src/queues/integrations/syncSecretsToThirdPartyServices.ts @@ -40,9 +40,9 @@ syncSecretsToThirdPartyServices.process(async (job: Job) => { const prefix = (integration.metadata?.secretPrefix || ""); const suffix = (integration.metadata?.secretSuffix || ""); const newKey = prefix + key + suffix; - + suffixedSecrets[newKey] = secrets[key]; - } + } } const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth); @@ -67,7 +67,7 @@ syncSecretsToThirdPartyServices.process(async (job: Job) => { }) syncSecretsToThirdPartyServices.on("error", (error) => { - console.log("QUEUE ERROR:", error) // eslint-disable-line + // console.log("QUEUE ERROR:", error) // eslint-disable-line }) export const syncSecretsToActiveIntegrationsQueue = (jobDetails: TSyncSecretsToThirdPartyServices) => { diff --git a/backend/src/utils/setup/index.ts b/backend/src/utils/setup/index.ts index d85d63aca..6625bff7b 100644 --- a/backend/src/utils/setup/index.ts +++ b/backend/src/utils/setup/index.ts @@ -55,9 +55,6 @@ export const setup = async () => { // initializing global feature set await EELicenseService.initGlobalFeatureSet(); - // initializing the database connection - await DatabaseService.initDatabase(await getMongoURL()); - await initializePassport(); // re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY diff --git a/ecosystem.config.js b/ecosystem.config.js deleted file mode 100644 index 5c8cc6832..000000000 --- a/ecosystem.config.js +++ /dev/null @@ -1,32 +0,0 @@ -module.exports = { - apps: [ - { - name: 'frontend', - script: "./scripts/start.sh", - instances: 1, - cwd: "./app", - interpreter: 'sh', - exec_mode: "fork", - autorestart: true, - watch: false, - max_memory_restart: '500M', - }, - { - name: 'backend', - script: 'npm', - args: 'run start', - cwd: "./backend", - instances: 1, - exec_mode: "fork", - autorestart: true, - watch: false, - max_memory_restart: '500M', - }, - { - name: "nginx", - script: "nginx", - args: "-g 'daemon off;'", - exec_interpreter: "none", - }, - ], -}; \ No newline at end of file diff --git a/frontend/next.config.js b/frontend/next.config.js index 51cf8f308..9d894694f 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -1,8 +1,4 @@ -// @ts-check -/** - * @type {import('next').NextConfig} - **/ const path = require("path"); const ContentSecurityPolicy = ` @@ -53,7 +49,9 @@ const securityHeaders = [ value: ContentSecurityPolicy.replace(/\s{2,}/g, " ").trim() } ]; - +/** + * @type {import('next').NextConfig} + **/ module.exports = { output: "standalone", i18n: { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f97070490..6a8026255 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,5 +1,5 @@ { - "name": "npm-proj-1695919945735-0.225773463026700768rr1Oh", + "name": "frontend", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/frontend/scripts/initialize-standalone-build.sh b/frontend/scripts/initialize-standalone-build.sh new file mode 100755 index 000000000..d9138bb77 --- /dev/null +++ b/frontend/scripts/initialize-standalone-build.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +scripts/replace-standalone-build-variable.sh "$BAKED_NEXT_PUBLIC_POSTHOG_API_KEY" "$NEXT_PUBLIC_POSTHOG_API_KEY" + +scripts/replace-standalone-build-variable.sh "$BAKED_NEXT_PUBLIC_INTERCOM_ID" "$NEXT_PUBLIC_INTERCOM_ID" + +if [ "$TELEMETRY_ENABLED" != "false" ]; then + echo "Telemetry is enabled" + scripts/set-standalone-build-telemetry.sh true +else + echo "Client opted out of telemetry" + scripts/set-standalone-build-telemetry.sh false +fi diff --git a/frontend/scripts/replace-standalone-build-variable.sh b/frontend/scripts/replace-standalone-build-variable.sh new file mode 100755 index 000000000..fde4ca282 --- /dev/null +++ b/frontend/scripts/replace-standalone-build-variable.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +ORIGINAL=$1 +REPLACEMENT=$2 + +if [ "${ORIGINAL}" = "${REPLACEMENT}" ]; then + echo "Environment variable replacement is the same, skipping.." + exit 0 +fi + +echo "Replacing pre-baked value.." + +find public .next -type f -name "*.js" | +while read file; do + sed -i "s|$ORIGINAL|$REPLACEMENT|g" "$file" +done diff --git a/frontend/scripts/replace-variable.sh b/frontend/scripts/replace-variable.sh old mode 100644 new mode 100755 diff --git a/frontend/scripts/set-standalone-build-telemetry.sh b/frontend/scripts/set-standalone-build-telemetry.sh new file mode 100644 index 000000000..5e788b6b9 --- /dev/null +++ b/frontend/scripts/set-standalone-build-telemetry.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +VALUE=$1 + +find public .next -type f -name "*.js" | +while read file; do + sed -i "s|TELEMETRY_CAPTURING_ENABLED|$VALUE|g" "$file" +done diff --git a/frontend/scripts/set-telemetry.sh b/frontend/scripts/set-telemetry.sh old mode 100644 new mode 100755 diff --git a/frontend/src/components/analytics/posthog.ts b/frontend/src/components/analytics/posthog.ts index 706f79515..246d5c9cc 100644 --- a/frontend/src/components/analytics/posthog.ts +++ b/frontend/src/components/analytics/posthog.ts @@ -10,7 +10,7 @@ export const initPostHog = () => { try { if (typeof window !== "undefined") { // @ts-ignore - if (ENV === "production" && TELEMETRY_CAPTURING_ENABLED) { + if (ENV === "production" && TELEMETRY_CAPTURING_ENABLED === "true") { posthog.init(POSTHOG_API_KEY, { api_host: POSTHOG_HOST }); diff --git a/frontend/src/components/utilities/telemetry/Telemetry.ts b/frontend/src/components/utilities/telemetry/Telemetry.ts index 2676bed85..76e86fa37 100644 --- a/frontend/src/components/utilities/telemetry/Telemetry.ts +++ b/frontend/src/components/utilities/telemetry/Telemetry.ts @@ -13,7 +13,7 @@ class Capturer { } capture(item: string) { - if (ENV === 'production' && TELEMETRY_CAPTURING_ENABLED) { + if (ENV === 'production' && TELEMETRY_CAPTURING_ENABLED === "true") { try { this.api.capture(item); } catch (error) { @@ -23,7 +23,7 @@ class Capturer { } identify(id: string, email?: string) { - if (ENV === 'production' && TELEMETRY_CAPTURING_ENABLED) { + if (ENV === 'production' && TELEMETRY_CAPTURING_ENABLED === "true") { try { this.api.identify(id, { email: email diff --git a/nginx/default-stand-alone-docker.conf b/nginx/default-stand-alone-docker.conf deleted file mode 100644 index b40e0fb13..000000000 --- a/nginx/default-stand-alone-docker.conf +++ /dev/null @@ -1,36 +0,0 @@ -events {} -http { - server { - listen 80; - - location /api { - proxy_set_header X-Real-RIP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - - proxy_set_header Host $http_host; - proxy_set_header X-NginX-Proxy true; - - proxy_pass http://localhost:4000; # for backend - proxy_redirect off; - - # proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict"; - proxy_cookie_path / "/; HttpOnly; SameSite=strict"; - } - - location / { - include /etc/nginx/mime.types; - - proxy_set_header X-Real-RIP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - - proxy_set_header Host $http_host; - proxy_set_header X-NginX-Proxy true; - - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - - proxy_pass http://localhost:3000; # for frontend - proxy_redirect off; - } - } -} \ No newline at end of file diff --git a/standalone-entrypoint.sh b/standalone-entrypoint.sh new file mode 100755 index 000000000..e15931dc4 --- /dev/null +++ b/standalone-entrypoint.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +cd frontend-build +scripts/initialize-standalone-build.sh + +cd ../ + +exec node build/index.js