Dockerizing a Full-stack TypeScript Application.
How to Dockerize a full-stack application Next, Nest with PostgreSQL
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:
Stage 1: Installing Dependencies (deps) - It installs the required build dependencies, sets up the working directory, and installs production application dependencies.
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.
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"]
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.Working Directory: The working directory inside the container is set to
/usr/src/app
. This is where all the subsequent actions will take place.Dependency Installation: The Dockerfile copies the application's
package.json
andpackage-lock.json
into the container. Then, it runsnpm install
to install the necessary Node.js dependencies required for the application.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.
Building the Application: Assuming there is a build script defined in the
package.json
, the Dockerfile runsnpm run build
to build the Node.js application.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