Develop a CI/CD Pipeline for NestJS Backend Using CircleCI and AWS EC2

Develop a CI/CD Pipeline for NestJS Backend Using CircleCI and AWS EC2

Automate Your NestJS Backend Deployment: A Comprehensive Guide to Setting Up CI/CD with CircleCI and AWS EC2

Hey there, fellow developers! Ever wondered how to make deploying your NestJS backend a breeze? Well, you're in the right place. In this blog, we'll walk you through setting up a simple Continuous Integration/Continuous Deployment (CI/CD) pipeline using CircleCI and AWS EC2. No fancy jargon, just easy steps. We'll cover Prisma ORM, Postgres, unit tests, end-to-end tests, and Nginx for deployment. Say goodbye to deployment headaches and hello to smooth sailing with your NestJS projects! Let's get started.

How to setup your project

.
├── node_modules
├── dist
├── .circleci/
│   ├── config.yml
│   ├── docker-compose-test.yaml
│   ├── e2e-test.env
│   ├── jest-e2e.json
│   └── test.env
├── deploy/
│   ├── default.conf.template
│   ├── docker-compose-deploy.yaml
│   ├── migrate_db.sh
│   ├── nestjsapp.deploy.Dockerfile
│   ├── reboot_app.sh
│   └── start_containers.sh
├── src/
│   ├── app.controller.ts
│   ├── app.module.ts
│   ├── app.service.ts
│   └── main.ts
├── test/
│   └── app.e2e.spec.ts
└── .env

OK! Lets describe each directory

  • .circleci - config files required for circle ci integration. Including the config file

  • deploy - config files and scripts required to deploy the app in the remote server

  • src - your app source code

  • test - end to end tests in your app

  • .env - environment variables

  • dist - artifacts or transpiled javascript code of your app which is ready to run in production server

  • node_modules - the heaviest folder in the universe 😁

First things first, configure the EC2 instance

What you need ?

  • An EC2 instance with Ubuntu 20.04 LTS

  • A private key to SSH to your instance (.pem file)

  • ports 22 and 443 open in the AWS network security for your instance

  • A domain name (since we are deploying in prod, we need SSL and domain)

  • SSL certificate (certificate and private key)

Make the server ready for deployment

  1. Install docker on the server. Follow the official docker installation steps below
    Official Docker Installation Steps for Ubuntu

  2. Run docker post installation steps from the official doc below
    Docker Ubuntu Post Installation Steps

  3.  docker volume create pgdata
    
  4.  #Navigate to home directory and create a directory for copying your app artifacts
     cd $HOME && mkdir my_nest_js_prod_app #my_nest_js_prod_app is where you store prod files
    
  5. Copy the certificate and key. Make sure you have certificate (.crt) and key file (.key) for your domain and copy the certificate file and key file to /etc/ssl/<your-domain>/
    eg: /etc/ssl/<your-domain>/certificate.crt
    /etc/ssl/<your-domain>/key.key
    Note: For automatic deployment and Nginx template file we store the domain name in an .env file.

Integration - Unit and E2E tests before deploying.

Integration testing, including unit and end-to-end tests, is crucial in the CI/CD pipeline for deploying backend apps. Unit tests catch bugs early, aid code maintainability, and provide rapid feedback. End-to-end tests validate the entire system, ensuring seamless component interaction and a reliable user experience. Together, these tests enhance code quality, support continuous improvement, and instill confidence in the deployed application's functionality and stability.

How to use CircleCI for integration ? the .circleci/config.yml file.

OK!. Lets see how we can configure the circleci config file for integration.

version: 2.1

