Skip to content

Docker Containerization

alfonsodelr edited this page May 4, 2024 · 7 revisions

Getting Started

Development

  • Before running the development script, place a .env into each microservice folder with the information included in #resources in Discord.
  1. npm run installs
  2. npm run docker-up
  3. Start respective servers you are working with

Production

  • Before running the production script, place a .env into the ROOT directory with the information included in #resources in Discord.
  1. npm run installs
  2. npm run containerized

Port Associations:


PUBLIC

  • 3000 - Shopper App
  • 3001 - Admin App
  • 3002 - Vendor API
  • 3003 - Vendor App

  • 3010 - Auth Service
  • 3011 - Product Service
  • 3012 - Orders Service
  • 3013 - Key Service
  • 3014 - Account Service

  • 5432 - Auth Service Development postgres instance
  • 5433 - Orders Service Development postgres instance
  • 5434 - Product Service Development postgres instance
  • 5435 - Key Service Development postgres instance
  • 5436 - Account Service Development postgres instance

Detailed Overview - Development

Package.json

When running docker-up, the following commands are executed under package.json:

"docker-up": "npm run docker-auth-up && npm run docker-order-up && npm run docker-product-up",
"docker-auth-up": "cd AuthService && docker-compose up -d",
"docker-product-up": "cd ProductService && docker-compose up -d",
"docker-order-up": "cd OrderService && docker-compose up -d",
"docker-down": "npm run docker-auth-down && npm run docker-order-down && npm run docker-product-down",
"docker-auth-down": "cd AuthService && docker-compose down",
"docker-product-down": "cd ProductService && docker-compose down",
"docker-order-down": "cd OrderService && docker-compose down",
"installs": "npm install && npm run install-admin && npm run install-shopper && npm run install-vendor-app && npm run install-vendor-api && npm run install-auth && npm run install-order && npm run install-product",

As you can see, running docker-up daisy chains into the commands directly underneath. To explain what it does in english, running this command will cd into every microservice directory and run docker-compose up -d. This command will trigger the docker-compose.yml file inside of each microservice

Docker-compose.yml

version: '3.7'

services:
  postgres:
    container_name: cse187-mockazon
    image: postgres
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    ports:
      - "5432:5432"
    volumes:
      - ./sql/databases.sql:/docker-entrypoint-initdb.d/1.databases.sql
      - ./sql/schema.sql:/docker-entrypoint-initdb.d/2.schema.sql
      - ./sql/test.sql:/docker-entrypoint-initdb.d/3.test.sql
      - ./sql/data.sql:/docker-entrypoint-initdb.d/4.data.sql

This file tells docker to spin up a database at port 5432 and populates that database with the files inside of /sql

IMPORTANT

Every microservice has a unique port it needs to map for each of its databases. In this example, we are mapping 5432:5432. We can't however have two databases mapped to the same port, so in other microservices we do 5433:5432, 5434:5432, etc. To explain the porting, the first part of the port is the incoming port and the last part is the port within the container it maps to. You don't need to know the full details, as long as you have a unique incoming port number for every microservices development database. You can check the ports and their associated services below.

What you see

image As you can see, each microservice now has its own database to work with, similar to how you are used to in previous assignments! From here you can just spin up the respective servers you are working with and go from there!

Detailed Overview - Production

Before running the containerized command, make sure you run npm run installs

Package.json

When running npm run containerized, the following things occur:

"containerized": "npm run build && docker-compose up --build",
"build": "npm run build-auth && npm run build-products && npm run build-orders && npm run build-admin && npm run build-vendorapp && npm run build-shopper && npm run build-vendorapi",
"build-auth": "cd AuthService && npm run build",
"build-products": "cd ProductService && npm run build",
"build-orders": "cd OrderService && npm run build",
"build-admin": "cd Admin && npm run build",
"build-vendorapp": "cd VendorApp && npm run build",
"build-vendorapi": "cd VendorAPI && npm run build",
"build-shopper": "cd Shopper && npm run build",

This set of commands just cd's into every directory and runs npm run build on them to generate a build folder. This build folder is crucial for creating the production container. It then runs docker-compose up --build on the root directory docker-compose file

Docker-compose.yml

