Skip to content

Commit

Permalink
refactor(root): introducing graceful shutdown and optimise docker ima…
Browse files Browse the repository at this point in the history
…ges (#6754)
  • Loading branch information
merrcury authored Nov 4, 2024
1 parent 28939d8 commit 2a64bdc
Show file tree
Hide file tree
Showing 35 changed files with 684 additions and 480 deletions.
44 changes: 44 additions & 0 deletions apps/api/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Ignore node_modules to avoid copying them into the image
node_modules

# Ignore local environment files
.env
.env.local
.env.*.local

# Ignore logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Ignore build directories
dist
build

# Ignore test directories and files
coverage
*.test.js
*.spec.js
*.test.ts
*.spec.ts

# Ignore Docker-related files
Dockerfile*
.dockerignore

# Ignore IDE/editor config files
.vscode
.idea
*.swp

# Ignore OS-specific files
.DS_Store
Thumbs.db

# Ignore temporary files
tmp
temp
*.tmp
*.temp
74 changes: 28 additions & 46 deletions apps/api/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,72 +1,54 @@
FROM node:20-alpine3.19 AS dev_base
RUN apk add g++ make py3-pip

ENV NX_DAEMON=false

RUN npm i pm2 -g
RUN npm --no-update-notifier --no-fund --global install [email protected]
RUN pnpm --version

USER 1000
WORKDIR /usr/src/app

# ------- DEV BUILD ----------
FROM dev_base AS dev
# Use the base image for development
FROM ghcr.io/novuhq/novu/base:1.0.0 AS dev
ARG PACKAGE_PATH

COPY --chown=1000:1000 ./meta .
COPY --chown=1000:1000 ./deps .
COPY --chown=1000:1000 ./pkg .

RUN --mount=type=secret,id=BULL_MQ_PRO_NPM_TOKEN,uid=1000 export BULL_MQ_PRO_NPM_TOKEN=$(cat /run/secrets/BULL_MQ_PRO_NPM_TOKEN) && \
if [ -n "${BULL_MQ_PRO_NPM_TOKEN}" ] ; then echo 'Building with Enterprise Edition of Novu'; rm -f .npmrc ; cp .npmrc-cloud .npmrc ; fi

RUN --mount=type=cache,id=pnpm-store-api,target=/root/.pnpm-store\
--mount=type=secret,id=BULL_MQ_PRO_NPM_TOKEN,uid=1000 export BULL_MQ_PRO_NPM_TOKEN=$(cat /run/secrets/BULL_MQ_PRO_NPM_TOKEN) && \
pnpm install --filter "novuhq" --filter "{${PACKAGE_PATH}}..."\
--frozen-lockfile\
--unsafe-perm
# Copy necessary directories to the image
COPY --chown=1000:1000 ./meta ./deps ./pkg ./

RUN --mount=type=secret,id=BULL_MQ_PRO_NPM_TOKEN,uid=1000 export BULL_MQ_PRO_NPM_TOKEN=$(cat /run/secrets/BULL_MQ_PRO_NPM_TOKEN) && NODE_ENV=production NX_DAEMON=false pnpm build:api
# Install dependencies and build the project
RUN --mount=type=secret,id=BULL_MQ_PRO_NPM_TOKEN,uid=1000 \
BULL_MQ_PRO_NPM_TOKEN=$(cat /run/secrets/BULL_MQ_PRO_NPM_TOKEN) && \
[ -n "$BULL_MQ_PRO_NPM_TOKEN" ] && echo 'Building with Enterprise Edition of Novu' && \
rm -f .npmrc && cp .npmrc-cloud .npmrc || true && \
pnpm install --filter "novuhq" --filter "{${PACKAGE_PATH}}..." --frozen-lockfile --unsafe-perm && \
NODE_ENV=production NX_DAEMON=false pnpm build:api

# Set the working directory to the API app and copy example environment file
WORKDIR /usr/src/app/apps/api

RUN cp src/.example.env dist/.env
RUN cp src/.env.test dist/.env.test
RUN cp src/.env.development dist/.env.development
RUN cp src/.env.production dist/.env.production

# Set the working directory to the root of the app
WORKDIR /usr/src/app

# ------- ASSETS BUILD ----------
# Create a new stage for building assets
FROM dev AS assets

WORKDIR /usr/src/app

# Remove all dependencies so later we can only install prod dependencies without devDependencies
# Remove node_modules and source directories
RUN rm -rf node_modules && pnpm recursive exec -- rm -rf ./src ./node_modules

# ------- PRODUCTION BUILD ----------
FROM dev_base AS prod
# Use the base image for production
FROM ghcr.io/novuhq/novu/base:1.0.0 AS prod

ARG PACKAGE_PATH

# Set environment variables for production
ENV CI=true
ENV NEW_RELIC_NO_CONFIG_FILE=true

# Set the working directory to the root of the app
WORKDIR /usr/src/app

COPY --chown=1000:1000 ./meta .

# Get the build artifacts that only include dist folders
# Copy necessary directories from the build stage
COPY --chown=1000:1000 ./meta ./
COPY --chown=1000:1000 --from=assets /usr/src/app .

RUN --mount=type=cache,id=pnpm-store-api,target=/root/.pnpm-store\
--mount=type=secret,id=BULL_MQ_PRO_NPM_TOKEN,uid=1000 export BULL_MQ_PRO_NPM_TOKEN=$(cat /run/secrets/BULL_MQ_PRO_NPM_TOKEN) && \
pnpm install --filter "{${PACKAGE_PATH}}..." \
--frozen-lockfile \
--unsafe-perm

ENV NEW_RELIC_NO_CONFIG_FILE=true
# Install production dependencies
RUN --mount=type=cache,id=pnpm-store-api,target=/root/.pnpm-store \
--mount=type=secret,id=BULL_MQ_PRO_NPM_TOKEN,uid=1000 \
pnpm install --filter "{${PACKAGE_PATH}}..." --frozen-lockfile --unsafe-perm --prod

# Set the working directory to the API app and start the application using pm2-runtime
WORKDIR /usr/src/app/apps/api
CMD [ "pm2-runtime","start", "dist/main.js" ]
CMD [ "pm2-runtime", "start", "dist/main.js" ]
1 change: 0 additions & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,6 @@
"@types/bull": "^3.15.8",
"@types/chai": "^4.2.11",
"@types/express": "4.17.17",
"@types/faker": "^6.6.9",
"@types/mocha": "^10.0.2",
"@types/node": "^20.15.0",
"@types/passport-github": "^1.1.5",
Expand Down
63 changes: 32 additions & 31 deletions apps/api/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,50 @@
/* eslint-disable global-require */
import { DynamicModule, Logger, Module, Provider } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { Type } from '@nestjs/common/interfaces/type.interface';
import { ForwardReference } from '@nestjs/common/interfaces/modules/forward-reference.interface';
import { ProfilingModule, TracingModule } from '@novu/application-generic';
import { Type } from '@nestjs/common/interfaces/type.interface';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { GracefulShutdownConfigModule, ProfilingModule, TracingModule } from '@novu/application-generic';
import { isClerkEnabled } from '@novu/shared';
import { SentryModule } from '@sentry/nestjs/setup';
import packageJson from '../package.json';
import { SharedModule } from './app/shared/shared.module';
import { UserModule } from './app/user/user.module';
import { AnalyticsModule } from './app/analytics/analytics.module';
import { AuthModule } from './app/auth/auth.module';
import { TestingModule } from './app/testing/testing.module';
import { HealthModule } from './app/health/health.module';
import { OrganizationModule } from './app/organization/organization.module';
import { ExecutionDetailsModule } from './app/execution-details/execution-details.module';
import { EventsModule } from './app/events/events.module';
import { WidgetsModule } from './app/widgets/widgets.module';
import { NotificationModule } from './app/notifications/notification.module';
import { StorageModule } from './app/storage/storage.module';
import { NotificationGroupsModule } from './app/notification-groups/notification-groups.module';
import { InvitesModule } from './app/invites/invites.module';
import { ContentTemplatesModule } from './app/content-templates/content-templates.module';
import { IntegrationModule } from './app/integrations/integrations.module';
import { BlueprintModule } from './app/blueprint/blueprint.module';
import { BridgeModule } from './app/bridge/bridge.module';
import { ChangeModule } from './app/change/change.module';
import { SubscribersModule } from './app/subscribers/subscribers.module';
import { ContentTemplatesModule } from './app/content-templates/content-templates.module';
import { EnvironmentsModuleV1 } from './app/environments-v1/environments-v1.module';
import { EnvironmentsModule } from './app/environments-v2/environments.module';
import { EventsModule } from './app/events/events.module';
import { ExecutionDetailsModule } from './app/execution-details/execution-details.module';
import { FeedsModule } from './app/feeds/feeds.module';
import { HealthModule } from './app/health/health.module';
import { InboundParseModule } from './app/inbound-parse/inbound-parse.module';
import { InboxModule } from './app/inbox/inbox.module';
import { IntegrationModule } from './app/integrations/integrations.module';
import { InvitesModule } from './app/invites/invites.module';
import { LayoutsModule } from './app/layouts/layouts.module';
import { MessagesModule } from './app/messages/messages.module';
import { NotificationGroupsModule } from './app/notification-groups/notification-groups.module';
import { NotificationModule } from './app/notifications/notification.module';
import { OrganizationModule } from './app/organization/organization.module';
import { PartnerIntegrationsModule } from './app/partner-integrations/partner-integrations.module';
import { TopicsModule } from './app/topics/topics.module';
import { InboundParseModule } from './app/inbound-parse/inbound-parse.module';
import { BlueprintModule } from './app/blueprint/blueprint.module';
import { TenantModule } from './app/tenant/tenant.module';
import { IdempotencyInterceptor } from './app/shared/framework/idempotency.interceptor';
import { WorkflowOverridesModule } from './app/workflow-overrides/workflow-overrides.module';
import { PreferencesModule } from './app/preferences';
import { ApiRateLimitInterceptor } from './app/rate-limiting/guards';
import { RateLimitingModule } from './app/rate-limiting/rate-limiting.module';
import { IdempotencyInterceptor } from './app/shared/framework/idempotency.interceptor';
import { ProductFeatureInterceptor } from './app/shared/interceptors/product-feature.interceptor';
import { AnalyticsModule } from './app/analytics/analytics.module';
import { InboxModule } from './app/inbox/inbox.module';
import { BridgeModule } from './app/bridge/bridge.module';
import { PreferencesModule } from './app/preferences';
import { WorkflowModule } from './app/workflows-v2/workflow.module';
import { SharedModule } from './app/shared/shared.module';
import { StorageModule } from './app/storage/storage.module';
import { SubscribersModule } from './app/subscribers/subscribers.module';
import { TenantModule } from './app/tenant/tenant.module';
import { TestingModule } from './app/testing/testing.module';
import { TopicsModule } from './app/topics/topics.module';
import { UserModule } from './app/user/user.module';
import { WidgetsModule } from './app/widgets/widgets.module';
import { WorkflowOverridesModule } from './app/workflow-overrides/workflow-overrides.module';
import { WorkflowModuleV1 } from './app/workflows-v1/workflow-v1.module';
import { EnvironmentsModuleV1 } from './app/environments-v1/environments-v1.module';
import { EnvironmentsModule } from './app/environments-v2/environments.module';
import { WorkflowModule } from './app/workflows-v2/workflow.module';

const enterpriseImports = (): Array<Type | DynamicModule | Promise<DynamicModule> | ForwardReference> => {
const modules: Array<Type | DynamicModule | Promise<DynamicModule> | ForwardReference> = [];
Expand Down Expand Up @@ -106,6 +106,7 @@ const baseModules: Array<Type | DynamicModule | Promise<DynamicModule> | Forward
BridgeModule,
PreferencesModule,
WorkflowModule,
GracefulShutdownConfigModule.forRootAsync(),
EnvironmentsModule,
];

Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/app/environments-v1/novu-bridge.controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Controller, Req, Res, Inject, Get, Post, Options } from '@nestjs/common';
import { Request, Response } from 'express';
import type { Request, Response } from 'express';
import { ApiExcludeController } from '@nestjs/swagger';
import { NovuClient } from '@novu/framework/nest';
import { NovuBridgeClient } from './novu-bridge-client';
Expand Down
12 changes: 5 additions & 7 deletions apps/api/src/bootstrap.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import 'newrelic';
import './config/env.config';
import './instrument';
import 'newrelic';