jobs:
  e2e_test:
    docker:
      - image: cimg/node:18.17.1
      - image: cimg/postgres:15.2
        environment:
          POSTGRES_USER: postgres_test
          POSTGRES_PASSWORD: test_pw
          POSTGRES_DB: demo1_test
    steps:
      - checkout
      - restore_cache:
          keys:
            - v1-yarn-deps-e2e_tests-{{ .Branch }}-{{ checksum "yarn.lock" }}
      - run:
          name: "Copying test.env File"
          command: |
            cp .circleci/e2e-test.env .env
      - run:
          name: "Installing Dependencies"
          command: |
            yarn install
      - save_cache:
          key: v1-yarn-deps-e2e_tests-{{ .Branch }}-{{ checksum "yarn.lock" }}
          paths:
            - node_modules
      - run:
          name: "Generating prisma models"
          command: |
            yarn prisma generate
      - run:
          name: "Running Migrations"
          command: |
            yarn prisma migrate deploy
      - run:
          name: "Running Seed"
          command: |
            yarn seed 2
      - run:
          name: "Running E2E Tests"
          command: |
            yarn jest --config ./.circleci/jest-e2e.json
      - store_test_results:
          path: junit.xml

  unit_test:
    machine:
      image: ubuntu-2004:current
      docker_layer_caching: true
    steps:
      - checkout
      - restore_cache:
          keys:
            - v1-yarn-deps-unit_tests-{{ .Branch }}-{{ checksum "yarn.lock" }}
      - run:
          name: "Copying test.env File"
          command: |
            cp .circleci/test.env .env
      - run:
          name: "Building Latest Docker Image"
          command: |
            docker build -t 'demo/app-backend-api-node'  -f 'deploy/nestjsapp.deploy.Dockerfile' .
      - run:
          name: "Starting Test Docker Containers"
          command: |
            docker-compose -f .circleci/docker-compose-test.yaml -p 'app_test'  --env-file .env up -d
      - run:
          name: "Installing Dependencies"
          command: |
            docker run --interactive --volume $PWD:/app:rw demo/app-backend-api-node yarn install
      - save_cache:
          key: v1-yarn-deps-unit_tests-{{ .Branch }}-{{ checksum "yarn.lock" }}
          paths:
            - node_modules
      - run:
          name: "Generating prisma models"
          command: |
            docker compose -f '.circleci/docker-compose-test.yaml' -p 'app_test' --env-file .env run app yarn prisma generate
      - run:
          name: "Running Unit Tests"
          command: |
            docker compose -f '.circleci/docker-compose-test.yaml' -p 'app_test' --env-file .env run app yarn test --forceExit
      - store_test_results:
          path: junit.xml

workflows:
  test:
    jobs:
      - unit_test
      - e2e_test
  1. Docker Images Setup:

    • Utilizes two Docker images: cimg/node:18.17.1 for Node.js application code and cimg/postgres:15.2 for PostgreSQL database.

    • Configures PostgreSQL environment variables for testing (POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB).

  2. Checkout and Caching:

    • Checks out the code from the repository.

    • Restores cached dependencies based on the yarn.lock file to speed up the process.

  3. Environment Setup:

    • Copies the e2e-test.env file to .env for environment configuration.
  4. Dependency Installation:

    • Installs project dependencies using yarn install.
  5. Prisma Models Generation:

    • Generates Prisma models with the command yarn prisma generate.
  6. Database Setup:

    • Runs database migrations using yarn prisma migrate deploy.

    • Seeds the database with sample data using yarn seed 2.

  7. E2E Testing:

    • Executes end-to-end tests using Jest with the configuration specified in .circleci/jest-e2e.json.
  8. Test Results Storage:

    • Stores the test results in junit.xml for further analysis.

unit_test Job:

  1. Machine Setup:

    • Uses an Ubuntu 20.04 image for building Docker containers.

    • Enables Docker layer caching to optimize dependency management.

  2. Checkout and Caching:

    • Checks out the code.

    • Restores cached dependencies based on the yarn.lock file.

  3. Environment Setup:

    • Copies the test.env file to .env for environment configuration.
  4. Docker Container Setup:

    • Builds the Docker image with the specified Dockerfile (deploy/nestjsapp.deploy.Dockerfile).

    • Starts Docker containers using Docker Compose with the configuration in .circleci/docker-compose-test.yaml.

  5. Dependency Installation:

    • Installs project dependencies within the Docker container using docker run.
  6. Prisma Models Generation:

    • Generates Prisma models within the Docker container using docker compose.
  7. Unit Testing:

    • Runs unit tests using the command docker compose ... yarn test --forceExit within the Docker container.
  8. Test Results Storage:

    • Stores the test results in junit.xml for further analysis.

Workflow:

  • Defines a workflow named test that orchestrates the execution of the unit_test and e2e_test jobs in sequence.

