← 글 목록

notes

Docker - Quick Starer (in turborepo)

Turborepo 프로젝트를 Docker로 시작하는 기본 구성·빌드 흐름 메모.

  • 서버에는 무료인 Docker CE를 깔고 systemctl enable로 부팅 때 같이 뜨게 해 둔다.
  • 모노레포는 turbo prune web --docker로 필요한 패키지만 잘라서 멀티 스테이지로 빌드한다.
  • Next는 standalone 출력만 최종 이미지에 넣고, compose로 포트를 연결해 띄운다.

회사 서버에 turborepo로 만든 Next 앱을 처음 올리면서 적어 둔 메모다. Docker는 무료인 CE(Community Edition)와 기업 지원이 붙는 유료 버전이 있는데, 서버 한두 대 운영에는 CE로 충분했다.

Docker 서버 설치

start는 지금 한 번 띄우는 거고 enable은 재부팅 후에도 자동으로 뜨게 하는 거다. 처음엔 start만 해 두고 서버 재시작 뒤에 컨테이너가 다 죽어 있어서 놀랐다. 매번 sudo 치기 싫으면 sudo usermod -aG docker $USER 후 다시 로그인하면 된다.



# download doker 
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

# doker-ce set 
sudo yum install -y docker-ce

# doker in start
sudo systemctl start docker


# doker auto start able 
sudo systemctl enable docker

#permission 

sudo chown -R $(whoami) ~/.docker

Docker 데몬 설정

데몬 설정은 /etc/docker/daemon.json 하나에 모인다. 나는 사내망에서 이미지 pull이 DNS 때문에 실패해서 DNS를 박아 두었다.

  • 설정 파일을 연다. 없으면 새로 만든다.
    sudo vi /etc/docker/daemon.json

    DNS만 넣으면 이 정도다.

    {
      "dns": ["8.8.8.8", "8.8.4.4"]
    }
  • 저장하고 재시작해야 반영된다.
    sudo systemctl restart docker
    

로그도 같이 제한해 두는 게 좋다. 기본 json-file 로그는 크기 제한이 없어서, 오래 떠 있는 컨테이너 하나가 디스크를 다 먹은 적이 있다. JSON이 깨지면 데몬이 아예 안 뜨니 저장 전에 쉼표를 확인한다.

{
  "dns": ["8.8.8.8", "8.8.4.4"],
  "log-driver": "json-file",
  "log-opts": { "max-size": "10m", "max-file": "3" }
}

자주 쓰는 명령


자주 쓰는 docker 명령
# 실행중인 컨테이너 나열 : 
sudo docker ps

# 모든 컨테이너 나열 (중지된 컨테이너 포함) : 
sudo docker ps -a

# Docker Hub에서 이미지 가져오기 : 
sudo docker pull nginx

# 이는 분리 모드에서 Nginx 컨테이너를 실행하여 호스트의 포트 8080을 컨테이너의 포트 80에 매핑합니다.
# 컨테이너 실행 : 
sudo docker run -d -p 8080:80 nginx

# 실행중인 컨테이너 중지 : 
sudo docker stop container_id_or_name

# 컨테이너 제거 : 
sudo docker rm container_id_or_name

# 이미지 제거 : 
sudo docker rmi image_id_or_name

# 테스트
sudo docker run --rm -it image_id_or_name

Next.js Docker

Next는 next.config에 output: 'standalone'을 주면 실행에 필요한 파일만 .next/standalone에 모아 준다. 그걸 복사해서 node server.js로 띄우면 node_modules 전체를 이미지에 넣지 않아도 된다.

아래 Dockerfile은 처음에 썼던 단순 복사 방식이다. 모노레포 전체를 install하다 보니 다른 앱 때문에 lockfile이 바뀌어도 매번 처음부터 설치했고, 그래서 아래 turbo prune 방식으로 갈아탔다.

처음 쓰던 단순 Dockerfile
FROM node:16
 
WORKDIR /usr/src/app
 
# Copy root package.json and lockfile
COPY package.json ./
COPY package-lock.json ./
 
# Copy the api package.json
COPY apps/api/package.json ./apps/api/package.json
 
RUN npm install
 
# Copy app source
COPY . .
 
EXPOSE 8080
 
CMD [ "node", "apps/api/server.js" ]
# .dockerignore

node_modules
npm-debug.log
Dockerfile
.dockerignore
.git

# 빌드
docker build -t my-nextjs-app .
# 실행
docker run -d -p 3000:3000 my-nextjs-app

turborepo Docker

