As I studied an open-source codebase with intent to contribute, I used ChatGPT as a note-taking tool. This is my side of one of our conversations.


can you explain the reasoning for how linters, formatters, and other dependencies are distributed across the following three package.json files? not sure why eslint appears in both root and frontend. also, do the dependencies / devDependencies here abide by the sorting system we’ve discussed before?

root:

{ 
"name": "codebase",
"version": "0.1.0",
"repository": "git@github.com:jaredjj3/code-base.git",
"private": true,
"scripts": {
    "test:api": "cd api && yarn test",
    "test:web": "cd web && yarn test",
    "dev:web": "cd web && yarn start",
    "dev:api": "cd api && yarn dev:api",
    "dev:worker": "cd api && yarn dev:worker",
    "dev:dispatcher": "cd api && yarn dev:dispatcher",
    "migrate": "cd api && yarn migrate",
    "seed": "cd api && yarn seed",
    "prod:api": "cd api && yarn prod:api",
    "prod:worker": "cd api && yarn prod:worker",
    "prod:dispatcher": "cd api && yarn prod:dispatcher"
},
"devDependencies": {
    "@types/node": "^14.14.30",
    "@types/node-cleanup": "^2.1.1",
    "@typescript-eslint/eslint-plugin": "5.30.4",
    "@typescript-eslint/parser": "5.30.4",
    "babel-eslint": "10.x",
    "chalk": "^4.1.0",
    "eslint": "^7.11.0",
    "eslint-config-prettier": "^6.13.0",
    "eslint-config-react-app": "^5.1.0",
    "eslint-loader": "^4.0.2",
    "eslint-plugin-flowtype": "3.13.0",
    "eslint-plugin-import": "^2.20.2",
    "eslint-plugin-jest-dom": "^1.3.0",
    "eslint-plugin-jsx-a11y": "6.2.3",
    "eslint-plugin-prettier": "^3.1.1",
    "eslint-plugin-react": "7.16.0",
    "eslint-plugin-react-hooks": "^1.6.1",
    "eslint-plugin-testing-library": "^1.3.4",
    "execa": "^5.1.1",
    "gulp": "^4.0.2",
    "node-cleanup": "^2.1.2",
    "prettier": "2.7.1",
    "prettier-plugin-organize-imports": "^1.1.1",
    "ts-node": "^10.0.0",
    "typescript": "^4.1.5"
},
"dependencies": {
    "@types/uuid": "^8.3.1"
}
}

frontend:

{
"name": "web",
"version": "1.4.3",
"private": true,
"dependencies": {
    "@ant-design/icons": "4.5.0",
    "@craco/craco": "6.4.1",
    "@graphql-codegen/cli": "1.21.2",
    "@graphql-codegen/typescript": "1.21.1",
    "@moonwave99/fretboard.js": "0.2.7",
    "@reduxjs/toolkit": "1.5.0",
    "@codebase/musicxml": "0.2.6",
    "@testing-library/dom": "8.13.0",
    "@testing-library/jest-dom": "5.16.4",
    "@testing-library/react": "13.3.0",
    "@testing-library/user-event": "14.2.0",
    "@tonaljs/tonal": "4.6.5",
    "@types/extract-files": "8.1.0",
    "@types/graphql": "14.5.0",
    "@types/jest": "26.0.20",
    "@types/jest-when": "2.7.2",
    "@types/jquery": "3.5.6",
    "@types/lodash": "4.14.168",
    "@types/node": "12.0.0",
    "@types/react": "18.0.9",
    "@types/react-dom": "18.0.4",
    "@types/react-router-dom": "5.3.3",
    "@types/react-scroll": "1.8.2",
    "@types/react-transition-group": "4.4.1",
    "@types/styled-components": "5.1.25",
    "@types/uuid": "8.3.1",
    "@types/video.js": "7.3.15",
    "@xstate/react": "1.5.1",
    "antd": "4.22.4",
    "babel-eslint": "10.1.0",
    "babel-plugin-import": "1.13.3",
    "craco-less": "1.17.1",
    "css-loader": "5.1.1",
    "customize-cra": "1.0.0",
    "dayjs": "1.10.4",
    "detect-it": "4.0.1",
    "extract-files": "9.0.0",
    "fast-deep-equal": "3.1.3",
    "graphql": "15.5.0",
    "intersection-observer": "0.12.0",
    "jest-environment-jsdom-sixteen": "1.0.3",
    "jest-extended": "0.11.5",
    "jest-junit": "12.0.0",
    "jest-when": "3.2.1",
    "jquery": "3.6.0",
    "lodash": "4.17.21",
    "opensheetmusicdisplay": "1.5.4",
    "react": "18.1.0",
    "react-dom": "18.1.0",
    "react-is": "17.0.1",
    "react-router-dom": "6.3.0",
    "react-scripts": "4.0.3",
    "react-scroll": "1.8.1",
    "react-snap": "1.23.0",
    "resize-observer-polyfill": "1.5.1",
    "styled-components": "5.3.5",
    "ts-toolbelt": "9.6.0",
    "typed-graphqlify": "3.1.4",
    "typescript": "4.7.4",
    "uuid": "8.3.1",
    "video.js": "7.14.3",
    "videojs-contrib-quality-levels": "^2.1.0",
    "web-vitals": "1.0.1",
    "workbox-background-sync": "5.1.3",
    "workbox-broadcast-update": "5.1.3",
    "workbox-cacheable-response": "5.1.3",
    "workbox-core": "5.1.3",
    "workbox-expiration": "5.1.3",
    "workbox-google-analytics": "5.1.3",
    "workbox-navigation-preload": "5.1.3",
    "workbox-precaching": "5.1.3",
    "workbox-range-requests": "5.1.3",
    "workbox-routing": "5.1.3",
    "workbox-strategies": "5.1.3",
    "workbox-streams": "5.1.3",
    "xstate": "4.23.1"
},
"scripts": {
    "start": "craco start",
    "build": "craco build",
    "postbuild": "react-snap",
    "test": "craco test",
    "eject": "craco eject",
    "typegen": "graphql-codegen"
},
"eslintConfig": {
    "extends": [
    "react-app",
    "react-app/jest"
    ]
},
"browserslist": {
    "production": [
    ">0.2%",
    "not dead",
    "not op_mini all"
    ],
    "development": [
    "last 1 chrome version",
    "last 1 firefox version",
    "last 1 safari version"
    ]
},
"jest-junit": {
    "suiteName": "web",
    "outputName": "junit.web.xml",
    "outputDirectory": "./reports",
    "suiteNameTemplate": "{displayName}",
    "classNameTemplate": "{classname}"
},
"jest": {
    "transformIgnorePatterns": [
    "/node_modules/(?!antd|@ant-design|rc-.+?|@babel/runtime).+(js|jsx)$"
    ]
},
"reactSnap": {
    "puppeteerArgs": [
    "--no-sandbox",
    "--disable-setuid-sandbox"
    ]
}
}

backend:

{
"name": "api",
"version": "1.4.3",
"description": "GraphQL server for @codebase",
"license": "MIT",
"private": true,
"scripts": {
    "migrate": "yarn mikro-orm migration:up",
    "seed": "yarn ts-node src/db/mikro-orm/seeds/seed.ts",
    "dev:api": "yarn ts-node-dev --transpile-only src/entrypoints/api.ts",
    "dev:worker": "yarn ts-node-dev --transpile-only src/entrypoints/worker.ts",
    "dev:dispatcher": "yarn ts-node-dev --transpile-only src/entrypoints/dispatcher.ts",
    "prod:api": "node build/entrypoints/api.js",
    "prod:worker": "node build/entrypoints/worker.js",
    "prod:dispatcher": "node build/entrypoints/dispatcher.js",
    "pretest": "yarn migrate",
    "test": "yarn jest --maxWorkers=$JEST_NUM_WORKERS",
    "typegen": "graphql-codegen"
},
"dependencies": {
    "@mikro-orm/core": "^4.5.7",
    "@mikro-orm/postgresql": "^4.5.7",
    "altair-express-middleware": "^3.2.1",
    "aws-sdk": "^2.844.0",
    "bcrypt": "^5.0.0",
    "bullmq": "1.89.1",
    "class-validator": "^0.13.1",
    "cls-hooked": "^4.2.2",
    "connect-redis": "^5.1.0",
    "cors": "^2.8.5",
    "dataloader": "^2.0.0",
    "execa": "^5.1.1",
    "express": "^4.17.1",
    "express-graphql": "^0.12.0",
    "express-session": "^1.17.1",
    "extract": "^1.0.0",
    "files": "^2.1.1",
    "graphql": "^15.5.0",
    "graphql-upload": "^11.0.0",
    "inversify": "^5.0.5",
    "knex": "^0.21.17",
    "lodash": "^4.17.21",
    "morgan": "^1.10.0",
    "nodemailer": "^6.4.18",
    "pg": "^8.5.1",
    "redis": "^3.1.1",
    "reflect-metadata": "^0.1.13",
    "type-graphql": "1.1.1",
    "uuid": "^8.3.2",
    "winston": "^3.3.3"
},
"devDependencies": {
    "@aws-cdk/aws-cloudfront": "^1.113.0",
    "@graphql-codegen/cli": "^1.20.1",
    "@graphql-codegen/typescript": "^1.21.0",
    "@mikro-orm/cli": "^4.5.7",
    "@types/aws-sdk": "^2.7.0",
    "@types/bcrypt": "^3.0.0",
    "@types/bluebird": "^3.5.33",
    "@types/cls-hooked": "^4.3.3",
    "@types/connect-redis": "^0.0.16",
    "@types/cors": "^2.8.10",
    "@types/express": "^4.17.11",
    "@types/express-graphql": "^0.9.0",
    "@types/express-session": "^1.17.3",
    "@types/extract-files": "^8.1.0",
    "@types/graphql": "^14.5.0",
    "@types/graphql-upload": "^8.0.4",
    "@types/inversify": "^2.0.33",
    "@types/jest": "^26.0.20",
    "@types/knex": "^0.16.1",
    "@types/lodash": "^4.14.168",
    "@types/morgan": "^1.9.2",
    "@types/node": "^14.14.28",
    "@types/nodemailer": "^6.4.0",
    "@types/redis": "^2.8.28",
    "@types/supertest": "^2.0.10",
    "@types/uuid": "^8.3.0",
    "@types/validator": "^13.1.3",
    "@types/winston": "^2.4.4",
    "jest": "^27.0.6",
    "jest-circus": "^27.0.6",
    "jest-extended": "^0.11.5",
    "jest-junit": "^12.0.0",
    "jest-watch-typeahead": "^0.6.1",
    "supertest": "^6.1.3",
    "ts-jest": "^27.0.4",
    "ts-node": "^10.2.0",
    "ts-node-dev": "^1.1.6",
    "typescript": "^4.1.5"
},
"jest-junit": {
    "suiteName": "api",
    "outputName": "junit.api.xml",
    "outputDirectory": "./reports",
    "suiteNameTemplate": "{displayName}",
    "classNameTemplate": "{classname}"
},
"mikro-orm": {
    "useTsNode": true,
    "configPaths": [
    "./src/db/mikro-orm/mikro-orm.config.ts"
    ]
}
}

high-level rundown of the project directory. i assume ./aws is used to orchestrate database and file storage.. as well as deployment? and ./nginx is for the server meant to be used in production? but i see that docker-compose.yml sources nginx config from ./nginx, but nothing about aws. is this because on the production server, the API is run in a container, but connects to AWS architecture outside of it?