This configuration ensures a comprehensive testing process for both unit and end-to-end scenarios, integrating seamlessly into a CI/CD pipeline for backend applications. The use of Docker containers, caching, and specific testing configurations enhances efficiency and reliability throughout the testing and deployment processes.

.circleci/docker-compose-test.yaml

version: '3.8'

services:
  app:
    build:
      context: ../deploy/
      dockerfile: nestjsapp.deploy.Dockerfile
    volumes:
      - ../:/app:rw
    restart: always
    ports:
      - '${APP_PORT}:${APP_PORT}'
    depends_on:
      - database
    networks:
      - default
    command: ['tail', '-f', '/dev/null']

  database:
    image: postgres:15.2-alpine
    restart: always
    ports:
      - '${POSTGRES_PORT}:5432'
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DATABASE}
    networks:
      - default
networks:
  default:
    name: ${NETWORK_NAME}

Services Section:

app Service:

  1. Build Configuration:

    • The service is built from the context specified by ../deploy/ using the Dockerfile named nestjsapp.deploy.Dockerfile.
  2. Volume Mounting:

    • Mounts the parent directory (../) to the /app directory inside the container with read and write permissions.
  3. Container Restart Policy:

    • Configures the service to restart always.
  4. Port Mapping:

    • Maps the host's ${APP_PORT} to the container's ${APP_PORT} for accessing the application.
  5. Dependency on database Service:

    • Specifies that the app service depends on the database service, ensuring the database container is started before the application container.
  6. Network Configuration:

    • Assigns the service to the default Docker network (default).
  7. Command Configuration:

    • Overrides the default command with ['tail', '-f', '/dev/null'], essentially keeping the container running in the foreground.

database Service:

  1. Image Configuration:

    • Pulls the postgres:15.2-alpine image from Docker Hub.
  2. Container Restart Policy:

    • Configures the service to restart always.
  3. Port Mapping:

    • Maps the host's ${POSTGRES_PORT} to the container's PostgreSQL default port (5432) for accessing the database.
  4. Environment Variables:

    • Sets environment variables for PostgreSQL user, password, and database name based on the provided variables.
  5. Network Configuration:

    • Assigns the service to the default Docker network (default).

Networks Section:

  • Defines a Docker network named ${NETWORK_NAME} (variable substitution) and assigns it as the default network for the services.

Variable Substitution:

  • Variables such as ${APP_PORT}, ${POSTGRES_PORT}, ${POSTGRES_USER}, ${POSTGRES_PASSWORD}, ${POSTGRES_DATABASE}, and ${NETWORK_NAME} are used for dynamic configuration. These should be defined elsewhere or in an environment file.

This Docker Compose file sets up two services, app and database, and defines a network for communication between them. The app service runs a Node.js application using a custom Dockerfile, while the database service uses a PostgreSQL container. The services are configured to restart always, and the necessary ports and network connections are established for seamless communication. Variable substitution allows for flexibility and customization based on specific deployment requirements.

.circleci/e2e-test.env file

NETWORK_NAME='app_test'
TESTING_PROJECT_NAME='app_e2e_test'
POSTGRES_USER=postgres_test
POSTGRES_PASSWORD=test_pw
POSTGRES_DATABASE=demo1_test
POSTGRES_PORT=5432
DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:${POSTGRES_PORT}/${POSTGRES_DATABASE}?schema=public"
APP_PORT=3000

.circleci/jest-e2e.json file

{
  "moduleFileExtensions": [
    "js",
    "json",
    "ts"
  ],
  "rootDir": "../test",
  "testEnvironment": "node",
  "testRegex": ".e2e-spec.ts$",
  "transform": {
    "^.+\\.(t|j)s$": [
      "ts-jest",
      {
        "astTransformers": {
          "before": [
            "test/transformer.js"
          ]
        }
      }
    ]
  },
  "moduleNameMapper": {
    "^src/(.*)": "<rootDir>/../src/$1"
  },
  "reporters": [
    "default",
    "jest-junit"
  ]
}

.circleci/test.env file

NETWORK_NAME='app_test'
TESTING_PROJECT_NAME='app_e2e_test'
POSTGRES_USER=postgres_test
POSTGRES_PASSWORD=test_pw
POSTGRES_DATABASE=demo1_test
POSTGRES_PORT=5432
DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@database:${POSTGRES_PORT}/${POSTGRES_DATABASE}?schema=public"
APP_PORT=3000

