서버 설치¶
이 저장소에서 서버 이미지 두 개를 빌드하고 서버 env 파일을 작성한 뒤 Docker Compose로 스택을 올리는 절차.
스택은 컨테이너 두 개로 구성된다. cctraced(OTLP 수집, 동기화 API, 대시보드)와 TimescaleDB다. 두 이미지 모두 레지스트리에 게시되지 않는 로컬 빌드 태그이고 compose 파일이 pull_policy: never를 지정하므로, 두 이미지를 먼저 빌드하지 않으면 스택이 시작되지 않는다.
요구 사항¶
- Compose 플러그인이 포함된 Docker
- 이 저장소의 클론. 아래 모든 명령은 저장소 루트에서 실행
1. 이미지 빌드¶
$ docker build -t cctrace/timescaledb-pgmq:latest docker/timescaledb-pgmq/
$ docker build -f deploy/Dockerfile --build-arg UPDATE_SIGNING=optional \
-t cctrace/cctraced:latest .
- 첫 번째 이미지: pgmq 확장을 포함한 TimescaleDB
- 두 번째 이미지: Docker 안에서 대시보드와
cctraced바이너리를 빌드.UPDATE_SIGNING=optional은 업데이트 서명 키 없이 빌드. 기본값required는 키가 없으면 실패 - 서버 env 파일에
IMAGE_TAG또는DB_IMAGE_TAG를 지정했다면 정확히 같은 태그로 빌드(cctrace/cctraced:<IMAGE_TAG>,cctrace/timescaledb-pgmq:<DB_IMAGE_TAG>). Compose는 레지스트리로 대체하지 않음
2. 서버 env 파일 작성¶
Compose는 deploy/ 디렉터리의 .env(서버 env 파일)에서 설정을 읽는다. 예제 파일을 복사한 뒤 아래 3개 키만 고친다. 나머지 키는 기본값과 설명 주석 그대로 둔다.
$ cp deploy/.env.example deploy/.env
| 키 | 필수인 이유 |
|---|---|
JWT_SECRET |
그대로 둘 기본값 없음. 없으면 Compose가 시작 거부, 32바이트 미만이면 cctraced 종료. openssl rand -hex 32로 생성 |
LOGS_DIR |
기본값 없음. 없으면 Compose가 시작 거부. 컨테이너의 /data/logs에 마운트되는 호스트 디렉터리. 미리 생성 권장 |
DB_PASSWORD |
change-me-strong-password 자리 표시 값이 기본값이며 그대로 두면 안 됨. openssl rand -hex 20으로 생성 |
deploy/.env의 3개 키는 에디터로 고치거나, 명령줄에서 바로 바꾼다.
$ jwt_secret=$(openssl rand -hex 32) && sed -i.bak "s|^JWT_SECRET=.*|JWT_SECRET=${jwt_secret}|" deploy/.env
$ db_password=$(openssl rand -hex 20) && sed -i.bak "s|^DB_PASSWORD=.*|DB_PASSWORD=${db_password}|" deploy/.env
$ LOGS_DIR=/absolute/path/on/the/host/for/logs && mkdir -p "$LOGS_DIR" && sed -i.bak "s|^LOGS_DIR=.*|LOGS_DIR=${LOGS_DIR}|" deploy/.env
$ rm deploy/.env.bak
DB_PASSWORD는 첫 시작 전에 결정
PostgreSQL은 데이터 볼륨을 처음 초기화할 때만 비밀번호를 적용한다. 이후 DB_PASSWORD를 바꾸면 DB 역할은 옛 비밀번호로 남고 cctraced는 password authentication failed로 재시작을 반복한다. 복구하려면 DB 볼륨을 지워야 하는데 그러면 수집한 데이터가 모두 삭제된다.
DB_APP_CREDENTIALS도 첫 시작 전에 지정한다. <user>:<password>를 넣으면 첫 시작 때 비슈퍼유저 역할이 만들어지고 cctraced가 그 역할로 접속한다. 비워 두면 cctraced는 DB 슈퍼유저로 접속한다. 역할은 첫 시작 때만 만들어지며 이름은 cctrace일 수 없다. 비밀번호는 DB_PASSWORD처럼 openssl rand -hex 20으로 만든다. compose가 이 값을 연결 URL에 그대로 넣으므로 /, #, ?, %가 들어가면 URL이 깨진다.
$ app_password=$(openssl rand -hex 20) && sed -i.bak "s|^DB_APP_CREDENTIALS=.*|DB_APP_CREDENTIALS=cctrace_app:${app_password}|" deploy/.env
$ rm deploy/.env.bak
나머지 이름 규칙, 첫 시작에서 역할이 만들어지지 않았을 때의 조치, 이미 사용 중인 DB의 경우는 설정 참조.
3. 노출 범위 확인¶
첫 시작 전에 각 포트가 어느 인터페이스에 바인드될지 정한다.
| 포트 | 기본 호스트 바인드 | 용도 |
|---|---|---|
| 8080 | 127.0.0.1 | 대시보드와 REST API(클라이언트 동기화 포함) |
| 4317 | 0.0.0.0(모든 인터페이스) | OTLP over gRPC |
| 4318 | 0.0.0.0(모든 인터페이스) | OTLP over HTTP |
| 5432 | 127.0.0.1(고정) | TimescaleDB |
OTLP는 TLS 없이 모든 인터페이스에서 수신
4317과 4318 포트는 다른 머신의 텔레메트리를 받는다. cctraced는 TLS를 종료하지 않는다. 통제하지 않는 네트워크라면 서버 env 파일에서 GRPC_BIND와 OTEL_HTTP_BIND를 루프백이나 사설 주소로 지정하거나, 방화벽·VPN으로 포트를 제한하거나, HTTPS 오버레이를 쓴다.
대시보드는 기본적으로 루프백에 바인드된다. 클라이언트 머신이 동기화하려면 8080 포트에 닿아야 하므로 HTTP_BIND를 도달 가능한 주소로 지정하거나 TLS를 종료하는 프록시를 앞에 둔다. 전체 포트 표는 포트 참조.
4. 스택 시작¶
$ docker compose --env-file deploy/.env -f deploy/docker-compose.yml up -d
up -d가 정상 종료했다는 것은 컨테이너가 생성됐다는 뜻일 뿐 서버가 시작됐다는 뜻은 아니다. cctraced의 설정 오류는 자체 로그에만 나온다.
5. 확인¶
$ docker compose --env-file deploy/.env -f deploy/docker-compose.yml ps
$ curl -fsS http://127.0.0.1:8080/api/health