.
|-- api
|   |-- src
|   |   |-- config
|   |   |-- db
|   |   |-- domain
|   |   |-- entrypoints
|   |   |-- errors
|   |   |-- jobs
|   |   |-- repos
|   |   |-- resolvers
|   |   |-- server
|   |   |-- services
|   |   |-- testing
|   |   |-- util
|   |   |-- inversify.config.ts
|   |   -- inversify.constants.ts
|   |-- codegen.yml
|   |-- jest.config.js
|   |-- package.json
|   |-- tsconfig.json
|   |-- tsconfig.prod.json
|   -- yarn.lock
|-- aws
|   |-- bin
|   |   -- aws.ts
|   |-- lib
|   |   |-- constructs
|   |   |-- CodebaseDevStack.ts
|   |   -- CodebaseStack.ts
|   |-- scripts
|   |   -- config.sh
|   |-- templates
|   |   -- vod.yml
|   |-- test
|   |   -- aws.test.ts
|   |-- README.md
|   |-- cdk.json
|   |-- jest.config.js
|   |-- package.json
|   |-- tsconfig.json
|   -- yarn.lock
|-- bin
|   -- ss
|-- e2e
|   |-- src
|   |   |-- util
|   |   |-- landing.test.ts
|   |   |-- library.test.ts
|   |   |-- login.test.ts
|   |   -- signup.test.ts
|   |-- Dockerfile
|   |-- PuppeteerEnvironment.js
|   |-- jest.config.js
|   |-- jest.setup.js
|   |-- package.json
|   |-- setup.js
|   |-- teardown.js
|   |-- tsconfig.json
|   -- yarn.lock
|-- env
|   |-- api.test.env
|   |-- dev.env
|   |-- fakeprod.env
|   -- web.test.env
|-- nginx
|   |-- nginx.conf
|   |-- nginx.dev.conf
|   -- nginx.fakeprod.conf
|-- scripts
|   |-- Env.ts
|   |-- aws.ts
|   |-- cleanup.ts
|   |-- constants.ts
|   |-- db.ts
|   |-- docker.ts
|   |-- graphql.ts
|   |-- test.ts
|   |-- types.ts
|   -- util.ts
|-- templates
|   -- secrets.template.env
|-- web
|   |-- public
|   |   |-- static
|   |   |-- favicon.ico
|   |   |-- index.html
|   |   |-- logo192.png
|   |   |-- logo384.png
|   |   |-- logo512.png
|   |   |-- manifest.json
|   |   -- robots.txt
|   |-- src
|   |   |-- assets
|   |   |-- components
|   |   |-- ctx
|   |   |-- domain
|   |   |-- hocs
|   |   |-- hooks
|   |   |-- lib
|   |   |-- testing
|   |   |-- util
|   |   |-- App.less
|   |   |-- App.test.tsx
|   |   |-- App.tsx
|   |   |-- config.ts
|   |   |-- constants.ts
|   |   |-- index.tsx
|   |   |-- jest.d.ts
|   |   |-- react-app-env.d.ts
|   |   |-- reportWebVitals.ts
|   |   |-- service-worker.ts
|   |   |-- serviceWorkerRegistration.ts
|   |   |-- setupTests.ts
|   |   -- theme.js
|   |-- README.md
|   |-- codegen.yml
|   |-- craco.config.js
|   |-- package.json
|   |-- tsconfig.json
|   -- yarn.lock
|-- Dockerfile
|-- Dockerfile.nginx
|-- LICENSE
|-- README.md
|-- SECURITY.md
|-- docker-compose.api.test.yml
|-- docker-compose.dev.yml
|-- docker-compose.migrator.yml
|-- docker-compose.web.test.yml
|-- docker-compose.yml
|-- gulpfile.ts
|-- package.json
|-- tree.txt
-- yarn.lock

42 directories, 92 files

so the nginx reverse proxy, like the aws services, runs outside of the API container? and it’s used as a security layer?


is Render’s nginx server also a reverse proxy that directs requests to your node server that runs on the instance? IWO, are the architectural choices in this codebase made in a Render deployment context, but abstracted from the developer? Also, I’ve used mongodb atlas in production, i.e., the database is in the cloud, not local to the instance. Is there an analogous cloud postgres database that doesn’t require navigating AWS?


