-
Docker 없이 격리하기 — 단일 로컬 서버에서 사고 반경을 줄이는 네 가지 경량 수단IT 2026. 7. 27. 23:00
집에 AI 개발용 서버 한 대를 두고 쓰고 있다. 이 서버 위에서는 자동화 에이전트(사람이 시키지 않아도 코드를 읽고 고치고 커밋하는 LLM 기반 프로그램)가 돌고, 동시에 챗봇 API, 문서 브라우저, 사진 분석 배치 같은 서비스가 여러 개 떠 있다. 프로젝트로 치면 열 개가 넘는다.
이렇게 한 대에 여러 작업을 몰아넣으면 곧바로 부딪히는 문제가 있다. 작업끼리 서로 간섭한다. 한 작업이 다른 작업이 쓰던 파일을 덮어쓰고, 한 서비스가 실수로 외부에 포트를 열어 두고, 한 배치가 메모리를 다 먹어 시스템 전체가 멈춘다. 그리고 무엇보다 — 에이전트가 잘못된 경로에
rm을 날리면 그 피해가 어디까지 번질지 알 수 없다.이 글은 그 상황에서 Docker나 Kubernetes 같은 컨테이너 오케스트레이션을 도입하지 않고, 더 가벼운 수단들을 조합해 위험을 줄인 경험을 정리한 것이다. 결론부터 말하면 — 컨테이너 수준의 완전 격리는 아니지만, 운영 부담 없이 "사고 반경"만 효과적으로 좁힐 수 있었다.
왜 컨테이너를 안 쓰기로 했나
먼저 솔직히 짚자. Docker(애플리케이션을 의존성째 격리된 컨테이너로 묶어 실행하는 도구)는 격리 문제의 정석 답이다. 컨테이너는 커널 네임스페이스(namespace, 리눅스 커널이 프로세스에게 파일시스템·네트워크·프로세스 목록 같은 자원을 따로 보여 주는 격리 메커니즘)로 작업마다 독립된 세계를 만들어 준다. 한 컨테이너 안에서 무슨 짓을 해도 바깥은 멀쩡하다.
그런데 집 서버에 컨테이너를 까는 건 다른 이야기다. 이미지를 빌드하고, 볼륨을 마운트하고, 네트워크 브리지를 잡고, 베이스 이미지를 갱신하고, 호스트의 GPU를 컨테이너에 넘기는 설정까지 — 이 모든 운영 부담이 따라온다. 작업 한 번 돌리려고 매번 이미지를 다시 빌드하는 마찰은, 혼자 쓰는 서버에서는 격리가 주는 안전보다 비용이 크다.
핵심은 이 질문이다. 나에게 정말 필요한 건 "완벽한 격리"가 아니라 "사고 반경 축소"다. 한 작업의 실수가 다른 작업으로 번지지 않게만 막으면 된다. 그렇다면 컨테이너의 무거운 신뢰 경계 전부를 가져올 게 아니라, 위험의 종류별로 가벼운 수단을 하나씩 붙이는 편이 낫다.
간섭은 한 종류가 아니다 — 위험을 네 갈래로 나누기
"격리하자"는 말은 너무 뭉뚱그려져 있다. 실제로 한 서버에서 일어나는 간섭은 종류가 다르고, 막는 방법도 다르다. 내가 마주친 간섭을 네 갈래로 나눠 봤다.
다이어그램 설명. 단일 서버에서 발생하는 간섭을 네 종류로 분해한 그림이다. 위에서 갈라져 나오는 각 가지가 서로 다른 위험을 나타낸다 — 메모리를 한 작업이 다 먹어 버리는 고갈, 같은 파일을 동시에 고치는 충돌, 한 작업이 자기 일과 무관한 곳까지 손대는 권한 과잉, 그리고 의도치 않은 외부 노출이다. 핵심은 이 넷이 서로 다른 문제이고 한 가지 수단으로 다 막히지 않는다는 점이다. 컨테이너는 이 넷을 한꺼번에 막아 주지만, 경량 전략은 각 위험에 맞는 도구를 따로 붙인다. 그래서 먼저 위험을 이렇게 분류하는 일 자체가 출발점이 된다 — 분류하지 않으면 "격리했다"고 믿으면서 실은 한 종류만 막고 나머지는 뚫려 있기 쉽다.
수단 1 — 프로세스 자원 한도로 시스템 마비 막기
첫 번째 간섭은 자원 고갈이다. 한 작업이 메모리를 욕심껏 잡으면 시스템 전체가 느려지거나, 최악의 경우 커널이 OOM(Out Of Memory, 메모리가 바닥나 커널이 임의의 프로세스를 강제 종료하는 상황)으로 엉뚱한 프로세스를 죽인다. 통합 메모리를 쓰는 서버에서는 이 위험이 특히 크다 — CPU 작업과 GPU 작업이 같은 메모리 풀을 나눠 쓰기 때문에, 한쪽이 다 먹으면 다른 쪽까지 마비된다.
여기서 쓴 수단이
systemd-run이다. systemd-run은 임의의 명령을 일회성 systemd 단위(unit)로 감싸 실행하면서, 그 명령에 자원 한도를 거는 도구다. 메모리 상한·CPU 우선순위 같은 제약을 명령 하나에 입혀 둘 수 있다.# 무거운 배치 작업을 자원 한도 안에 가둬 실행한다 systemd-run --user --scope \ -p MemoryMax=16G \ # 이 작업이 쓸 수 있는 메모리 상한 — 넘으면 이 작업만 종료 -p CPUWeight=20 \ # CPU 경쟁 시 우선순위를 낮게 (기본 100, 낮을수록 양보) python3 heavy_batch.py코드 설명. 무거운 배치 작업 하나를 자원 우리(cage) 안에 넣어 실행하는 명령이다. 메모리 상한을 16기가로 걸어 두면, 이 작업이 그 선을 넘는 순간 시스템 전체가 아니라 이 작업만 종료된다 — 피해가 한 작업 안에 갇힌다. CPU 우선순위를 낮게 주면 다른 작업과 CPU를 다툴 때 이 배치가 양보한다. 이 패턴이 막는 건 "한 작업의 폭주가 시스템 전체를 끌어내리는" 시나리오다. 이게 왜 중요하냐면 — 격리의 목적이 결국 "한 작업의 사고가 다른 작업으로 번지지 않게" 하는 것인데, 자원 고갈은 파일이나 네트워크를 건드리지 않고도 그 번짐을 일으키는 통로이기 때문이다. 함정은 메모리 상한을 너무 빡빡하게 잡으면 정상 작업도 죽는다는 점 — 실제 사용량을 한 번 재 보고 여유를 둬서 잡아야 한다.
수단 2 — 작업별 독립 작업 디렉토리로 파일 간섭 막기
가장 흔한 간섭은 파일 충돌이다. 에이전트 두 개가 같은 코드베이스에서 동시에 일하면, 한쪽이 고치는 중인 파일을 다른 쪽이 다시 고쳐서 서로의 변경을 덮어쓴다.
여기서 쓴 수단이 git worktree다. git worktree는 하나의 git 저장소를 여러 개의 독립된 작업 디렉토리로 펼쳐 주는 기능이다 — 같은 저장소 히스토리를 공유하되, 각 디렉토리는 자기만의 브랜치와 자기만의 파일 상태를 가진다. 작업마다 worktree를 따로 떼어 주면, 한 작업이 자기 디렉토리 안에서 무슨 파일을 건드려도 다른 작업의 디렉토리에는 닿지 않는다.
다이어그램 설명. 하나의 git 저장소가 두 개의 독립 작업 디렉토리로 펼쳐진 모습이다. 위의 저장소가 아래로 두 갈래 나뉘고, 각 갈래는 자기 브랜치를 가진 별도 디렉토리가 된다. 작업 A가 자기 디렉토리 안에서 파일을 고쳐도 작업 B의 디렉토리에는 그 변경이 보이지 않는다(점선이 둘 사이가 끊겨 있음을 표시). 이게 왜 좋냐면 — 디렉토리 복사(clone)를 새로 뜨지 않고도 파일 격리를 얻기 때문이다. 저장소 히스토리는 한 벌만 두고 작업 공간만 늘리니, 디스크도 절약되고 브랜치 간 이동도 빠르다.
수단 3 — 디렉토리 스코프로 권한 과잉 막기
다음 간섭은 권한 과잉이다. 자동화 에이전트는 셸 명령을 직접 실행한다. 의도는 자기 프로젝트 디렉토리만 건드리는 것이지만, 경로 하나 잘못 짚으면 무관한 곳까지 손이 간다. 가장 무서운 건 되돌릴 수 없는 명령 —
rm -rf가 엉뚱한 경로를 향하는 경우다.여기서 쓴 수단은 명령 실행 직전에 가로채는 훅(hook, 특정 이벤트가 일어날 때 자동으로 끼어들어 실행되는 코드)이다. 에이전트가 셸 명령을 실행하기 직전에 그 명령을 검사해서, 위험 패턴이면 막고 작업 범위 밖 경로면 경고한다. "무엇이든 허용하되 정해진 위험 패턴만 차단"하는 블랙리스트 방식으로, 평소엔 마찰 없이 진행하되 비가역적 명령에서만 멈춘다.
다이어그램 설명. 에이전트가 셸 명령을 실행하기 직전에 거치는 검문 흐름이다. 맨 위의 실행하려는 명령이 가운데 판단 지점으로 들어가면, 세 갈래로 갈린다 — 평범한 명령은 그대로 통과하고, 시스템을 파괴하는 되돌릴 수 없는 명령은 즉시 막히며, 작업 범위 밖 경로를 건드리는 명령은 사람에게 확인을 묻는다. 이 설계의 핵심은 "기본은 통과, 예외만 차단"이다 — 모든 명령을 일일이 승인받게 하면 자동화의 의미가 없으니, 비가역성이 높고 영향 범위가 넓은 것만 골라 막는다. 위험도를 "되돌릴 수 있는가 × 영향 범위가 넓은가"로 따져, 둘 다 높으면 차단, 범위만 좁으면 확인, 낮으면 통과로 나눈다. 함정은 이게 커널 수준 차단이 아니라 명령 문자열 검사라는 점이다. 패턴을 우회하는 변형(예: 변수에 담아 우회)을 잡으려면 검사 규칙을 이중으로 둬야 하고, 그래도 완벽하진 않다 — 진짜 신뢰 경계가 필요하면 결국 컨테이너로 가야 한다.
수단 4 — 루프백 바인드로 네트워크 노출 줄이기
마지막 간섭은 의도치 않은 외부 노출이다. 서비스를 띄울 때 흔히
0.0.0.0(모든 네트워크 인터페이스에서 들어오는 연결을 받겠다는 주소)에 바인드한다. 그러면 같은 네트워크의 다른 기기에서도 그 서비스에 접속할 수 있다. 챗봇 API나 문서 편집기처럼 나만 쓰는 내부 서비스가 이렇게 열려 있으면, 인증이 허술한 틈을 누가 찔러도 모른다.해법은 단순하다. 외부에서 접근할 이유가 없는 서비스는
127.0.0.1(루프백 주소 — 자기 자신에게만 연결되는 가상 네트워크 인터페이스)에 바인드한다. 그러면 그 서비스는 같은 서버 안의 프로세스에서만 닿을 수 있고, 바깥 네트워크에서는 아예 보이지 않는다.# 잘못된 예 — 모든 인터페이스에 노출 # 같은 네트워크의 어떤 기기든 8092 포트로 들어올 수 있다 app.run(host="0.0.0.0", port=8092) # 올바른 예 — 자기 자신에게만 노출 # 외부 네트워크에서는 이 서비스가 보이지 않는다 # 원격 접근이 필요하면 그 앞에 인증 프록시(예: tailscale)를 한 겹 둔다 app.run(host="127.0.0.1", port=8092)코드 설명. 서비스가 어느 네트워크 인터페이스에서 연결을 받을지 정하는 한 줄의 차이를 보여 준다. 위쪽처럼 모든 인터페이스를 가리키는 주소에 묶으면 같은 공유기에 붙은 다른 기기에서도 포트로 들어올 수 있고, 아래처럼 자기 자신만 가리키는 루프백 주소에 묶으면 같은 서버 내부 프로세스만 접근할 수 있다. 이 패턴의 핵심은 "기본을 닫힘으로 두고, 열어야 할 때만 명시적으로 한 겹 더 얹는다"는 것이다. 원격에서 정말 써야 하는 서비스는 루프백에 묶어 둔 채 그 앞에 인증을 거치는 프록시를 따로 세운다 — 노출 결정을 서비스 코드가 아니라 별도 계층에서 통제하니, 어떤 서비스가 외부에 열려 있는지 한곳에서 파악된다.
네 수단을 겹쳐 쌓기 — 경량 격리 스택
지금까지의 네 수단은 각자 다른 위험을 막는다. 이걸 한데 쌓으면 하나의 방어 계층이 된다. 어느 한 겹이 막지 못하는 위험을 다른 겹이 받는 구조다.
다이어그램 설명. 네 가지 경량 수단을 위에서 아래로 통과시키며 쌓은 방어 계층이다. 하나의 작업이 위에서 시작해 자원·파일·권한·네트워크 계층을 차례로 지나면서, 각 계층이 자기가 맡은 위험을 한 겹씩 걷어 내고, 맨 아래에 도달했을 때는 사고가 번질 통로가 거의 닫혀 있다. 이 구조의 가치는 한 수단의 한계를 다른 수단이 메운다는 데 있다 — 파일 격리는 자원 고갈을 못 막고, 자원 한도는 권한 과잉을 못 막지만, 겹쳐 두면 각자의 빈틈이 서로의 강점으로 덮인다. 이걸 보안에서 흔히 "심층 방어(defense in depth)"라 부른다. 함정은 이 스택이 독립된 네 겹이라는 것이다 — 컨테이너처럼 하나의 경계로 전부를 감싸는 게 아니라, 각 겹을 따로 설정하고 따로 관리해야 한다. 한 겹을 빠뜨리면 그 종류의 위험은 그대로 뚫린다.
컨테이너 격리와 무엇이 다른가
그래서 이 경량 스택은 컨테이너와 어떻게 다른가. 가장 중요한 차이는 신뢰 경계의 강도다. 둘을 나란히 놓고 보자.
▲ 컨테이너 격리 — 하나의 강한 경계, 그 대신 운영 부담
다이어그램 설명. 컨테이너 방식이 무엇을 격리하는지 정리한 그림이다. 커널이 제공하는 단 하나의 경계가 파일·네트워크·프로세스·자원을 통째로 갈라놓는다 — 그래서 컨테이너 안에서 무슨 일이 나도 바깥은 안전하다는 강한 신뢰 경계가 선다. 다만 그 강함의 대가로 이미지 빌드, 볼륨 마운트, 네트워크 브리지 같은 운영 비용이 항상 따라온다. 핵심은 "하나의 경계로 전부"라는 점이다 — 한 번 세우면 모든 위험이 그 경계 안에 갇히지만, 그 경계를 세우고 유지하는 비용이 혼자 쓰는 서버에는 부담스럽다.
▲ 경량 격리 — 약한 경계 여럿, 그 대신 운영 부담 없음
다이어그램 설명. 경량 방식이 무엇을 격리하는지 정리한 그림이다. 하나의 강한 경계 대신, 위험 종류마다 도구를 따로 골라 붙인 네 개의 약한 경계가 있다. 각 경계는 컨테이너만큼 단단하지 않지만, 운영 비용이 거의 없다 — 리눅스가 이미 제공하는 기능(자원 한도·작업 디렉토리·실행 훅·루프백)을 조합할 뿐이라 새로 빌드하거나 유지할 게 없다. 두 그림을 나란히 놓으면 트레이드오프가 분명해진다. 컨테이너는 강한 경계 하나를 비싸게 사고, 경량 전략은 약한 경계 여럿을 싸게 산다. 함정은 "여럿"이라고 합치면 컨테이너만큼 강해진다고 오해하는 것 — 약한 경계를 네 개 겹쳐도 커널 네임스페이스 한 겹의 신뢰 경계에는 못 미친다. 이건 완전 봉쇄가 아니라 간섭 감소다.
효과와 한계 — 무엇을 얻고 무엇을 포기했나
이 전략으로 얻은 건 분명하다. 컨테이너의 운영 부담 없이, 한 대의 서버에서 여러 작업을 돌려도 한 작업의 폭주가 시스템 전체를 끌어내리지 않고, 서로 파일을 덮어쓰지 않고, 에이전트의 위험 명령이 무관한 곳까지 번지지 않으며, 내부 서비스가 외부에 새지 않는다. 일상적으로 마주치던 간섭 사고가 거의 사라졌다.
동시에 포기한 것도 분명히 해야 한다. 이건 "완전 격리"가 아니라 "간섭 감소"다. 자원 한도는 우회 가능하고, 명령 검사 훅은 커널 차단이 아니라 문자열 검사라 변형 공격에 약하고, 같은 서버 안의 프로세스는 여전히 루프백으로 서로 닿을 수 있다. 신뢰할 수 없는 코드를 진짜로 가둬야 한다면 — 예컨대 외부에서 받은 임의의 코드를 실행해야 한다면 — 이 전략으로는 부족하고 컨테이너나 가상 머신이 답이다.
핵심은 이 선택이 위협 모델에 맞춘 결정이라는 데 있다. 내 서버에서 도는 코드는 내가 통제하는 코드다. 악의적 침입자를 막는 게 아니라, 내 작업끼리의 사고와 에이전트의 실수를 막는 게 목적이다. 그 목적에는 약한 경계 넷이면 충분하고, 그 이상을 위해 컨테이너의 운영 비용을 치르는 건 과잉이었다. 격리에는 정답이 하나가 아니다 — 막아야 할 위험이 무엇인지 먼저 정하면, 거기에 맞는 가장 가벼운 수단이 보인다.
이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.
'IT' 카테고리의 다른 글
채점자도 AI라면 — LLM-as-judge로 답변 품질을 자동 평가할 때 빠지는 함정 (0) 2026.07.29 스킬이 50개 넘으니 엉뚱한 게 불렸다 — 겹침을 임베딩으로 자동 탐지하기 (0) 2026.07.29 스킬은 키워드가 아니라 '의미'로 불려 온다 — 의미 기반 매칭의 양날 (0) 2026.07.28 어떤 모델보다 '얼마나 깊게 생각하게 할까' — model × effort 2축으로 LLM 제어하기 (0) 2026.07.28 에이전트 셋을 동시에 풀었더니 서로의 코드를 덮어썼다 — git worktree로 푼 병렬 격리 (0) 2026.07.28 에이전트에게 셸을 풀어주되, 사고 반경은 봉쇄하기 — Bash 샌드박스 경계 설계 (0) 2026.07.27 에이전트를 통제하는 두 개의 손 — 무엇을 코드로 막고 무엇을 LLM에게 맡길까 (0) 2026.07.27 자동화의 두 층 — OpenClaw는 '언제 깨울지', Hermes는 '깨어난 여럿이 어떻게 나눌지' (0) 2026.07.26 AI가 긴 작업에서 계획을 어디에 두나 — OpenClaw는 맥락 속에, Hermes는 외부 목록에 (1) 2026.07.25 AI가 나를 오래 기억하는 두 방식 — OpenClaw는 검색으로 회상하고, Hermes는 늘 머릿속에 둔다 (0) 2026.07.25