Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(root): introducing graceful shutdown and optimise docker images #6754

Open
wants to merge 22 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
21d78ed
feat(application-generic): Add graceful shutdown module
merrcury Oct 22, 2024
4f2b9b4
Merge branch 'next' of github.com:novuhq/novu into feat/docker-improv…
merrcury Oct 22, 2024
aa9cb4e
build: make zod-to-json-schema a dynamic import
rifont Oct 22, 2024
a2c2fe3
feat(docker): Add base Dockerfile for GHCR publishing
merrcury Oct 23, 2024
7b0bc43
feat(docker): Optimize Dockerfile for GHCR publishing
merrcury Oct 23, 2024
ecc0632
feat(docker): Add .dockerignore file to exclude unnecessary files and…
merrcury Oct 23, 2024
a55daf7
feat(docker): Add .dockerignore file and optimize Dockerfile for GHCR…
merrcury Oct 23, 2024
f5394d3
feat(docker): Add .dockerignore file and optimize Dockerfile for GHCR…
merrcury Oct 23, 2024
e5bd920
feat(docker): Add .dockerignore file and optimize Dockerfile for GHCR…
merrcury Oct 23, 2024
5ae7d8f
refactor(deps): Remove unused dependencies in package.json files
merrcury Oct 23, 2024
ca541f6
refactor(nest): Update import statement for Request and Response type…
merrcury Oct 23, 2024
134098f
Merge branch 'next' of github.com:novuhq/novu into feat/docker-improv…
merrcury Oct 23, 2024
2bf0eed
Merge branch 'next' of github.com:novuhq/novu into feat/docker-improv…
merrcury Oct 23, 2024
b722e36
Merge branch 'next' of github.com:novuhq/novu into feat/docker-improv…
merrcury Oct 23, 2024
5a82753
refactor(docker): add appropriate comments to each file
merrcury Oct 23, 2024
5ab918e
refactor(application-generic): update import statement for graceful-s…
merrcury Oct 23, 2024
4ccc8b5
Merge branch 'next' of github.com:novuhq/novu into feat/docker-improv…
merrcury Oct 24, 2024
6d620b1
Merge branch 'next' of github.com:novuhq/novu into feat/docker-improv…
merrcury Oct 29, 2024
f8a1800
Merge branch 'next' of github.com:novuhq/novu into feat/docker-improv…
merrcury Oct 29, 2024
24de9ff
feat(config): Add GRACEFUL_SHUTDOWN_TIMEOUT environment variable vali…
merrcury Oct 30, 2024
1ab6f15
Merge branch 'next' of github.com:novuhq/novu into feat/docker-improv…
merrcury Oct 30, 2024
4a3e98d
fix(root): Align @nestjs/common versions across the repo
SokratisVidros Oct 30, 2024
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
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 @@ -107,7 +107,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 { SharedModule } from './app/shared/shared.module';
import { StepSchemasModule } from './app/step-schemas/step-schemas.module';
import { WorkflowModule } from './app/workflows-v2/workflow.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 @@ -107,6 +107,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
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
Loading