Files
nick-doc/08 - Operations/Docker Setup.md
2026-05-23 20:35:34 +03:30

13 KiB

title, tags
title tags
Docker Setup
operations

Docker Setup

Walk-through of every Dockerfile, compose file, volume, and network used by the marketplace stack. Cross-references Deployment for the live-host configuration and Local Setup for developer use.


1. Backend — Dockerfile.dev

Path: /Users/mojtabaheidari/code/backend/Dockerfile.dev

FROM node:22-alpine
RUN corepack enable
WORKDIR /app
COPY package.json ./
RUN yarn install --frozen-lockfile
COPY . .
RUN mkdir -p uploads/{avatars,documents,products,temp}
RUN addgroup -g 1001 -S nodejs && adduser -S marketplace -u 1001
RUN chown -R marketplace:nodejs /app
USER marketplace
EXPOSE 5001
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD node healthcheck.js
CMD ["yarn", "dev"]

Notes:

  • Base. node:22-alpine — small, glibc-musl. Corepack is enabled to use the pinned Yarn 1.22.22.
  • Install. yarn install --frozen-lockfile brings dev dependencies (needed for ts-node + nodemon hot reload).
  • Uploads scaffold. Creates the four canonical upload directories so the API doesn't have to mkdir at runtime.
  • Non-root user. Process runs as marketplace (uid 1001). Defence-in-depth.
  • Healthcheck. healthcheck.js does a local HTTP GET to /health (see Monitoring).
  • CMD. yarn devnodemon --exec ts-node src/app.ts. Source code is mounted from the host so saves trigger restarts.

Used by docker-compose.dev.yml. Not pushed to the registry — dev images are local.


2. Backend — Dockerfile.prod

Path: /Users/mojtabaheidari/code/backend/Dockerfile.prod

Multi-stage build to keep the runtime image small and free of build tooling.

# ---- builder ----
FROM node:22-alpine AS builder
RUN corepack enable
WORKDIR /app
COPY package.json ./
COPY healthcheck.js ./
RUN yarn install --frozen-lockfile
COPY . .
RUN yarn build              # tsc → ./dist

# ---- production ----
FROM node:22-alpine AS production
RUN corepack enable
WORKDIR /app
COPY package.json ./
COPY healthcheck.js ./
RUN yarn install --frozen-lockfile --production && yarn cache clean
COPY --from=builder /app/dist ./dist
RUN mkdir -p uploads/{avatars,documents,products,temp}
RUN addgroup -g 1001 -S nodejs && adduser -S marketplace -u 1001
RUN chown -R marketplace:nodejs /app
USER marketplace
EXPOSE 5001
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD node healthcheck.js
CMD ["node", "dist/app.js"]

Notes:

  • Two stages. builder compiles TS to JS; production keeps only the compiled output + production deps. Final image is ~150 MB.
  • No dev deps. --production flag in the second stage trims away TypeScript, Jest, ts-node etc.
  • Same non-root pattern. marketplace:nodejs (uid 1001).
  • CMD. Plain node dist/app.js — no transpilation at runtime.
  • Uploads. The directory is created inside the image, then the running container mounts /app/uploads from a host volume in compose (overrides the embedded dir).

Built and pushed by .gitea/workflows/docker-build-no-cache.yml (and friends — see CI-CD Pipeline). The resulting image is git.manko.yoga/manawenuz/escrow-backend:<version> + :latest.


3. Frontend — Dockerfile (production)

Path: /Users/mojtabaheidari/code/frontend/Dockerfile

Multi-stage Next.js standalone build.

# ---- builder ----
FROM node:22-alpine AS builder
# (NEXT_PUBLIC_* vars set here so they bake into the bundle)
ENV NEXT_PUBLIC_API_URL=https://dev.amn.gg/api
ENV NEXT_PUBLIC_BACKEND_URL=https://dev.amn.gg
# ...more ENV lines (see file)...

RUN apk add --no-cache git python3 make g++ py3-pip
RUN corepack enable
WORKDIR /app
COPY package.json yarn.lock* ./
RUN yarn install --frozen-lockfile --production=false --network-timeout 600000
COPY src ./src
COPY public ./public
COPY next.config.ts tsconfig.json ./
COPY *.config.mjs ./
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN yarn build              # produces .next/standalone + .next/static