OK!. Lets move to deploy.

First things first. You need to provide your EC2 instance private key (.pem file) to CircleCi project. Need to store instance domain (host), login username (ubuntu) in CircleCi environment variables. (SSH_USER_PROD,SSH_HOST_PROD)

CircleCI config for deployment

version: 2.1

parameters:
  remote_workingdir:
    type: string
    default: "my_nest_js_prod_app"
  archive_name:
    type: string
    default: "build.tar"


jobs:
  deploy_prod:
    machine:
      image: ubuntu-2004:current
      docker_layer_caching: true
    steps:
      - checkout
      - restore_cache:
          keys:
            - v1-yarn-deps-build-{{ .Branch }}-{{ checksum "yarn.lock" }}
      - run:
          name: "Building Latest Docker Image"
          command: |
            docker build -t 'demo/app-backend-api-node'  -f 'deploy/nestjsapp.deploy.Dockerfile' .
      - run:
          name: "Installing Dependencies"
          command: |
            docker run --interactive --volume $PWD:/app:rw demo/app-backend-api-node yarn install
      - run:
          name: "Generating prisma models"
          command: |
            docker run --interactive --volume $PWD:/app:rw demo/app-backend-api-node yarn prisma generate
      - save_cache:
          key: v1-yarn-deps-build-{{ .Branch }}-{{ checksum "yarn.lock" }}
          paths:
            - node_modules
      - run:
          name: "Building App"
          command: |
            docker run --interactive --volume $PWD:/app:rw demo/app-backend-api-node yarn build

      - run:
          name: "Archiving Build"
          command: |
            tar cvf << pipeline.parameters.archive_name >> deploy/
            tar rvf << pipeline.parameters.archive_name >> dist/
            tar rvf << pipeline.parameters.archive_name >> node_modules/
            tar rvf << pipeline.parameters.archive_name >> prisma/
            tar rvf << pipeline.parameters.archive_name >> nest-cli.json
            tar rvf << pipeline.parameters.archive_name >> package.json
            tar rvf << pipeline.parameters.archive_name >> tsconfig.build.json
            tar rvf << pipeline.parameters.archive_name >> tsconfig.json
            tar rvf << pipeline.parameters.archive_name >> yarn.lock

      - run:
          name: "Copying Build To Remote Server"
          command: |
            scp << pipeline.parameters.archive_name >> $SSH_USER_PROD@$SSH_HOST_PROD:<< pipeline.parameters.remote_workingdir >>

      - run:
          name: "Unzipping Build"
          command: |
            ssh $SSH_USER_PROD@$SSH_HOST_PROD "cd << pipeline.parameters.remote_workingdir >> && tar xvf << pipeline.parameters.archive_name >>"

      - run:
          name: "Creating Symlink for .env"
          command: |
            ssh $SSH_USER_PROD@$SSH_HOST_PROD "cd << pipeline.parameters.remote_workingdir >> && ln -sf ../.env deploy"

      - run:
          name: "Starting Containers"
          command: |
            ssh $SSH_USER_PROD@$SSH_HOST_PROD "cd << pipeline.parameters.remote_workingdir >> && sh deploy/start_containers.sh"

      - run:
          name: "Migrating Database"
          command: |
            ssh $SSH_USER_PROD@$SSH_HOST_PROD "cd << pipeline.parameters.remote_workingdir >> && sh deploy/migrate_db.sh"

      - run:
          name: "Rebooting App Container"
          command: |
            ssh $SSH_USER_PROD@$SSH_HOST_PROD "cd << pipeline.parameters.remote_workingdir >> && sh deploy/reboot_app.sh"

      - run:
          name: "Removing The Archive"
          command: |
            ssh $SSH_USER_PROD@$SSH_HOST_PROD "cd << pipeline.parameters.remote_workingdir >> && rm << pipeline.parameters.archive_name >>"

workflows:
  production:
    jobs:
      - deploy_prod:
          requires:
            - unit_test
            - e2e_test
          filters:
            branches:
              only:
                - main

