Initial commit: nick docs
This commit is contained in:
381
08 - Operations/Docker Setup.md
Normal file
381
08 - Operations/Docker Setup.md
Normal file
@@ -0,0 +1,381 @@
|
||||
---
|
||||
title: Docker Setup
|
||||
tags: [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`
|
||||
|
||||
```dockerfile
|
||||
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 dev` → `nodemon --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.
|
||||
|
||||
```dockerfile
|
||||
# ---- 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.
|
||||
|
||||
```dockerfile
|
||||
# ---- 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`
|
||||
|
||||
```dockerfile
|
||||
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`
|
||||
|
||||
```yaml
|
||||
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]].
|
||||
|
||||
```yaml
|
||||
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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
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):
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
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".
|
||||
Reference in New Issue
Block a user