import helmet from 'helmet';
import { INestApplication, Logger, ValidationPipe, VersioningType } from '@nestjs/common';
import { NestFactory, Reflector } from '@nestjs/core';
import bodyParser from 'body-parser';
import helmet from 'helmet';

import { BullMqService, getErrorInterceptor, Logger as PinoLogger } from '@novu/application-generic';
import { ExpressAdapter } from '@nestjs/platform-express';
import { CONTEXT_PATH, corsOptionsDelegate, validateEnv } from './config';
import { BullMqService, getErrorInterceptor, Logger as PinoLogger } from '@novu/application-generic';
import { AppModule } from './app.module';
import { setupSwagger } from './app/shared/framework/swagger/swagger.controller';
import { SubscriberRouteGuard } from './app/auth/framework/subscriber-route.guard';
import { ResponseInterceptor } from './app/shared/framework/response.interceptor';
import { setupSwagger } from './app/shared/framework/swagger/swagger.controller';
import { CONTEXT_PATH, corsOptionsDelegate, validateEnv } from './config';
import { AllExceptionsFilter } from './exception-filter';

const passport = require('passport');
Expand Down Expand Up @@ -108,8 +108,6 @@ export async function bootstrap(expressApp?): Promise<INestApplication> {
await app.listen(process.env.PORT);
}