still not clocking exactly why docker-compose.yml references the nginx RP but not AWS services. here’s the content:

version: '3.7'

services:
db:
    container_name: db
    image: postgres:11
    restart: always
    environment:
    POSTGRES_DB: codebase
    POSTGRES_USER: codebase
    POSTGRES_PASSWORD: codebase
    ports:
    - 5432:5432

redis:
    container_name: redis
    image: redis:5.0

nginx:
    container_name: nginx
    image: codebasenginx:latest
    volumes:
    - ./nginx/nginx.fakeprod.conf:/etc/nginx/nginx.conf
    links:
    - api
    ports:
    - 80:80

migrate:
    container_name: migrate
    image: codebase:latest
    build:
    context: .
    env_file:
    - ./env/fakeprod.env
    - ./env/secrets.env
    links:
    - db
    command: 'bash -c "cd api && yarn migrate && yarn seed"'

api:
    container_name: api
    image: codebase:latest
    env_file:
    - ./env/fakeprod.env
    - ./env/secrets.env
    links:
    - db
    - redis
    command: ['node', '/app/api/build/entrypoints/api.js']

worker:
    container_name: worker
    image: codebase:latest
    build:
    context: .
    env_file:
    - ./env/fakeprod.env
    - ./env/secrets.env
    environment:
    NODE_ENV: production
    PORT: 3000
    links:
    - db
    - redis
    ports:
    - 3000:3000
    command: ['node', '/app/api/build/entrypoints/worker.js']

dispatcher:
    container_name: dispatcher
    image: codebase:latest
    build:
    context: .
    env_file:
    - ./env/fakeprod.env
    - ./env/secrets.env
    environment:
    NODE_ENV: production
    PORT: 3001
    links:
    - db
    - redis
    ports:
    - 3001:3001
    command: ['node', '/app/api/build/entrypoints/dispatcher.js']

it seems from the project readme that docker is used to build the app locally too. so in all environments, the react frontend and express api, and the other services in docker-compose.yml are all run in a series of containers?


why isn’t the frontend run as a container?


There are two dockerfiles, one just called dockerfile:

FROM node:16.0.0

# Includes ffprobe
RUN apt-get update && apt-get -y install ffmpeg

WORKDIR /app

# make ss commands work
COPY ./package.json .
COPY ./yarn.lock .
RUN yarn
COPY ./bin bin
COPY ./scripts scripts
COPY ./gulpfile.ts .
ENV PATH="/app/bin:${PATH}"

# install api dependencies
COPY ./api/package.json api/
COPY ./api/yarn.lock api/
RUN ss installapi

# install web dependencies
COPY ./web/package.json web/
COPY ./web/yarn.lock web/
RUN ss installweb

# copy the web files over
COPY ./web/tsconfig.json web/
COPY ./web/craco.config.js web/
COPY ./web/.env web/

# copy the api files over
COPY ./api/tsconfig.json api/
COPY ./api/tsconfig.prod.json api/
COPY ./api/jest.config.js api/
COPY ./api/src api/src/

# build the api project
RUN yarn tsc --project api/tsconfig.prod.json

# run the project

CMD ["node", "yarn prod:api"]

and one called dockerfile.nginx:

FROM nginx:1.21

WORKDIR /www/data
COPY ./web/build/ /www/data/
COPY ./nginx/nginx.conf /etc/nginx/nginx.conf

I can see that Dockerfile.nginx bundles the frontend build into the nginx container, just not sure why that step requires a separate file. Also realizing the root level package.json provides gulp, which seems to be a tool that allows build/teardown/etc logic written in javascript (for which scripts/ may contain module code) to be triggered in the terminal as “./bin/ss [command] [args]”.


“Copies the contents of ./web/build (which must have already been built)”

Yes, at that point in the pipeline, ./web/build is presumably created by the gulp command “installweb” specified in the main Dockerfile.


I’m gathering from the series commands exported by the gulpfile…

exports['dev'] = series(buildapp, dev);
exports['fakeprod'] = series(buildnginx, buildapp, fakeprod);