Parameters Section:

  1. remote_workingdir:

    • Type: string

    • Default: "my_nest_js_prod_app"

    • Represents the remote working directory where the application will be deployed.

  2. archive_name:

    • Type: string

    • Default: "build.tar"

    • Specifies the name of the archive file that will be created for deployment.

Jobs Section:

deploy_prod Job:

  1. Machine Setup:

    • Uses an Ubuntu 20.04 image for building Docker containers.

    • Enables Docker layer caching to optimize dependency management.

  2. Checkout and Caching:

    • Checks out the code from the repository.

    • Restores cached dependencies based on the yarn.lock file.

  3. Docker Image Build:

    • Builds a Docker image tagged as 'demo/app-backend-api-node' using the specified Dockerfile (deploy/nestjsapp.deploy.Dockerfile).
  4. Dependency Installation:

    • Installs project dependencies within the Docker container using docker run.
  5. Prisma Models Generation:

    • Generates Prisma models within the Docker container.
  6. Dependency Caching:

    • Saves the cache of dependencies for future builds.
  7. Build Application:

    • Builds the application using the command yarn build within the Docker container.
  8. Archiving Build:

    • Archives the build files, including dist/, node_modules/, and configuration files, into a tar file specified by the parameter archive_name.
  9. Copy Build to Remote Server:

    • Uses scp to copy the archive to the specified remote working directory on the production server.
  10. Unzipping Build:

  • Connects to the production server via SSH and extracts the archive.
  1. Creating Symlink for .env:
  • Creates a symbolic link for the .env file within the deployed directory.
  1. Starting Containers:
  • Executes a script (start_containers.sh) on the production server to start Docker containers.
  1. Migrating Database:
  • Executes a script (migrate_db.sh) on the production server to perform database migrations.
  1. Rebooting App Container:
  • Executes a script (reboot_app.sh) on the production server to reboot the application container.
  1. Removing The Archive:
  • Removes the archive file from the production server to clean up after deployment.

Workflows Section:

production Workflow:

  1. Job Dependencies:

    • The deploy_prod job is triggered after the successful completion of both unit_test and e2e_test jobs.
  2. Branch Filtering:

    • The workflow is only triggered for the main branch.

This CircleCI configuration file defines a job (deploy_prod) responsible for deploying a Nest.js application to a production server. It encompasses steps such as building Docker images, installing dependencies, archiving the build, copying files to a remote server, and executing deployment scripts. The workflow ensures deployment only on the main branch after passing both unit and end-to-end tests. Slack notifications are incorporated to communicate the deployment status. The use of parameters enhances flexibility and customization in the deployment process.

deploy/docker-compose-deploy.yaml file

version: '3.8'

services:
  app:
    build:
      context: ./
      dockerfile: nestjsapp.deploy.Dockerfile
    volumes:
      - ../:/app:rw
      - /var/log/:/app/logs/:rw
    restart: always
    ports:
      - '${APP_PORT}:${APP_PORT}'
    depends_on:
      - database
    networks:
      - default
    env_file:
      - .env
    environment:
      - NODE_ENV=${NODE_ENV}
    command: ['node', 'dist/src/main.js']
    healthcheck:
      test: ['CMD', 'curl', '-f', 'http://localhost:${APP_PORT}/health']
      interval: 1m30s
      timeout: 10s
      retries: 3
      start_period: 60s

  database:
    image: postgres:15.2-alpine
    restart: always
    ports:
      - '5432:5432'
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DATABASE}
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test:
        [
          'CMD-SHELL',
          "sh -c 'pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DATABASE}'",
        ]
      interval: 1m30s
      timeout: 30s
      retries: 5
      start_period: 30s
    networks:
      - default

  nginx:
    image: nginx:1.25.2
    ports:
      - 443:443
    depends_on:
      app:
        condition: service_started
    healthcheck:
      test: ['CMD', 'service', 'nginx', 'status']
      interval: 1m30s
      timeout: 30s
      retries: 5
      start_period: 30s
    environment:
      - APP_PORT
      - DOMAIN
    volumes:
      - ./default.conf.template:/etc/nginx/templates/default.conf.template:ro
      - '/var/log/nginx:/var/log/nginx'
      - '/etc/ssl:/etc/ssl:ro'
    networks:
      - default
    restart: always

networks:
  default:
    name: ${NETWORK_NAME}

volumes:
  pgdata:
    external: true