app.enableShutdownHooks();

Logger.log(`Started application in NODE_ENV=${process.env.NODE_ENV} on port ${process.env.PORT}`);

return app;
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/config/env.validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export const envValidators = {
NOVU_INVITE_TEAM_MEMBER_NUDGE_TRIGGER_IDENTIFIER: str({ default: undefined }),
HUBSPOT_INVITE_NUDGE_EMAIL_USER_LIST_ID: str({ default: undefined }),
HUBSPOT_PRIVATE_APP_ACCESS_TOKEN: str({ default: undefined }),
GRACEFUL_SHUTDOWN_TIMEOUT: num({ default: 5000 }),
// Feature Flags
...Object.keys(FeatureFlagsKeysEnum).reduce(
(acc, key) => {
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/exception-filter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ArgumentsHost, ExceptionFilter, HttpException, HttpStatus } from '@nestjs/common';
import { Response } from 'express';
import type { Response, Request } from 'express';
import { CommandValidationException, PinoLogger } from '@novu/application-generic';
import { randomUUID } from 'node:crypto';
import { captureException } from '@sentry/node';
Expand Down
44 changes: 44 additions & 0 deletions apps/webhook/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Ignore node_modules to avoid copying them into the image
node_modules

# Ignore local environment files
.env
.env.local
.env.*.local

# Ignore logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Ignore build directories
dist
build

# Ignore test directories and files
coverage
*.test.js
*.spec.js
*.test.ts
*.spec.ts

# Ignore Docker-related files
Dockerfile*
.dockerignore

# Ignore IDE/editor config files
.vscode
.idea
*.swp

# Ignore OS-specific files
.DS_Store
Thumbs.db

# Ignore temporary files
tmp
temp
*.tmp
*.temp
Loading

0 comments on commit 2a64bdc

Please sign in to comment.