Dockerizing a Full-stack TypeScript Application.

How to Dockerize a full-stack application Next, Nest with PostgreSQL

Dockerizing a Full-stack TypeScript Application.

Dockerization stands out as a game-changer in the current dynamic software landscape. Docker's lightweight, portable containers have revolutionized application development and deployment. With Docker, you can encapsulate applications and their dependencies, ensuring uniformity across diverse environments and simplifying deployment.

In this blog, we explore Dockerizing a full-stack application, incorporating Nest.js for the backend and Next.js for the front end. Learn how to structure your project, create Dockerfiles for each component, and manage them using Docker Compose.

Docker's containerization is distinct from traditional virtualization. While virtualization emulates full operating systems, containers share a common OS, enabling greater efficiency and faster startup times. In simpler terms:

while virtualization and virtual machine are virtualizing hardware, docker virtualizes the operating system.

NetworkChuck, one of my favorite YouTubers

This efficiency makes Docker ideal for microservices and continuous integration pipelines.

Below is the directory structure of our application

my-project/
├── docker-compose.yml
├── .dockerignore
├── web/
│   └── ...
└── api/
    └── ...

web/:- This directory contains a Next.js application

api/:- This directory contains a Nest.js application

Dockerizing the Next.js application

The provided Dockerfile below utilizes a multi-stage approach for containerizing our Next.js application.

# Stage 1: Installing Dependencies
FROM node:18-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /usr/src/app

COPY package.json package-lock.json ./  
RUN npm install --production

# Stage 2: Building the Application
FROM node:18-alpine AS builder
WORKDIR /usr/src/app
COPY --from=deps /usr/src/app/node_modules ./node_modules
COPY . .

ENV NEXT_TELEMETRY_DISABLED 1 # disable nextjs telemetry to speed up build
RUN npm run build

# Stage 3: Running the Application
FROM node:18-alpine AS runner
WORKDIR /usr/src/app

ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder --chown=nextjs:nodejs /usr/src/app/.next ./.next
COPY --from=builder /usr/src/app/node_modules ./node_modules
COPY --from=builder /usr/src/app/package.json ./package.json

USER nextjs

# for documentation purposes
EXPOSE 3000

ENV PORT 3000

CMD ["npm", "start"]

It consists of three stages:

  1. Stage 1: Installing Dependencies (deps) - It installs the required build dependencies, sets up the working directory, and installs production application dependencies.

  2. Stage 2: Building the Application (builder) - This stage copies the application source code, along with dependencies from the previous stage, and builds the application. It also disables Next.js telemetry.

  3. Stage 3: Running the Application (runner) - The final image is created for running the application. It sets up environment variables, creates a non-root user for added security, copies build artifacts, exposes a port, and specifies the command to start the application.

By breaking the process into stages, the Dockerfile optimizes image size, build efficiency, and security, following best practices for Node.js application containerization.

Dockerizing the Nest.js application

The simple Dockerfile below can be used to containerize our Nest.js api:

FROM node:18-alpine

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm install

COPY . .

RUN npm run build

CMD ["npm", "run", "start:dev"]
  1. Base Image: The Dockerfile starts with the node:18-alpine as the base image. This image provides a lightweight Node.js environment based on Alpine Linux.

  2. Working Directory: The working directory inside the container is set to /usr/src/app. This is where all the subsequent actions will take place.

  3. Dependency Installation: The Dockerfile copies the application's package.json and package-lock.json into the container. Then, it runs npm install to install the necessary Node.js dependencies required for the application.

  4. Application Code: The application code is copied into the container. This step includes all the files and directories from the host machine's current directory.

  5. Building the Application: Assuming there is a build script defined in the package.json, the Dockerfile runs npm run build to build the Node.js application.

  6. Running the Application: The Dockerfile specifies the command to start the application using npm run start:dev. This is where the application server starts running inside the container.

Docker Compose: Simplifying Container Orchestration

Docker Compose is a handy tool for managing multi-container applications. Here, we'll explore what Docker Compose files are and how they streamline container orchestration.

A Docker Compose file, in YAML format, describes the configuration of your multi-container application. It defines services, networks, volumes, and settings. Docker Compose simplifies multi-container app management. With a well-structured Compose file, you define architecture, dependencies, and networking in a version-controlled manner. It's a valuable tool for development and production environments.

Defining Services

In a Compose file, you define the services that make up your app. Each service corresponds to a container. You can specify image, environment, and port details for each service.

Linking Containers

Compose simplifies container communication. Containers can easily connect with others by service name, making interaction seamless.

Shared Volumes and Networks

Compose lets you create shared volumes for data persistence and custom networks for communication.

Running with One Command

Start your app with a single command:

docker-compose up

Environment Variables and Secrets

Compose supports environment variables and secrets, ensuring secure configuration management.

Below is a docker-compose that has our web and api as services, in addition to that it has a PostgreSQL database and a PGAdmin web interface for managing the data.

version: "3.8"
services:
  web:
    container_name: craze_code_web
    build:
      context: ./web
      target: runner
    volumes:
      - ./web:/usr/src/app
    command: npm run dev
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: development

  db:
    image: postgres:15-alpine
    restart: always
    environment:
      - POSTGRES_PASSWORD=postgres
    container_name: postgres
    volumes:
      - ./pgdata:/var/lib/postgresql/data
    ports:
      - "5432:5432"

  api:
    build:
      context: ./api
      dockerfile: Dockerfile
    container_name: craze_code_api
    environment:
      - NODE_ENV=development
    ports:
      - "3030:3030"
    depends_on:
      - db
    volumes:
      - ./api:/usr/src/app

  pgadmin:
    image: dpage/pgadmin4
    restart: always
    container_name: pgadmin
    environment:
      - PGADMIN_DEFAULT_EMAIL=<pg_admin email>
      - PGADMIN_DEFAULT_PASSWORD=<pg_admin password>
    ports:
      - "5050:80"
    depends_on:
      - db