Services Section:

app Service:

  1. Build Configuration:

    • Builds the service from the current context using the Dockerfile nestjsapp.deploy.Dockerfile.
  2. Volume Mounting:

    • Mounts the parent directory (../) to /app inside the container with read and write permissions.

    • Mounts the host's /var/log/ to /app/logs/ inside the container for log storage.

  3. Container Restart Policy:

    • Configures the service to restart always.
  4. Port Mapping:

    • Maps the host's ${APP_PORT} to the container's ${APP_PORT} for accessing the application.
  5. Dependency on database Service:

    • Specifies that the app service depends on the database service, ensuring the database container is started before the application container.
  6. Network Configuration:

    • Assigns the service to the default Docker network (default).
  7. Environment Variables:

    • Loads environment variables from the .env file.

    • Sets the NODE_ENV environment variable.

  8. Command Configuration:

    • Runs the application using the command ['node', 'dist/src/main.js'].
  9. Healthcheck Configuration:

    • Defines a healthcheck to ensure the service is healthy using a curl command to check the /health endpoint.

database Service:

  1. Image Configuration:

    • Pulls the postgres:15.2-alpine image from Docker Hub.
  2. Container Restart Policy:

    • Configures the service to restart always.
  3. Port Mapping:

    • Maps the host's 5432 to the container's 5432 for accessing the PostgreSQL database.
  4. Environment Variables:

    • Sets environment variables for PostgreSQL user, password, and database name.
  5. Volume Configuration:

    • Mounts a named volume (pgdata) to /var/lib/postgresql/data for persistent storage.
  6. Healthcheck Configuration:

    • Defines a healthcheck using the pg_isready command to check the availability of the PostgreSQL database.

nginx Service:

  1. Image Configuration:

    • Pulls the nginx:1.25.2 image from Docker Hub.
  2. Port Mapping:

    • Maps the host's 443 to the container's 443 for accessing the Nginx service.
  3. Dependency on app Service:

    • Specifies that the nginx service depends on the app service and will start once the app service is started.
  4. Healthcheck Configuration:

    • Defines a healthcheck by checking the status of the Nginx service using the service nginx status command.
  5. Environment Variables:

    • Passes APP_PORT and DOMAIN environment variables to the Nginx service.
  6. Volume Configuration:

    • Mounts the default.conf.template file as a read-only template for Nginx configuration.

    • Mounts host directories for Nginx logs and SSL certificates.

Networks Section:

  • Defines a Docker network named ${NETWORK_NAME} (variable substitution) and assigns it as the default network for the services.

Volumes Section:

  • Defines an external volume named pgdata, which is used for persisting PostgreSQL data.

This Docker Compose file sets up three services (app, database, and nginx) with defined configurations for each. It incorporates health checks to ensure the services' availability and dependencies between services. The use of named volumes and environment variable substitution enhances configurability and maintainability. The overall setup is geared towards deploying a Nest.js application with a PostgreSQL database and Nginx as a reverse proxy.

deploy/default.conf.template file

include mime.types;
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name $DOMAIN; 
    error_log /var/log/nginx/error.log;
    access_log /var/log/nginx/access.log;
    charset utf-8;
    location / {
        proxy_pass http://app:$APP_PORT;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_cache_bypass $http_upgrade;
    }
    ssl_certificate /etc/ssl/$DOMAIN/certificate.crt;
    ssl_certificate_key /etc/ssl/$DOMAIN/key.key;
}
server {
    listen 80;
    listen [::]:80;
    server_name $DOMAIN;
    return 302 https://$server_name$request_uri;
}

SSL Configuration (HTTPS):

  1. Listen on Port 443 (HTTPS):

    • listen 443 ssl; and listen [::]:443 ssl; specify that NGINX should listen on port 443 for secure SSL connections.
  2. Server Name and Logging:

    • server_name $DOMAIN; defines the server name using the $DOMAIN variable.

    • error_log and access_log directives set the paths for error and access logs.

  3. Character Set:

    • charset utf-8; declares the character set for encoding.
  4. Proxy Configuration:

    • location / {...} block configures the reverse proxy settings.

      • proxy_pass http://app:$APP_PORT; specifies the backend server's address and port.

      • proxy_http_version 1.1; sets the HTTP version for proxy connections.

      • proxy_set_header directives configure headers for the proxy request.

      • proxy_cache_bypass $http_upgrade; ensures WebSocket connections work correctly.

  5. SSL Certificate and Key:

    • ssl_certificate and ssl_certificate_key directives specify the paths to the SSL certificate and private key, respectively. The paths are based on the $DOMAIN variable.