version: '3.7'
services:

  shopper:
    container_name: shopper
    env_file: .env
    build:
      context: ./Shopper
      dockerfile: Dockerfile
    depends_on:
      - postgres
    ports:
      - "3000:3000"

  admin:
    container_name: admin
    env_file: .env
    build:
      context: ./Admin
      dockerfile: Dockerfile
    depends_on:
      - postgres
    ports:
      - "3001:3001"

  vendor_api:
    container_name: vendor_api
    env_file: .env
    build:
      context: ./VendorAPI
      dockerfile: Dockerfile
    depends_on:
      - postgres
    ports:
      - "3002:3002"

  vendor_app:
    container_name: vendor_app
    env_file: .env
    build:
      context: ./VendorApp
      dockerfile: Dockerfile
    depends_on:
      - postgres
    ports:
      - "3003:3003"

  microservice:
    container_name: microservice
    env_file: .env
    build:
      context: .
      dockerfile: Microservice.Dockerfile
    depends_on:
      - postgres
  
  postgres:
    container_name: database
    image: postgres:alpine
    environment:
      POSTGRES_PORT: ${POSTGRES_PORT}
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - ./sql/databases.sql:/docker-entrypoint-initdb.d/1.databases.sql
      - ./AuthService/sql/schema.sql:/docker-entrypoint-initdb.d/2.schema.sql
      - ./AuthService/sql/data.sql:/docker-entrypoint-initdb.d/3.data.sql
      - ./ProductService/sql/schema.sql:/docker-entrypoint-initdb.d/4.schema.sql
      - ./ProductService/sql/data.sql:/docker-entrypoint-initdb.d/5.data.sql
      - ./OrderService/sql/schema.sql:/docker-entrypoint-initdb.d/6.schema.sql
      - ./OrderService/sql/data.sql:/docker-entrypoint-initdb.d/7.data.sql

This might seem horrifying at first, but don't worry! Its actually quite easy to track and we will walk through it together. Each of these "services" are their own little containers that the compose file creates. These containers have these field:

  1. conatiner name
    • name of the container
  2. env_file
    • environment variables included (from the root)
  3. build
    • Specifications for building the container
    • search in the "context" directory for a dockerfile named Dockerfile
  4. depends on
    • Before this container is built, build the postgres container
  5. ports:
    • ports exposed to the public. Notice the ports that are exposed are ONLY for the public apps and not the microservices

Cool! Now lets take a look at one of the dockerfiles this yaml builds from

Dockerfile

Here we will talk about the microservice dockerfile at the root level directory as it is the most complex and the other dockerfiles follow suit

FROM node:20-alpine

WORKDIR /home/app

COPY package.json /home/app/
COPY package-lock.json /home/app/
COPY .env /home/app/

COPY AuthService/build/ /home/app/AuthService/build/
COPY AuthService/package.json /home/app/AuthService/
COPY AuthService/package-lock.json /home/app/AuthService

COPY ProductService/build/ /home/app/ProductService/build/
COPY ProductService/package.json /home/app/ProductService/
COPY ProductService/package-lock.json /home/app/ProductService

COPY OrderService/build/ /home/app/OrderService/build/
COPY OrderService/package.json /home/app/OrderService/
COPY OrderService/package-lock.json /home/app/OrderService

RUN npm run cis

CMD npm run start

You can think of this dockerfile as a set of instruction to create the operating system the microservices will live in.

FROM node:20-alpine

Specifies to install node in this container (Also includes a lightweight version of Ubuntu ~5mb)

WORKDIR /home/app

Change the working directory WITHIN THE CONTAINER

COPY package.json /home/app/
COPY package-lock.json /home/app/
COPY .env /home/app/

Copy the package.json, package-lock.json, and .env from the repository to the container

COPY AuthService/build/ /home/app/AuthService/build/
COPY AuthService/package.json /home/app/AuthService/
COPY AuthService/package-lock.json /home/app/AuthService

Copy the microservices build folder along with the package.json and package-lock.json into the container

RUN npm run cis

This runs npm run cis INSIDE of the docker container. Because we have just copied the package.json file into this container, this command will work. This command is basically a lighter weight version of npm install that uses the package-lock.json files.