dev: buildapp() ("docker build Dockerfile") -> dev() ("docker compose ./docker-compose-dev.yml")
fakeprod: buildnginx() ("docker build Dockerfile.nginx") -> buildapp() ("docker build Dockerfile") -> fakeprod() ("docker compose ./docker-compose.yml")

that buildnginx() is only run in fakeprod ("./bin/ss fakeprod" command) – so in fakeprod, Dockerfile.nginx is run before Dockerfile, which builds the frontend and bundles it in the nginx container; whereas in dev, the frontend runs on a node dev server.

So first you build the container with Dockerfile, then compose?


I’m just confused that both nginx and web services are listed in docker-compose-dev.yml:

nginx:
    container_name: nginx
    image: nginx:1.21
    volumes:
    - ./nginx/nginx.dev.conf:/etc/nginx/nginx.conf
    links:
    - api
    - web
    ports:
    - 80:80

// node
web:
    container_name: web
    image: codebase:latest
    volumes:
    - ./web/src:/app/web/src
    - ./web/public:/app/web/public
    command: ['yarn', 'dev:web']

Explain nginx.dev.conf:

http { 

client_max_body_size 2G;

server {
    listen 80;

    location / {
    proxy_pass http://web:3000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "Upgrade";
    proxy_set_header Host $host;
    }

    location /altair {
    proxy_pass http://api:3000;
    }

    location /health {
    proxy_pass http://api:3000;
    }

    location /graphql {
    proxy_pass http://api:3000;
    }
}
}

events {}

as contrasted with nginx.fakeprod.conf:

http {
include mime.types;

client_max_body_size 2G;

gzip on;
gzip_proxied any;
gzip_types text/plain application/json;
gzip_min_length 1000;

# Lie about scheme since it's fake prod.
proxy_set_header X-Forwarded-Proto https;

server {
    listen 80;
    
    root /www/data;
    index index.html

    error_page 404 =200 /;

    location / {
    try_files $uri /index.html;
    }

    location /altair {
    proxy_pass http://api:3000;
    }

    location /health {
    proxy_pass http://api:3000;
    }

    location /graphql {
    proxy_pass http://api:3000;
    }
}
}

events {}

Further, explain how the docker-compose files copy the nginx.conf files into the appropriate docker container,


And how do compose files know where to find the image mounted by a Dockerfile?


I’ve been using this project on and off for a while to develop architectural knowledge. Am I a maniacal genius?


how does the nginx container understand what “http://web:3000” and “http://api:3000” mean? are those actual domains, or shorthand for localhost:3000 in each container?


So docker is imposing a communications layer on top of http. otherwise, i’m not sure how those domains would work, virtualization or not.


so communication between services (i.e., frontend and backend, or backend and redis), is happening on different internal hosts + same port, rather than same host + different ports as in a typical dev environment?


why would redis need its own container for a session store when the api container could simply import connect-redis as usually happens?


do these internal domains work in a browser on the physical machine? how does the cra frontend communicate with them in dev? what is a docker “link”? why do only some services across the docker-compose files have “build: context .”? How do two images between both Dockerfiles become 5+ containers? Perhaps break down both docker-compose files again bearing these questions in mind.


so build applies to images built from existing dockerfiles, and build isn’t included for images specified directly in the docker-compose. comments below to demonstrate understanding:

version: '3.7'

services:

# build container from Dockerfile.nginx image

nginx:
    container_name: nginx
    image: codebasenginx:latest
    volumes:
    - ./nginx/nginx.fakeprod.conf:/etc/nginx/nginx.conf
    links:
    - api
    ports:
    - 80:80

# build containers from Dockerfile image (same backend, different service)

migrate:
    container_name: migrate
    image: codebase:latest
    build:
    context: .
    env_file:
    - ./env/fakeprod.env
    - ./env/secrets.env
    links:
    - db
    command: 'bash -c "cd api && yarn migrate && yarn seed"'