turbo prune web --docker는 web 앱과 web이 의존하는 패키지만 남긴 작은 모노레포를 out/에 만들어 준다. 여기서 --docker가 핵심이다.

out/json에는 package.json들과 잘라낸 lockfile만 있어서, 이걸 먼저 COPY하고 install하면 소스만 바뀐 빌드에서는 설치 레이어 캐시가 그대로 쓰인다. 그다음 out/full을 복사해서 빌드한다. 마지막 runner 단계는 standalone 결과물과 static, public만 복사하고 root가 아닌 nextjs 유저로 띄운다.

turbo prune 멀티 스테이지 Dockerfile
# Docker file create


FROM node:18-slim AS base

# Set working directory
WORKDIR /app

# Install pnpm and turbo globally
RUN npm install -g pnpm turbo

FROM base AS builder

# Copy project files
COPY . .

# Generate a partial monorepo with a pruned lockfile for a target workspace
RUN turbo prune web --docker

FROM base AS installer

WORKDIR /app

# Copy the pruned lockfile and package.json files
COPY --from=builder /app/out/json/ .
RUN pnpm install

# Build the project
COPY --from=builder /app/out/full/ .
RUN pnpm turbo run build --filter=web...

FROM base AS runner

WORKDIR /app

# Don't run production as root
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
USER nextjs

# Automatically leverage output traces to reduce image size
COPY --from=installer --chown=nextjs:nodejs /app/apps/web/.next/standalone ./
COPY --from=installer --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static
COPY --from=installer --chown=nextjs:nodejs /app/apps/web/public ./apps/web/public

CMD ["node", "apps/web/server.js"]
# .dockerignore

node_modules
npm-debug.log
Dockerfile
.dockerignore
.git

pnpm turbo prune web --docker   # web 자리에 워크스페이스 이름 (예전 --scope 옵션은 이제 안 씀)

sudo docker build -t 컨테이너이름 .

모노레포에서 standalone을 쓰면 outputFileTracingRoot를 루트로 잡아 줘야 packages/* 코드까지 추적해서 넣어 준다. 아래는 당시 설정인데 output: 'standalone'이 빠져 있어서, 지금 다시 쓴다면 nextConfig에 그 줄부터 넣는다. Next 15부터는 outputFileTracingRoot도 experimental 밖 최상위 옵션으로 옮겨졌다.

당시 next.config
// next.config.mjs or js

import path from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);

/** @type {import('next').NextConfig} */
const nextConfig = {
    reactStrictMode: false,
   /*  transpilePackages: ['ui',"@ui"], */
    async rewrites() { 
      return [
  
         {
           source: 'url넣어줘잉/:path*',
           destination: `api 넣어줘잉`,
         },
  
      ];
    },
    experimental:{
outputFileTracingRoot: path.join(path.dirname(__filename), `../../`) 
    }}

export default nextConfig;


version: '3.8'

services:
  web:
    image: smartq-app  # 빌드된 Docker 이미지의 이름 또는 태그
    build:
      context: .
      dockerfile: Dockerfile
    # 13002(외부) 로 -> 3000 (내부 -> 프로젝트 내부 포트 변경시 해당부분 변경) 
    ports:
      - "13002:3000"
    environment:
      NODE_ENV: production
    command: ["node", "apps/web/server.js"]
    networks:
      - webnet

networks:
  webnet:

  

정리할 때 쓰는 prune 명령 모음

1번은 볼륨까지 지우고, 7번은 떠 있는 컨테이너까지 전부 내린다. 운영 서버에서는 이 두 개를 치기 전에 한 번 더 본다.

# 1. 사용하지 않는 컨테이너, 네트워크, 이미지, 캐시, 볼륨 모두 제거
sudo docker system prune -a --volumes

# 2. 중지된 컨테이너만 제거
sudo docker container prune

# 3. 사용하지 않는 이미지 제거
sudo docker image prune -a

# 4. 사용하지 않는 볼륨 제거
sudo docker volume prune

# 5. 사용하지 않는 네트워크 제거
sudo docker network prune

# 6. Docker 빌드 캐시 제거
sudo docker builder prune

# 7. 모든 컨테이너 중지 및 제거
sudo docker stop $(sudo docker ps -a -q)
sudo docker rm $(sudo docker ps -a -q)

배포를 몇 번 반복하면 이미지가 금방 쌓인다. 디스크가 차면 docker system df로 뭐가 큰지 먼저 보고, 위 prune 명령을 돌린다. 그래도 데몬이 이상하면 daemon.json을 백업해 두고 설정을 빼서 재시작해 본다. 매일 자동으로 정리하는 방법은 auto clean 글에 적어 뒀다.