CMD npm run start

This starts every microservice inside of the container. Below is the exact functionality of npm run cis AND npm run start

"cis": "npm ci && npm run ci-auth && npm run ci-products && npm run ci-orders",
"ci-auth": "cd AuthService && npm ci",
"ci-products": "cd ProductService && npm ci",
"ci-orders": "cd OrderService && npm ci",
"start": "concurrently --kill-others \"npm run start-auth\" \"npm run start-products\" \"npm run start-orders\"",
"start-auth": "cd AuthService && npm start",
"start-products": "cd ProductService && npm start",
 "start-orders": "cd OrderService && npm start"

Once you understand this dockerfile, the rest should be easy to follow. Notice some dockerfiles have

EXPOSE 3000

This is just a simple way to tell the container that port 3000 will be exposed to the public.

Postgres

In the production environment, instead of creating a new instance of postgres within each container we have ONE instance of postgres that spawns a database for every microservice. The compose file then runs the data and schema sql files inside of each microservice in the instance. This looks like the following:

  postgres:
    container_name: database
    image: postgres:alpine
    environment:
      POSTGRES_PORT: ${POSTGRES_PORT}
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - ./sql/databases.sql:/docker-entrypoint-initdb.d/1.databases.sql
      - ./AuthService/sql/schema.sql:/docker-entrypoint-initdb.d/2.schema.sql
      - ./AuthService/sql/data.sql:/docker-entrypoint-initdb.d/3.data.sql
      - ./ProductService/sql/schema.sql:/docker-entrypoint-initdb.d/4.schema.sql
      - ./ProductService/sql/data.sql:/docker-entrypoint-initdb.d/5.data.sql
      - ./OrderService/sql/schema.sql:/docker-entrypoint-initdb.d/6.schema.sql
      - ./OrderService/sql/data.sql:/docker-entrypoint-initdb.d/7.data.sql

I trust you to be able to follow this yourself.

What you see

image

Fetch URL's

Due to the differences between production and development environments, the URL's for calling a microservices will change very slightly. This is how the fetch url will be structured:

http://${process.env.MICROSERVICE_URL||'localhost'}:3010/api/v0/authenticate
MICROSERVICE_URL=microservice

This is our microservice url, the above url essentials equates to

http://localhost:3010/api/v0/authenticate

in development and

http://microservice:3010/api/v0/authenticate

in production. This is necessary because we cannot access the microservice via localhost in production because they are separate containers.

Database URL's

Along with fetch urls, database pools are set up as follows:

import { Pool } from 'pg';

const pool = new Pool({
  host: (process.env.POSTGRES_HOST || 'localhost'),
  port: + (process.env.POSTGRES_PORT || 5434),
  database: process.env.POSTGRES_DB,
  user: process.env.POSTGRES_USER,
  password: process.env.POSTGRES_PASSWORD,
});

export { pool };

This is for the same reason as fetch URL's

Creating a new Microservice

Creating a new microservice can seem really daunting at first as there are a few steps involved but don't worry, its not quite as complicated as it sounds. These kind of things seem really scary but once you get the hang of it you will think "huh oh that wasn't so bad" and feel amazing about yourself!

To create a new microservice, first just create the directory and the structure of the service. The first thing you want to worry about is setting it up so it works in development, and that's incredibly easy!

  1. Create a docker-compose.yml file in the root of the microservice and copy+paste one from another microservice. Then pick a port from the first numbers in "5432:5432" to something that is not being used by another microservice.

Inside of the root package.json, add this to the set of commands in

"docker-up": "npm run docker-auth-up && npm run docker-order-up && npm run docker-product-up",
"docker-auth-up": "cd AuthService && docker-compose up -d"

thats it for development!

For production, modify the existing Microservice.Dockerfile in the root directory and copy in the build folder, package.json, and package-lock.json like so

COPY AuthService/build/ /home/app/AuthService/build/
COPY AuthService/package.json /home/app/AuthService/
COPY AuthService/package-lock.json /home/app/AuthService

Once that is done, list the port of the microservice at the top of this wiki. Then add the microservice to the root package.json under the "cis" and "start" commands.

Clone this wiki locally