HTTP to HTTPS Redirect Configuration:

  1. Listen on Port 80 (HTTP):

    • listen 80; and listen [::]:80; specify that NGINX should listen on port 80 for regular HTTP connections.
  2. Server Name and Redirect:

    • server_name $DOMAIN; defines the server name for the HTTP block.

    • return 302 https://$server_name$request_uri; returns a temporary (302) redirect to the equivalent HTTPS URL.

Variable Usage:

  • The usage of variables like $DOMAIN and $APP_PORT allows for dynamic configuration based on the values provided in the environment.

deploy/nestjsapp.deploy.Dockerfile file

FROM node:18.17.1-alpine3.18

LABEL 'maintainer'='Demo/Application'

WORKDIR /app

ENV TZ=UTC

RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

#update the system
RUN apk update


#install nestjs cli
RUN yarn global add @nestjs/cli@10.1.12

Base Image:

  • FROM node:18.17.1-alpine3.18: Specifies the base image for the Docker image, using Node.js version 18.17.1 on Alpine Linux version 3.18.

Metadata:

  • LABEL 'maintainer'='Demo/Application': Adds a metadata label to the image, specifying the maintainer of the Dockerfile.

Working Directory:

  • WORKDIR /app: Sets the working directory inside the container to /app. This will be the default directory for subsequent commands.

Timezone Configuration:

  • ENV TZ=UTC: Sets the TZ environment variable to UTC, which is then used for configuring the timezone.

  • RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone: Configures the timezone in the container based on the value of the TZ environment variable.

System Update:

  • RUN apk update: Updates the package index of the Alpine Linux distribution. This step ensures that the package manager (apk) has the latest information about available packages.

NestJS CLI Installation:

  • RUN yarn global add @nestjs/cli@10.1.12: Installs the NestJS Command Line Interface (CLI) globally using the Yarn package manager. The specified version is 10.1.12.

deploy/migrate_db.sh file

docker compose -f 'deploy/docker-compose-deploy.yaml' -p 'app_deploy' run app yarn prisma migrate deploy

deploy/reboot_app.sh file

docker compose -f 'deploy/docker-compose-deploy.yaml' -p 'app_deploy' restart app

deploy/start_containers.sh file

docker compose -f 'deploy/docker-compose-deploy.yaml' -p 'app_deploy' up -d

Finally, the .env file (in the root of the deployment folder)

NETWORK_NAME='nestjs'
POSTGRES_USER=postgres
POSTGRES_PASSWORD=test_pw
POSTGRES_DATABASE=demo1
DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DATABASE}?schema=public"
APP_PORT=3000
DOMAIN="your_domain.com"

Summary
This blog post introduces a comprehensive guide to streamline the deployment of NestJS backends through a Continuous Integration/Continuous Deployment (CI/CD) pipeline, utilizing CircleCI and AWS EC2. The significance of CI/CD is emphasized, providing developers with a seamless and automated process for testing, integrating, and deploying code changes.

The post highlights the importance of integration testing, covering both unit and end-to-end tests in the CI/CD pipeline. It stresses the benefits of early bug detection, enhanced code maintainability, and confidence in the application's functionality and stability.

Key components of the guide include the project structure, EC2 instance configuration, and server setup with Docker. The deployment workflow is explained, focusing on Docker image creation, dependency management, and efficient caching. Integration testing is facilitated through Docker containers, optimizing the testing process.

Docker Compose configurations are dissected, emphasizing the services for the NestJS app, PostgreSQL database, and Nginx reverse proxy. The Nginx configuration file is provided to illustrate SSL setup, reverse proxy configurations, and HTTP to HTTPS redirection.

The guide concludes by presenting deployment scripts and essential environment variables, facilitating a successful deployment process. Overall, the blog post equips developers with insights into the significance of CI/CD, showcasing its role in ensuring code quality, reliability, and a streamlined deployment experience for NestJS applications.