On this page
- First, I identified what had actually changed
- Make it safe immediately, even if the workflow is temporary
- I improved durability with a bind mount and git
- I did not put GitHub credentials on the server
- Turn commit history into the project timeline
- Lessons learned
- 한국어 버전
- 컨테이너 안에서 자란 코드를 잃어버리지 않는 법
- 먼저 무엇이 진짜 변경인지 확인했다
- 임시방편이라도 즉시 잃지 않게 만들기
- bind mount와 git으로 내구성을 올렸다
- 서버에 GitHub 자격증명을 두지 않았다
- 커밋 히스토리를 프로젝트 타임라인으로 만들기
- 남은 교훈
For a while, I was editing code directly inside a running container.
That was not the original plan. I needed a fast loop for fixing the agent runtime, running it immediately, and then changing it again. The problem was that the code only existed in the writable layer on top of the container image. There was no bind mount on the host, and there was no git repository. If the container was recreated, every change I had made could disappear.
Looking back, it was a risky way to work. At the time, though, moving the feature forward felt more urgent. Even while I was working on the Planner layer, I kept feeling the same uneasiness: if this container goes away, the work goes with it.
First, I identified what had actually changed
The first thing I did was not a backup. It was an audit of the changes.
A container does not only contain code I wrote by hand. It also accumulates runtime caches, dependency directories, lock files, temporary test files, bytecode, and other incidental artifacts. If I copied all of that, I would later have a hard time separating real assets from noise.
So I used docker diff to inspect every file that differed from the base image.
The result was clearer than I expected. The only changes that really needed to be preserved were the Planner work and one existing adapter modification. Most of the rest was reproducible junk. Dependency directories, caches, empty test files, and similar artifacts were intentionally excluded from the backup set.
That step mattered. When you are nervous, “just copy everything” is tempting. Over time, though, it makes the recovery state harder to understand. Before backing anything up, I needed to decide what actually counted as an asset.
Make it safe immediately, even if the workflow is temporary
The next step was simple. At the end of each unit of work, I copied the code out of the container.
I used docker cp to copy the needed files into the host-side repository structure, then committed them to git on the host. It was not a perfect setup. I was still developing inside the container and extracting the result afterward. But at least I was no longer in a state where one container recreation could erase every change.
That temporary backup workflow worked better than expected. It required no complex infrastructure change, and it was enough for the immediate goal: do not lose today’s work.
When you are repairing a live system, the first safety rail can matter more than the ideal final architecture. Especially when work is already happening in a risky state, it is often better to reduce the chance of loss first instead of spending more time trying to design the perfect structure up front.
I improved durability with a bind mount and git
After that, I changed the setup so the source directory was bind-mounted from the host. Code that previously existed only in a specific working directory inside the container became owned by a git repository on the host filesystem.
Before switching over, I verified that the code in the running container and the code prepared on the host were byte-identical. This was not a casual “these files look about the same” check. I compared hashes to confirm identity. That kind of verification is important when moving a live codebase. A one-line difference can turn into a long debugging session.
After the switch, the code survives container recreation. The container is now the execution environment, and the host git repository is the source of truth. That distinction matters.
A container writable layer is not a repository. It is closer to a temporary workspace. Code can grow there, but if it stays there long enough, it can eventually disappear during an operational task.
I did not put GitHub credentials on the server
I also needed an offsite backup. A host-side git repository is still not enough if the server itself disappears. So I pushed the work to a private GitHub repository.
However, I decided not to put GitHub credentials on the server. The server does not push directly to GitHub. Instead, my local machine pulls the repository from the server over SSH, then pushes to GitHub using credentials that already live on the local machine. Early on, I also used git bundles to move the repository around.
The core principle was simple.
Credentials should live only on the machines that already manage them safely.
Putting more privileges on an operations server is convenient. But convenience was not worth increasing the blast radius if the server were compromised. The server runs and stores the code. The local development environment owns the authority to write to the external repository.
Turn commit history into the project timeline
After that, I started committing work in meaningful units. Planner, Shadow Executor, Harness, Read-Only Executor, and the evaluation framework each got their own commits as the project progressed.
That helped with more than backup. Later, when I reviewed the project, the history itself became a timeline. The commit log made it possible to trace which features landed in what order and when the structure changed.
The code started in a hurry inside a container, but eventually it took the shape of a real repository.
Lessons learned
This work left me with three lessons.
First, a container writable layer is not a repository. You can edit code inside a running container, but you should not let that state last for long.
Second, use docker diff before backing things up so you can separate real changes from junk. If you copy everything just because you are anxious, recoverable assets and reproducible byproducts get mixed together.
Third, durability does not have to be solved all at once. First, use docker cp so the code cannot disappear immediately. Then fix the structure with a bind mount and git. Finally, add an offsite backup.
The important part was the order.
Even if I could not build the perfect final structure from the start, the first job was obvious: make sure the code that could disappear today would not disappear today. After that, I could improve the structure.
한국어 버전
컨테이너 안에서 자란 코드를 잃어버리지 않는 법
한동안 나는 운영 중인 컨테이너 안에서 코드를 직접 고치고 있었다.
처음부터 그렇게 하려던 것은 아니었다. 에이전트 런타임을 빠르게 고치고, 바로 실행해보고, 다시 수정하는 흐름이 필요했다. 문제는 그 코드가 컨테이너 이미지 위의 writable layer에만 존재했다는 점이다. 호스트에 bind mount도 없었고, git 저장소도 없었다. 컨테이너를 재생성하는 순간 그동안의 수정이 전부 사라질 수 있는 상태였다.
돌이켜보면 꽤 위험한 방식이었다. 하지만 당시에는 기능을 앞으로 밀어내는 일이 더 급했다. Planner 쪽 작업을 진행하면서도 “이 컨테이너가 날아가면 끝이다”라는 찜찜함이 계속 남아 있었다.
먼저 무엇이 진짜 변경인지 확인했다
가장 먼저 한 일은 백업이 아니었다. 변경분 실사였다.
컨테이너 안에는 내가 직접 쓴 코드만 있는 것이 아니다. 실행 중에 생긴 캐시, 의존성 디렉터리, lock 파일, 임시 테스트 파일, 바이트코드 같은 것들이 같이 섞인다. 이것들을 전부 백업하면, 나중에 무엇이 자산이고 무엇이 노이즈인지 알기 어려워진다.
그래서 docker diff로 베이스 이미지와 달라진 파일을 전수 확인했다.
결과는 예상보다 명확했다. 실제로 보존해야 할 변경은 Planner 관련 작업과 기존 어댑터 수정 하나 정도였다. 나머지는 대부분 재생성 가능한 정크였다. 의존성 디렉터리, 캐시, 비어 있는 테스트 파일 같은 것들은 백업 대상에서 의도적으로 제외했다.
이 단계가 중요했다. “일단 다 복사하자”는 방식은 불안할 때 매력적이지만, 장기적으로는 복구 가능한 상태를 더 흐리게 만든다. 백업 전에 먼저 자산 목록을 확정해야 했다.
임시방편이라도 즉시 잃지 않게 만들기
다음 단계는 단순했다. 작업 단위가 끝날 때마다 컨테이너 밖으로 코드를 꺼냈다.
docker cp로 필요한 파일을 호스트의 저장소 구조에 맞춰 복사하고, 호스트 쪽 git에 커밋했다. 완벽한 구조는 아니었다. 여전히 컨테이너 안에서 개발하고, 작업이 끝나면 밖으로 꺼내는 방식이었다. 하지만 적어도 컨테이너 재생성 한 번으로 모든 변경이 사라지는 상태에서는 벗어났다.
이 임시 백업 워크플로는 생각보다 효과가 있었다. 복잡한 인프라 변경 없이 바로 적용할 수 있었고, “지금 당장 안 잃는 것”이라는 목적에는 충분했다.
운영 중인 시스템을 고칠 때는 이상적인 최종 상태보다 첫 번째 안전장치가 더 중요할 때가 있다. 특히 이미 위험한 상태에서 작업이 진행 중이라면, 처음부터 완벽한 구조를 만들려고 시간을 쓰기보다 손실 가능성을 먼저 낮춰야 한다.
bind mount와 git으로 내구성을 올렸다
그 다음에는 소스 디렉터리를 호스트 경로에 bind mount하는 구조로 바꿨다. 컨테이너 안의 특정 작업 디렉터리에만 존재하던 코드를 호스트 파일시스템의 git 저장소가 소유하도록 만든 것이다.
전환 전에는 실행 중인 컨테이너의 코드와 호스트에 준비한 코드가 byte-identical인지 검증했다. 단순히 “대충 같은 파일이 있다”가 아니라 해시를 비교해 동일성을 확인했다. 운영 중인 코드베이스를 옮기는 작업에서는 이런 확인이 필요하다. 한 줄 차이 때문에 디버깅 시간이 길어질 수 있기 때문이다.
전환 후에는 컨테이너를 재생성해도 코드가 남는다. 컨테이너는 실행 환경이고, 소스의 원본은 호스트 git 저장소가 되었다. 이 차이가 크다.
컨테이너 writable layer는 저장소가 아니다. 임시 작업 공간에 가깝다. 거기서 코드가 자랄 수는 있지만, 계속 거기에만 두면 언젠가 운영 작업 하나로 사라질 수 있다.
서버에 GitHub 자격증명을 두지 않았다
오프사이트 백업도 필요했다. 호스트에 git이 있어도 서버 자체가 사라지면 의미가 없기 때문이다. 그래서 프라이빗 GitHub 저장소로 푸시했다.
다만 서버에는 GitHub 자격증명을 두지 않기로 했다. 서버가 직접 GitHub에 푸시하지 않는다. 대신 내 로컬 머신이 서버의 저장소를 SSH로 가져오고, 로컬 머신이 가진 기존 자격증명으로 GitHub에 푸시한다. 초기에는 git bundle을 만들어 옮기는 방식도 사용했다.
핵심 원칙은 간단했다.
자격증명은 이미 그것을 안전하게 관리하고 있는 머신에만 둔다.
운영 서버에 더 많은 권한을 얹으면 편해진다. 하지만 편의 때문에 서버가 탈취됐을 때의 피해 범위를 넓힐 필요는 없었다. 서버는 코드를 실행하고 보관한다. 외부 저장소에 쓰는 권한은 로컬 개발 환경이 맡는다.
커밋 히스토리를 프로젝트 타임라인으로 만들기
이후에는 작업을 의미 단위로 커밋했다. Planner, Shadow Executor, Harness, Read-Only Executor, 평가 프레임워크처럼 단계마다 커밋을 남겼다.
이 방식은 단순한 백업 이상으로 도움이 됐다. 나중에 프로젝트를 돌아볼 때 히스토리 자체가 타임라인이 된다. 어떤 기능이 어떤 순서로 들어왔고, 어느 시점에 구조가 바뀌었는지 커밋 로그만 봐도 대략 추적할 수 있다.
컨테이너 안에서 급하게 시작한 코드였지만, 결국에는 저장소로서의 형태를 갖추게 된 셈이다.
남은 교훈
이번 작업에서 얻은 교훈은 세 가지다.
첫째, 컨테이너 writable layer는 저장소가 아니다. 운영 중인 컨테이너 안에서 코드를 고칠 수는 있지만, 그 상태를 오래 유지하면 안 된다.
둘째, 백업 전에 docker diff로 정크와 진짜 변경을 분리해야 한다. 불안하다고 전부 복사하면 복구 가능한 자산과 재생성 가능한 부산물이 뒤섞인다.
셋째, 내구성은 한 번에 완성하지 않아도 된다. 먼저 docker cp로 당장 잃지 않게 만들고, 그 다음 bind mount와 git으로 구조를 바로잡고, 마지막으로 오프사이트 백업까지 올리면 된다.
중요한 것은 순서였다.
완벽한 최종 구조를 처음부터 만들지 못했더라도, 가장 먼저 해야 할 일은 명확했다. 지금 사라질 수 있는 코드를 오늘 안에 사라지지 않게 만드는 것. 그 다음에 구조를 개선하면 된다.