# ---- runner ----
FROM node:22-alpine AS runner
RUN apk add --no-cache curl
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
WORKDIR /app
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
ENV PORT=8083 HOSTNAME="0.0.0.0" NODE_ENV=production
ENV NEXT_PUBLIC_SENTRY_DSN=https://...sentry.io/...
USER nextjs
EXPOSE 8083
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:8083 || exit 1
CMD ["node", "server.js"]

Notes:

  • Baked env vars. NEXT_PUBLIC_* variables are set as ENV in the builder stage so Next inlines them into the static bundle at build time. To deploy to a different domain you must rebuild — there is no runtime override for NEXT_PUBLIC_*. See Environment Variables#how-env-is-loaded.
  • System packages. git python3 make g++ py3-pip are needed by node-gyp for native modules (e.g. sharp, @google-cloud/local-auth).
  • Standalone output. next.config.ts sets output: 'standalone', so the runner stage copies only .next/standalone/ and public/ — a self-contained tree with a built-in server.js. Final runtime image: ~250 MB.
  • Non-root. nextjs (uid 1001).
  • server.js is generated by Next.js — it embeds the necessary Node modules and starts the production server.

4. Frontend — Dockerfile.dev

Path: /Users/mojtabaheidari/code/frontend/Dockerfile.dev

FROM node:22-alpine
RUN apk add --no-cache git python3 make g++ py3-pip
RUN corepack enable
WORKDIR /app
COPY package.json yarn.lock* ./
RUN yarn config set network-timeout 600000 && \
    yarn config set network-concurrency 1 && \
    yarn install --frozen-lockfile --network-timeout 600000
COPY . .
EXPOSE 3000
CMD ["yarn", "dev:docker"]

Notes:

  • Listens on port 3000 in dev (matches the legacy convention).
  • yarn dev:docker is a variant of dev that binds 0.0.0.0 so the container is reachable from the host.
  • No multi-stage — speed > size.

Used for local development if you choose to run the frontend in Docker instead of via yarn dev. Most developers run frontend natively for HMR speed; backend in Docker for parity.


5. docker-compose.dev.yml

Path: /Users/mojtabaheidari/code/backend/docker-compose.dev.yml

name: nickapp-development

services:
  nickdev-backend:
    build: { context: ., dockerfile: Dockerfile.dev }
    container_name: nickdev-backend
    env_file: [.env.local]
    ports: ["5001:5001"]
    volumes:
      - ./src:/app/src
      - ./uploads:/app/uploads
    depends_on: [mongodb, redis]
    restart: unless-stopped
    networks: [nickapp-network]

  mongodb:
    image: mongo:8.2
    container_name: nickdev-mongodb
    ports: ["27017:27017"]
    env_file: [.env.local]
    volumes: [mongodb_data:/data/db]
    restart: unless-stopped
    networks: [nickapp-network]

  redis:
    image: redis:8-alpine
    container_name: nickdev-redis
    env_file: [.env.local]
    command: redis-server
    volumes: [redis_data:/data]
    restart: unless-stopped
    networks: [nickapp-network]

networks:
  nickapp-network: { driver: bridge }

volumes:
  mongodb_data:
  redis_data:

Highlights:

  • No auth on Mongo/Redis in dev. Mongo runs default; Redis runs plain redis-server.
  • Source mounted. ./src is volume-mounted into the backend container so hot reload works.
  • Uploads mounted. ./uploads on the host is bind-mounted to /app/uploads so files survive container restarts.
  • Port mappings: 5001 (backend) + 27017 (Mongo) exposed to host. Redis is not exposed by default.
  • Network. nickapp-network bridge — Mongo/Redis are reachable as mongodb / redis from the backend container.

6. docker-compose.production.yml

Path: /Users/mojtabaheidari/code/backend/docker-compose.production.yml

Five services. Reproducing only the most important bits — full file lives in the repo and is summarised in Deployment#compose-file.

name: nickapp-production

