Containers
이 문서는 source-repo가 deployable app container image를 어떻게 build, validate, publish하는지 설명합니다. Package artifact가 정상이어도 app image가 deployment 가능한 것은 아니므로, source repo는 package lifecycle과 별도로 container lifecycle을 관리합니다.
Container 기준은 CI/CD의 gate 구조와 Release Policy의 handoff 기준을 따릅니다. 이 문서는 그중 container image에 특화된 naming, platform, package 소비, BuildKit secret, local Compose 기준을 다룹니다.
1. 이 문서의 역할
1-1. Container 문서가 다루는 것
이 문서는 source repo 안에서 결정해야 하는 container 기준을 다룹니다.
| Topic | 이 문서에서 정하는 것 |
|---|---|
| Dockerfile 위치 | app별 Dockerfile 위치와 build context 기준 |
| Image repository | image repository 이름과 ownership |
| Platform | linux/amd64, linux/arm64 image variant 기준 |
| Package 소비 | image build가 platform-specific package를 어떻게 소비하는지 |
| Build secret | registry credential을 image layer에 남기지 않는 방식 |
| Local Compose | local demo container topology와 production 배포의 차이 |
| Release handoff | CD repo에 넘겨야 하는 image 정보 |
1-2. Container 문서가 다루지 않는 것
Source repo의 container 기준은 deployable image를 만들고 검증하는 데까지입니다. 실제 environment promotion은 CD repo 책임입니다.
이 문서는 아래를 정의하지 않습니다.
- Kubernetes Deployment, Service, Ingress manifest
- environment별 Helm/Kustomize overlay
- production secret injection 방식
- cluster rollout, rollback, promotion policy
- runtime autoscaling, node scheduling, ingress policy
이 경계를 두면 app code 변경과 cluster 운영 정책 변경이 한 PR에 섞이는 것을 줄일 수 있습니다.
2. Source Repo의 Container 책임
2-1. Package와 image는 다른 artifact다
Package artifact는 library consumer를 위한 산출물이고, container image는 app runtime을 위한 산출물입니다.
Package가 publish되었다고 해서 deployment가 가능한 것은 아닙니다. Container build에서만 확인되는 항목이 있습니다.
- runtime dependency
- startup command
- health/readiness endpoint
- non-root user와 filesystem layout
- app config default
- BuildKit secret 사용
- platform-specific native package 설치
- container image label, SBOM, provenance
그래서 package 변경도 관련 app image build까지 이어져야 합니다. Package job이 통과했더라도 app bootstrap이나 Dockerfile이 깨져 있으면 deployable artifact는 아직 준비되지 않은 상태입니다.
2-2. Dockerfile 위치와 build context
Dockerfile은 각 app 디렉터리에 둡니다. Build context는 항상 source-repo root를 사용합니다.
| App | Dockerfile | Build context |
|---|---|---|
patient-risk-web |
apps/patient-risk-web/Dockerfile |
source-repo root |
patient-risk-python-api |
apps/patient-risk-python-api/Dockerfile |
source-repo root |
risk-monitor-operator |
apps/risk-monitor-operator/Dockerfile |
source-repo root |
Build context를 root로 두는 이유는 app image가 app directory만이 아니라 workspace package, lockfile, shared contract, generated config를 함께 소비하기 때문입니다. Dockerfile이 app 디렉터리에 있더라도 image build는 repo boundary에서 재현되어야 합니다.
2-3. Local command와 CI command
Local과 CI는 같은 Make target을 사용합니다.
make container/build IMAGE_TAG=0.1.0
make container/push IMAGE_TAG=0.1.0
make container/build/patient-risk-web IMAGE_TAG=0.1.0
make container/build/patient-risk-python-api IMAGE_TAG=0.1.0
make container/build/risk-monitor-operator IMAGE_TAG=0.1.0
Pull request에서는 build 가능 여부만 검증합니다. main merge에서는 source revision이 보이는 dev image를 자동 push하고, root release tag에서는 container-release protected environment approval 뒤 release image를 push합니다.
3. Image Repository와 Tag
3-1. Image repository ownership
Container image 이름은 언어별 source repo CI workflow의 publish job이 결정합니다. 이름을 app directory나 CD repo에서 암묵적으로 추론하지 않는 이유는 image repository가 release contract이기 때문입니다.
| App | Image repository |
|---|---|
dev-guide-docs |
ghcr.io/tirosh-chain/guide/dev-guide-docs |
patient-risk-web |
ghcr.io/tirosh-chain/guide/patient-risk-web |
patient-risk-python-api |
ghcr.io/tirosh-chain/guide/patient-risk-python-api |
risk-monitor-operator |
ghcr.io/tirosh-chain/guide/risk-monitor-operator |
CD repo는 release 뒤 repository와 immutable tag 또는 digest를 받아 overlay에 반영합니다.
dev-guide-docs image는 docs portal, source-repo MkDocs site, cd-repo MkDocs site를 한 nginx static image 안에서 제공합니다.
3-2. Tag와 digest 기준
Tag는 사람이 읽기 좋고, digest는 불변성을 보장합니다.
| Identifier | 역할 |
|---|---|
| Human-readable tag | release note, manual inspection, troubleshooting에서 읽기 쉽습니다. |
| Immutable digest | CD repo promotion과 rollback에서 같은 image를 정확히 가리킵니다. |
이 샘플에서는 tag 흐름을 먼저 보여주지만, 실제 운영에서는 digest pinning을 CD repo 정책으로 추가하는 것이 좋습니다. Release handoff에는 tag만이 아니라 digest도 포함해야 합니다.
3-3. Dev tag와 release tag
Release tag는 root release version과 연결합니다.
ghcr.io/tirosh-chain/guide/patient-risk-python-api:v0.1.0
main merge나 manual dev build는 source commit이 보이는 tag를 사용합니다.
ghcr.io/tirosh-chain/guide/patient-risk-python-api:dev-37-g1a2b3c4d5e6f
Dev image는 moving tag와 trace tag를 함께 가집니다.
| Tag | 역할 |
|---|---|
dev |
항상 최신 main dev image를 가리킵니다. Preview, smoke, 빠른 수동 확인에 사용합니다. |
dev-<count>-g<sha> |
특정 source revision을 다시 찾기 위한 trace tag입니다. |
sha-<sha> |
GitHub Actions/Docker metadata 기준 source SHA tag입니다. |
Dev tag는 production promotion 대상이 아니라 troubleshooting과 preview 확인용입니다.
4. Image Platform 기준
4-1. Docker platform을 release contract로 둔다
Container image는 Docker platform을 release contract로 사용합니다. 기본 release target은 linux/amd64, linux/arm64입니다.
docker buildx build --platform linux/amd64,linux/arm64 ...
Multi-platform image publish는 Docker manifest list를 통해 같은 image tag 아래 platform별 image variant를 묶습니다. CD repo는 가능하면 tag보다 digest를 사용하고, release handoff에는 어떤 platform image를 검증했는지 함께 남깁니다.
4-2. Platform별 package artifact 매핑
Container build가 platform-specific package를 소비할 때는 Docker platform과 package artifact platform이 맞아야 합니다.
| Docker platform | Required package artifact |
|---|---|
linux/amd64 |
manylinux_*_x86_64.whl, Conan arch=x86_64 package |
linux/arm64 |
manylinux_*_aarch64.whl, Conan arch=armv8 package |
Mac에서 만든 macosx_* wheel은 local Python smoke에는 사용할 수 있지만 Linux container image에는 설치하지 않습니다. Local Compose는 developer workstation의 OS가 아니라 Docker daemon의 Linux platform을 기준으로 app image를 만듭니다.
4-3. Buildx와 DinD 기준
Docker-in-Docker는 multi-platform의 표준 인터페이스가 아닙니다. CI에서는 BuildKit/buildx builder를 사용하고, 필요하면 QEMU emulation, native arm64/amd64 runner, 또는 remote builder를 선택합니다.
어떤 방식을 쓰더라도 workflow contract는 아래처럼 표현합니다.
image build
-> linux/amd64 image variant
-> linux/arm64 image variant
-> manifest list
-> repository tag and digest
Implementation detail은 runner 환경마다 달라질 수 있지만, release artifact contract는 --platform linux/amd64,linux/arm64와 platform별 package artifact 소비입니다.
5. Package 변경과 Container Build
5-1. Package 변경은 app image 검증으로 이어진다
Container workflow는 app 파일만 바뀔 때만 돌면 부족합니다. App이 package를 소비하기 때문에 package 변경도 관련 app image build로 이어져야 합니다.
| Changed area | Expected container validation |
|---|---|
packages/patient-monitor-ui-core |
patient-risk-web image build |
packages/patient-monitor-react-components |
patient-risk-web image build |
packages/patient-risk-python-core |
patient-risk-python-api image build |
packages/patient-signal-python-rust |
patient-risk-python-api image build with Rust Python wheel |
packages/patient-signal-rust-core |
patient-risk-python-api image build with Rust Python wheel |
packages/patient-risk-go-core |
risk-monitor-operator image build |
apps/* |
해당 app image build |
contracts/, fixtures/ |
demo smoke와 관련 app validation |
이 기준은 path filter를 쓸 때도 유지해야 합니다. 문서 변경만으로 container build가 돌 필요는 없지만, package 변경을 app과 무관하다고 보면 실제 deployment boundary에서 깨진 artifact를 늦게 발견하게 됩니다.
5-2. Platform-specific runtime artifact는 image build에서 검증한다
Rust Python package는 image build 전에 package job에서 먼저 검증합니다. Multi-platform image publish에서는 Docker build stage가 각 Linux platform용 wheel을 만들고 runtime image에 설치합니다.
Rust core test
-> Rust Python wheel package validation
-> API app validation
-> API image build per platform
이 순서를 지키면 patient-risk-python-api image build가 실패했을 때 원인이 wheel build인지, wheel install인지, app startup인지 더 빨리 구분할 수 있습니다.
5-3. Image build가 확인하는 것
Container build는 package test가 확인하지 않는 runtime 조합을 봅니다.
| Check | 예시 |
|---|---|
| Dependency install | lockfile, Nexus credential, platform wheel install |
| Entrypoint | CMD, startup module, web server command |
| Runtime config | default env, required env, port |
| Health endpoint | /readyz, /healthz, container healthcheck |
| Filesystem | non-root user, writable directory, copied artifact |
| Supply chain | image label, SBOM, provenance |
Image build는 business rule test를 반복하는 자리가 아닙니다. Business rule은 package test에서 먼저 검증하고, image build는 runtime packaging과 startup boundary를 확인합니다.
6. BuildKit Secret
6-1. Credential은 image layer에 남기지 않는다
Container build가 private package registry에서 dependency를 받아야 할 때는 Dockerfile ARG나 image layer에 credential을 남기지 않습니다. BuildKit secret을 사용하고, workflow에서는 composite action이 secret file을 주입합니다.
RUN --mount=type=secret,id=uv_index_username ...
RUN --mount=type=secret,id=uv_index_password ...
Secret은 build 중에만 존재해야 하고 final image filesystem에 남으면 안 됩니다. 이 기준은 npm, uv, Cargo, Conan 모두 동일합니다.
6-2. Build arg와 secret을 구분한다
Build arg는 secret이 아닌 build-time configuration에만 사용합니다.
| 값 | 전달 방식 |
|---|---|
| image version, source revision | build arg 또는 label |
| public API base path | build arg |
| Nexus username/password | BuildKit secret |
| npm token, uv password, Cargo token | BuildKit secret 또는 generated config |
Build arg는 image metadata나 layer history에 노출될 수 있다고 가정합니다. Credential은 build arg로 넘기지 않습니다.
7. Local Compose 기준
7-1. Compose demo의 목적
compose.demo.yml은 production deployment manifest가 아니라 local demo topology입니다. 목적은 API, Web, PostgreSQL, Redis가 local Docker runtime에서 함께 동작하는지 확인하는 것입니다.
make demo/run
make demo/logs
make demo/stop
make demo/reset
Compose demo는 아래 흐름을 검증합니다.
PostgreSQL + Redis
-> patient-risk-python-api container
-> Alembic migration inside API container
-> patient-risk-web container
-> signal-device-simulator posts low-spo2-trend packet
7-2. Local Rust Python wheel 기준
patient-risk-python-api local Compose image는 local-runtime build target을 사용합니다. 이 target은 Docker build stage 안에서 현재 Docker platform용 Linux wheel을 만들고 API image에 설치합니다.
이 기준이 필요한 이유는 developer workstation에서 만든 wheel이 container platform과 다를 수 있기 때문입니다. 예를 들어 macOS arm64에서 만든 macosx_*_arm64.whl은 Linux container에 설치할 수 없습니다. Compose는 local machine OS가 아니라 Docker daemon의 Linux platform을 기준으로 봅니다.
Release image도 multi-platform publish 경로에서는 Docker build stage 안에서 platform별 Linux wheel을 만들고 API image에 설치합니다. 별도의 Rust Python wheel package publish는 package 소비자를 위한 artifact 경로로 유지합니다.
7-3. Local storage container
make storage/up은 production deployment manifest가 아니라 local demo용 storage topology입니다. PostgreSQL과 Redis container를 직접 띄우고, make demo/check/storage에서 Python API가 PATIENT_RISK_STORAGE_BACKEND=postgres-redis 경로를 타는지 확인합니다.
make storage/up
make storage/check
make storage/down
Source repo의 local storage target은 app image 배포 방식을 정의하지 않습니다. App image, registry, Kubernetes manifest, secret injection은 CI/CD와 CD repo 책임입니다. 여기서는 repository adapter가 실제 datastore와 연결되는 최소 smoke만 검증합니다.
make demo/stop은 container만 내리고 Compose volume은 남깁니다. Local database schema를 처음부터 다시 만들고 싶거나, migration 적용 전 실패로 부분 schema가 남은 경우에는 make demo/reset으로 volume까지 제거합니다.
8. Publish와 Release Handoff
8-1. Publish gate
CI에서는 pull request에서 build만 검증하고, main merge에서 dev image를 자동 push합니다. Root release tag 또는 수동 release workflow에서는 container-release protected environment approval 뒤 release image를 push합니다.
| Trigger | Publish 기준 |
|---|---|
| Pull request | build/test only, push 없음 |
main push |
linux/amd64, linux/arm64 dev image를 dev, dev-<count>-g<sha>, sha-<sha> tag로 push |
v* release tag |
approval 뒤 linux/amd64, linux/arm64 release image push |
| Manual workflow | 목적에 따라 dev 또는 release publish를 명시적으로 선택 |
Publish job은 아래 evidence를 생성합니다.
- image tag
- image digest
- BuildKit provenance
- SBOM attestation
- source revision label
- platform variant 정보
8-2. Dev image retention
Dev image는 계속 쌓이지 않도록 retention workflow로 정리합니다. dev moving tag는 항상 최신 main image를 가리키고, release tag는 보존합니다. 오래된 dev trace image만 삭제합니다.
일반적인 registry 운영 패턴은 release image와 dev image의 lifecycle을 분리하는 것입니다. Release image는 rollback, audit, release evidence에 연결되므로 보존하고, dev image는 최신 검증과 troubleshooting에 필요한 범위만 남깁니다.
| Pattern | 기준 |
|---|---|
| Release는 삭제하지 않습니다. | vX.Y.Z tag와 digest는 CD rollback과 release evidence 기준입니다. |
| Moving tag는 보존합니다. | dev는 최신 main image, latest는 최신 release convenience tag로 사용합니다. |
| Trace tag는 제한합니다. | dev-<count>-g<sha>와 sha-<sha>는 원인 추적용이므로 최신 N개만 남깁니다. |
| Untagged version은 정리합니다. | manifest 교체나 tag 이동 뒤 남은 version은 운영 가치가 낮습니다. |
| Image version | Retention policy |
|---|---|
vX.Y.Z release tag |
보존합니다. Release evidence와 rollback 기준입니다. |
latest |
보존합니다. Release convenience tag로 봅니다. |
dev |
보존합니다. 최신 main dev image를 가리키는 moving tag입니다. |
dev-<count>-g<sha> |
최신 10개 non-release version만 보존합니다. |
sha-<sha> |
최신 10개 non-release version 안에 포함될 때만 보존합니다. |
| untagged version | 최신 10개 non-release version 안에 포함되지 않으면 삭제 대상입니다. |
Retention은 .github/workflows/source-repo-ghcr-retention.yml에서 수행합니다. 기본 정책은 image repository별로 non-release version 최신 10개를 남기고 나머지를 삭제합니다.
| Operation | 기준 |
|---|---|
| 자동 실행 | 매일 schedule로 실행합니다. GitHub cron은 UTC 기준입니다. |
| 수동 실행 | workflow_dispatch로 실행합니다. |
| 사전 확인 | dry-run=true로 삭제 대상을 먼저 확인합니다. |
| 보존 개수 변경 | keep-dev-versions input으로 image별 non-release version 보존 개수를 조정합니다. |
이 workflow는 GitHub Packages REST API로 package version을 조회하고 삭제합니다. 따라서 workflow token은 package에 대한 삭제 권한을 가져야 하며, package가 repository와 연결되어 있지 않거나 repository가 package admin role을 갖지 못하면 삭제가 실패할 수 있습니다. 그런 경우에는 GHCR package access 설정에서 이 repository에 admin 권한을 부여합니다.
| Reference | 용도 |
|---|---|
| GitHub Packages REST API | package version 조회와 삭제 endpoint 기준 |
| Deleting and restoring packages | package version 삭제 권한과 package admin 조건 |
| actions/delete-package-versions | GitHub 공식 cleanup action의 retention option 참고 |
8-3. CD repo에 넘기는 정보
CD repo는 release 뒤 repository와 immutable tag 또는 digest를 받아 overlay에 반영합니다.
Release handoff에는 아래를 포함합니다.
| Field | 예시 |
|---|---|
| Image repository | ghcr.io/tirosh-chain/guide/patient-risk-python-api |
| Tag | v0.1.0 |
| Digest | sha256:... |
| Platforms | linux/amd64, linux/arm64 |
| Runtime port | 8080 |
| Readiness endpoint | /readyz |
| Migration 필요 여부 | yes/no |
CD repo는 이 정보를 받아 environment별 manifest를 갱신합니다. Source repo는 manifest를 직접 수정하지 않습니다.