- 두 컨테이너 모두
Up과(healthy)표시가 정상.cctraced헬스 체크는/api/version을 확인하며 시작 유예가 30초라 처음에는health: starting표시 /api/health응답: DB에 연결되면"status":"ok", 연결되지 않으면 HTTP 503과"status":"unhealthy"cctraced가Restarting이면 로그 확인:
$ docker compose --env-file deploy/.env -f deploy/docker-compose.yml logs cctraced
6. 첫 관리자 생성¶
사용자가 없는 DB에서는 cctraced가 로그에 일회용 설정 토큰을 출력한다.
[cctraced] initial administrator setup token: <token>

토큰을 직접 정하려면 첫 시작 전에 서버 env 파일에 CCTRACE_SETUP_TOKEN을 지정한다. 첫 관리자가 생성되면 토큰은 무효가 된다. 이어서 첫 관리자로 진행한다.
선택: Caddy로 HTTPS 적용¶
deploy/docker-compose.tls.yml은 Caddy 컨테이너를 추가하는 선택형 오버레이다. Caddy가 세 리스너 모두의 TLS를 종료하고 deploy/caddy/Caddyfile에 따라 compose 네트워크를 통해 cctraced로 전달한다. 기본 compose 파일은 그대로이며 Caddy 이미지(caddy:2-alpine)는 Docker Hub에서 받는다.
-
서버 env 파일에서 평문 포트를 루프백으로 옮기고 서버 이름 지정:
HTTP_BIND=127.0.0.1 GRPC_BIND=127.0.0.1 OTEL_HTTP_BIND=127.0.0.1 CADDY_SITE_ADDRESS=cctrace.company.exampleCADDY_SITE_ADDRESS는 오버레이 필수값이며 호스트 이름이나 서버 IP 주소 모두 가능. 바인드 설정 세 개를 빼면 평문 포트가 TLS 포트 옆에서 계속 열려 있음. -
compose 파일 두 개로 시작:
$ docker compose --env-file deploy/.env \ -f deploy/docker-compose.yml -f deploy/docker-compose.tls.yml up -d
| 채널 | 평문(컨테이너) | TLS(호스트 기본값) |
|---|---|---|
| 대시보드와 REST API | 8080 | 8443 |
| OTLP gRPC | 4317 | 5317 |
| OTLP HTTP | 4318 | 5318 |
이후 클라이언트는 대시보드와 동기화에 https://cctrace.company.example:8443, OTLP에 5317 또는 5318 포트를 사용한다.
Codex는 OTLP/HTTP로 보내므로 cctrace는 프로필의 OTEL 엔드포인트에서 4317을 4318로 바꾸듯 5317을 5318로 바꿔 Codex 메트릭 엔드포인트를 만든다. 내부 CA를 쓰면 Codex 설정에도 그 CA가 있어야 한다. 아래와 Codex CLI 참조.
인증서¶
CADDY_TLS_MODE 기본값은 internal이다. Caddy가 자체 CA로 인증서를 발급하므로 공개 DNS가 필요 없고 IP 주소만으로도 동작한다. 클라이언트는 이 CA를 신뢰해야 한다. CA 내보내기:
$ docker compose --env-file deploy/.env \
-f deploy/docker-compose.yml -f deploy/docker-compose.tls.yml \
exec caddy cat /data/caddy/pki/authorities/local/root.crt > cctrace-ca.crt
이 파일을 각 클라이언트 머신에 복사해 cctrace에 한 번 지정한다. 엔드포인트가 https://이면 cctrace init이 묻고, 직접 지정해도 된다.
$ cctrace config set server.ca_cert_file ~/cctrace-ca.crt
OS 키체인에 이미 CA가 있어도 지정한다. 키체인만으로 Claude Code와 Codex에 충분한지는 시험하지 않았다. 클라이언트마다 CA를 받는 곳이 달라서 cctrace가 각각에 이 파일 하나를 넣는다.
| 클라이언트 | CA를 읽는 곳 | 설정 주체 |
|---|---|---|
| cctrace | 시스템 루트 + server.ca_cert_file |
cctrace |
| Claude Code | 설정 파일의 NODE_EXTRA_CA_CERTS, exporter는 5318로 http/protobuf(이유) |
cctrace |
| Codex | [otel] 섹션의 tls = { ca-certificate = "..." } |
cctrace |
실행 중인 동기화 데몬은 시작할 때의 CA를 계속 쓴다. CA를 지정하거나 해제한 뒤에는 데몬을 다시 시작한다(cctrace sync --stop 후 평소대로 시작). Codex 섹션은 다음 동기화 시작 때 다시 쓰인다. 클라이언트 설정 참조.
클라이언트 바이너리를 처음 curl로 내려받을 때는 이 설정이 쓰이지 않으므로 --cacert cctrace-ca.crt를 붙인다. 클라이언트 설치 참조.
이름이 공개 DNS로 해석되고 인터넷에서 80 포트에 닿을 수 있다면 CADDY_TLS_MODE에 ACME 계정 이메일 주소(예: ops@example.com)를 지정한다. acme라는 단어를 그대로 넣으면 Caddy가 시작하지 않는다. Caddy가 공개적으로 신뢰되는 인증서를 받으므로 cctrace와 Claude Code는 CA 설정이 필요 없다. Codex가 server.ca_cert_file 없이 공인 인증서로 보내는지는 측정하지 않았다.
caddy_data 볼륨 유지
발급한 인증서와 내부 CA가 이 볼륨에 있다. 지우면 새 CA가 만들어지고 이전 CA를 신뢰하던 클라이언트는 모두 연결이 끊긴다.