services:
  nginx:
    image: nginx:alpine
    container_name: nickapp-nginx
    ports: ["8083:80"]
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/logs:/var/log/nginx
      - ./uploads:/uploads
    depends_on: [nickapp-backend, nickapp-frontend]
    networks: [default]

  nickapp-backend:
    build: { context: ., dockerfile: Dockerfile.prod }
    image: nickapp-backend:latest
    container_name: nickapp-backend
    platform: linux/amd64
    env_file: [.env]
    volumes: [./uploads:/app/uploads]
    depends_on: [mongodb, redis]
    networks: [default]
    healthcheck: { test: ["CMD","curl","-f","http://localhost:5001/health"], ... }
    labels: ["com.centurylinklabs.watchtower.enable=true"]

  mongodb:
    image: mongo:8.2
    container_name: nickapp-mongodb
    env_file: [.env]
    volumes:
      - mongodb_data:/data/db
      - ./mongo-init:/docker-entrypoint-initdb.d
    healthcheck: { test: ["CMD","mongosh","--eval","db.adminCommand('ping')"], ... }

  redis:
    image: redis:8-alpine
    container_name: nickapp-redis
    env_file: [.env]
    command: ["sh","-lc","redis-server --requirepass \"$${REDIS_PASSWORD}\""]
    volumes: [redis_data:/data]
    healthcheck: { test: ["CMD","redis-cli","-a","$${REDIS_PASSWORD}","ping"], ... }

  nickapp-frontend:
    build: { context: ../frontend, dockerfile: Dockerfile }
    image: nickapp-frontend:latest
    container_name: nickapp-frontend
    platform: linux/amd64
    env_file: [.env]
    environment: [PORT=8083, NODE_ENV=production]
    expose: ["8083"]
    healthcheck: { test: ["CMD","curl","-f","http://localhost:8083/"], ... }
    labels: ["com.centurylinklabs.watchtower.enable=true"]

networks:
  default: { driver: bridge }

volumes:
  mongodb_data:
  redis_data:

Key differences from dev:

  • Nginx added as the public entry point.
  • Backend and frontend are labelled for Watchtower auto-updates.
  • Mongo and Redis are not Watchtower-managed — their major versions need manual planning + backup (Backup & Recovery).
  • Redis password is read from .env (escaped $$ so docker compose doesn't expand it).
  • Frontend build context points at ../frontend — the two repos must live as siblings on disk.
  • No host port mapping for backend/frontend — they are reached only via the nginx container.
  • platform: linux/amd64 is pinned because production hosts are x86_64; ARM developers must --platform=linux/amd64 if they build locally for prod.

7. Volumes

Volume Mount point Lifecycle Notes
mongodb_data (named) /data/db in mongodb Persistent The whole database. Back up via mongodump.
redis_data (named) /data in redis Persistent RDB snapshots + AOF if configured.
./uploads (bind) /app/uploads in backend, /uploads in nginx Persistent on host User-uploaded files. Critical — back up the directory.
./nginx/nginx.conf (bind, RO) /etc/nginx/nginx.conf Static Reverse-proxy config.
./nginx/logs (bind) /var/log/nginx Append-only on host Access + error logs.
./mongo-init (bind, RO) /docker-entrypoint-initdb.d One-time JS files Mongo runs only on a fresh datadir to create initial users / indexes.

Inspect named volumes:

docker volume ls
docker volume inspect nickapp-production_mongodb_data

[!warning] docker compose down -v deletes named volumes. Never run this in production unless you've backed up first.


8. Networks

  • Dev: nickapp-network bridge. All three services join it; the backend reaches mongodb and redis by container name.
  • Prod: the default compose network (also a bridge), named nickapp-production_default. Same DNS-by-container-name semantics. Nginx talks to nickapp-backend:5001 and nickapp-frontend:8083 over this network.

Inspect:

docker network ls
docker network inspect nickapp-production_default

9. Image build & push from a developer machine

For a production-parity build locally (without going through CI):

cd ~/code/backend
docker build --platform=linux/amd64 -f Dockerfile.prod \
  -t git.manko.yoga/manawenuz/escrow-backend:test .

# Sanity-check size + run
docker images git.manko.yoga/manawenuz/escrow-backend
docker run --rm -p 5001:5001 --env-file .env.local \
  git.manko.yoga/manawenuz/escrow-backend:test

For the official path (build + push to registry) use ./scripts/build-and-push.sh — see Scripts#build-and-push-sh — or rely on CI-CD Pipeline to do it on every push.


10. Image cleanup

Builds accumulate. Periodically prune:

docker system prune -a -f
docker volume prune -f          # ⚠ removes unused named volumes — check first
docker builder prune -a -f      # buildx cache

# scripted (backend)
npm run docker:clean

docker:clean runs docker system prune -a -f && docker volume prune -f — confirm you don't need anything before you run it.

[!warning] docker volume prune will delete mongodb_data and redis_data if their compose project is currently down. Always run docker compose up -d first to keep the volumes "in use".