api:
    container_name: api
    image: codebase:latest
    env_file:
    - ./env/fakeprod.env
    - ./env/secrets.env
    links:
    - db
    - redis
    command: ['node', '/app/api/build/entrypoints/api.js']

worker:
    container_name: worker
    image: codebase:latest
    build:
    context: .
    env_file:
    - ./env/fakeprod.env
    - ./env/secrets.env
    environment:
    NODE_ENV: production
    PORT: 3000
    links:
    - db
    - redis
    ports:
    - 3000:3000
    command: ['node', '/app/api/build/entrypoints/worker.js']

dispatcher:
    container_name: dispatcher
    image: codebase:latest
    build:
    context: .
    env_file:
    - ./env/fakeprod.env
    - ./env/secrets.env
    environment:
    NODE_ENV: production
    PORT: 3001
    links:
    - db
    - redis
    ports:
    - 3001:3001
    command: ['node', '/app/api/build/entrypoints/dispatcher.js']

# build containers from other images
    
db:
    container_name: db
    image: postgres:11
    restart: always
    environment:
    POSTGRES_DB: codebase
    POSTGRES_USER: codebase
    POSTGRES_PASSWORD: codebase
    ports:
    - 5432:5432

redis:
    container_name: redis
    image: redis:5.0

then why don’t any services have build fields in docker-compose.dev.yml? it’s only used in the fakeprod version. look:

version: '3.7'

services:

# ===========================================
# build container from Dockerfile image (frontend)
# ===========================================

web:
    container_name: web
    image: codebase:latest
    volumes:
    - ./web/src:/app/web/src
    - ./web/public:/app/web/public
    command: ['yarn', 'dev:web']

# ===========================================
# build containers from Dockerfile image (same backend, different service)
# ===========================================

api:
    container_name: api
    image: codebase:latest
    env_file:
    - ./env/dev.env
    - ./env/secrets.env
    volumes:
    - ./api/src:/app/api/src
    links:
    - db
    - redis
    ports:
    - 3000:3000
    command: ['yarn', 'dev:api']

migrate:
    container_name: migrate
    image: codebase:latest
    env_file:
    - ./env/dev.env
    - ./env/secrets.env
    volumes:
    - ./api/src:/app/api/src
    links:
    - db
    command: 'bash -c "yarn migrate && yarn seed"'

worker:
    container_name: worker
    image: codebase:latest
    env_file:
    - ./env/dev.env
    - ./env/secrets.env
    environment:
    PORT: 3001
    volumes:
    - ./api/src:/app/api/src
    links:
    - db
    - redis
    ports:
    - 3001:3001
    command: ['yarn', 'dev:worker']

dispatch:
    container_name: dispatcher
    image: codebase:latest
    env_file:
    - ./env/dev.env
    - ./env/secrets.env
    environment:
    PORT: 3002
    volumes:
    - ./api/src:/app/api/src
    links:
    - db
    - redis
    ports:
    - 3002:3002
    command: ['yarn', 'dev:dispatcher']

# ===========================================
# build containers from Docker Hub
# ===========================================

db:
    container_name: db
    image: postgres:11
    restart: always
    environment:
    POSTGRES_DB: codebase
    POSTGRES_USER: codebase
    POSTGRES_PASSWORD: codebase
    ports:
    - 5432:5432

redis:
    container_name: redis
    image: redis:5.0

# nginx as reverse proxy only
nginx:
    container_name: nginx
    image: nginx:1.21
    volumes:
    - ./nginx/nginx.dev.conf:/etc/nginx/nginx.conf
    links:
    - api
    - web
    ports:
    - 80:80

“docker build -t” is run in both dev and fakeprod.

gulpfile.ts:

exports['dev'] = series(buildapp, dev);
exports['fakeprod'] = series(buildnginx, buildapp, fakeprod);

async function buildapp() {
const dockerTag = DOCKER_TAG.getOrDefault('codebase:latest');
const dockerfile = DOCKERFILE.getOrDefault('Dockerfile');

await docker.build(dockerfile, dockerTag);
}