- 들어가며 — VPS를 끊으려다 시작된 실험
- 첫 번째 실패 — 드라이버를 버리고 리눅스를 얻으려 하다
- Termux를 호스트 제어 평면으로 쓴다
- 안드로이드 전원 관리와 싸우는 목록
- PRoot와 chroot — 비용은 CPU가 아니라 시스템 콜에 붙는다
- 호환 계층은 격리 경계가 아니다
- 인그레스 — 아웃바운드 연결 하나로 만드는 공개 서비스
- 재현 가능성 — 셸 히스토리가 아니라 Git
- 참고 자료
들어가며 — VPS를 끊으려다 시작된 실험
2026년 8월 4일 seg6.space에 my server is a phone now라는 글이 올라왔습니다. Hetzner VPS에서 돌리던 개인 서비스들을 서랍에 있던 CMF Phone 1로 옮긴 기록입니다.
폰의 사양은 ARM 코어 8개, RAM 8GB, 플래시 128GB, Wi-Fi 6, 5G 모뎀, 그리고 내장 배터리입니다. 마지막 항목이 은근히 중요한데, 서버 입장에서는 작은 무정전 전원 장치이기 때문입니다.
이 글이 읽을 만한 이유는 "폰으로 서버 돌렸다"는 결과가 아니라, 저자가 두 번 실패하고 그 실패에서 정확한 교훈을 뽑아냈기 때문입니다. 특히 두 번째 실패는 리눅스 컨테이너와 호환 계층을 다루는 사람이라면 누구에게나 해당되는 이야기입니다.
첫 번째 실패 — 드라이버를 버리고 리눅스를 얻으려 하다
가장 깔끔해 보이는 접근은 평범한 리눅스 배포판을 플래싱하는 것입니다. CMF Phone 1에는 postmarketOS 디바이스 포트가 있고, 부팅되며, 디바이스 페이지에 초록색 표시가 충분히 많습니다.
저자가 덜 주의 깊게 본 것은 "깨짐"으로 표시된 항목들이었습니다. Wi-Fi, 블루투스, 하드웨어 가속. 결과는 postmarketOS 스플래시 화면과 검은 디스플레이였고, 그 시점에 서버도 폰도 없는 상태가 되었습니다.
복구도 만만치 않았습니다. 플래싱 도구가 윈도우를 요구해서 QEMU에 윈도우를 설치하고 USB 패스스루와 MediaTek 드라이버와 씨름했으며, 결국 실제 윈도우 머신에서 공장 이미지를 복원했습니다. 중간에 소프트 브릭 상태로 검은 화면만 나오는 구간도 있었습니다.
저자가 뽑은 교훈은 정확합니다. 안드로이드에는 이 하드웨어의 모든 조각에 대한 동작하는 드라이버가 이미 있습니다. Wi-Fi, 전원 관리, 배터리, GPU, 모뎀, 온갖 벤더 고유 세부 사항까지 전부. 더 익숙한 유저스페이스를 얻겠다고 그걸 통째로 버리는 것은 나쁜 교환이었습니다.
필요했던 것은 폰이 평범한 리눅스 머신이 되는 것이 아니라, 안드로이드가 하드웨어 관련 일을 계속 하는 동안 리눅스 애플리케이션을 안정적으로 돌리는 것이었습니다.
Termux를 호스트 제어 평면으로 쓴다
두 번째 시도는 스톡 안드로이드를 유지하고 Termux를 호스트 환경으로 삼습니다.
Termux는 OpenSSH, runit, Caddy, cloudflared, 패키지 관리, 그리고 충분히 정상적인 유닉스 도구들을 제공합니다. Termux:Boot이 재부팅 후 슈퍼바이저와 SSH를 시작하고, Tailscale이 안정적인 사설 주소를 줍니다. 그래서 tailnet에 속한 어느 머신에서든 그냥 접속하면 됩니다.
ssh cmf
여기서 저자가 짚는 구조가 중요합니다. Termux는 가상 머신이 아닙니다. 프로세스들은 안드로이드의 리눅스 커널 위에서 그대로 실행됩니다. 다만 유저스페이스가 Bionic 기반이라 일반 데비안과 충분히 달라서, 기존 리눅스 애플리케이션 이미지를 그냥 떨어뜨려 넣을 수는 없습니다.
이 분리가 오히려 유용한 구조가 됩니다. Termux는 작은 호스트 제어 평면으로 남고, 각 애플리케이션은 자기가 기대하는 리눅스 파일시스템을 직접 들고 옵니다. 서비스 수명 주기는 runit이 관리합니다.
안드로이드 전원 관리와 싸우는 목록
폰을 서버로 쓸 때 가장 성가신 상대는 CPU가 아니라 배터리 최적화입니다. 저자의 표현으로 "안드로이드의 배터리 관리는 원래 하는 일에는 아주 뛰어나고, 서버인 척하는 기기에는 아주 나쁘다"입니다.
Ansible 빌드가 적용하는 안드로이드 호스트 프로파일은 다음을 합니다.
- 지속적인 웨이크 락 설치
- light idle과 deep idle 비활성화
- Termux, Termux:Boot, Tailscale을 백그라운드 제한에서 제외
- 자식 프로세스 제한기 비활성화
- Wi-Fi 서스펜드 방지
- Tailscale을 상시 VPN으로 설정
그런데 개별 설정보다 중요한 것은 복구 사슬이라고 저자는 강조합니다.
Android boot
-> Tailscale always-on VPN
-> Termux:Boot
-> runit
-> resident services
-> local and public health checks
이 사슬이 있으면 폰은 사람이 알아차리기를 기다리지 않고 스스로 재부팅에서 복귀합니다. 자가 복구 능력은 개별 튜닝의 합이 아니라 부팅 경로 설계에서 나옵니다. 이건 폰이 아니라 어떤 서버에도 해당됩니다.
PRoot와 chroot — 비용은 CPU가 아니라 시스템 콜에 붙는다
여기가 이 글의 핵심입니다.
저자의 애플리케이션 대부분은 이미 리눅스 ARM64 OCI 이미지로 배포되고 있었습니다. proot-distro를 쓰면 데비안 안에서 이 이미지들을 애플리케이션 수정 없이 돌릴 수 있었습니다.
PRoot가 하는 일은 이렇습니다. 파일시스템과 프로세스 관련 연산을 유저스페이스에서 가로채서, 평범한 Termux 프로세스가 자신이 데비안 루트 파일시스템 안에 산다고 믿게 만듭니다. 루트 권한도 특별한 커널도 필요 없다는 점이 큰 장점입니다.
일반적인 웹 서비스들은 이 방식으로 잘 돌았습니다. 애플리케이션마다 검증된 루트 파일시스템, 루프백 포트 하나, runit 서비스 정의를 주고, Caddy가 호스트명을 그 포트로 라우팅합니다.
예외가 하나 있었습니다. Surf라는 원격 브라우저 워크로드입니다. 프로세스 시작, 라이브러리 열기, 경로 순회, 브라우저 프로파일 읽기, 캡처 데이터 이동이 전부 PRoot의 유저스페이스 변환 계층을 통과했습니다. 저자의 문장이 정확합니다. "CPU는 남아 있었지만 Chrome이 거기에 효율적으로 도달하지 못했다."
이 문장을 곱씹을 가치가 있습니다. 모니터링 대시보드에는 유휴 CPU가 보입니다. 메모리도 남습니다. 그런데 애플리케이션은 느립니다. 병목이 자원의 양이 아니라 자원에 도달하는 경로에 있기 때문입니다.
해결은 코드를 고치는 것이 아니었습니다. 폰을 루팅해서, 안드로이드를 대체하기 위해서가 아니라 같은 데비안 파일시스템을 제대로 마운트하고 진짜 chroot로 진입하기 위해서 루팅했습니다. runit이 여전히 Termux에서 수명 주기를 관리하고 설정도 그대로인데, 워크로드만 변환 계층 대신 네이티브 시스템 콜로 커널에 도달합니다. 저자의 표현으로 "개선은 미묘하지 않았습니다."
일반화하면 이렇습니다. 호환 계층의 비용은 계산량이 아니라 시스템 콜 빈도에 비례합니다. 큰 배열을 도는 계산 위주 프로그램은 PRoot 위에서도 거의 손해가 없습니다. 파일을 수천 개 열고 프로세스를 계속 띄우는 프로그램은 같은 계층에서 무너집니다. 어떤 워크로드가 어느 쪽인지 미리 아는 것이 이 판단의 전부입니다.
호환 계층은 격리 경계가 아니다
성능 이야기 다음에 저자가 못 박는 문장이 있는데, 이 부분은 인용할 가치가 있습니다.
PRoot에 대해 "이것은 컨테이너 경계가 아니다"라고 씁니다. 모든 것이 여전히 안드로이드의 커널, 네트워크 네임스페이스, Termux UID를 공유합니다. 애플리케이션 호환 계층으로서는 극히 유용하지만, 그것이 전부입니다.
chroot로 옮긴 뒤에도 같은 태도를 유지합니다. 배포 흐름은 이렇습니다. 작업 컴퓨터가 각 ARM64 이미지를 정확한 다이제스트로 해석해 파일시스템을 추출하고, Ansible이 검증한 뒤 폰에 설치합니다. 작은 루트 헬퍼가 사설 마운트 네임스페이스를 만들고 필요한 경로를 바인드하고 chroot로 진입한 뒤 권한을 낮추고 원본 이미지의 엔트리포인트를 실행합니다. 폰에는 Docker도 컴파일러도 필요 없습니다.
그리고 이렇게 덧붙입니다. 이것들은 여전히 보안 경계가 아니라 호환 환경이며, 사설 마운트 네임스페이스는 주로 마운트와 정리를 예측 가능하게 유지하는 용도라고요.
이 구분은 폰 밖에서도 유효합니다. 컨테이너를 격리라고 부르는 습관 때문에 우리는 종종 네임스페이스가 주는 것과 샌드박스가 주는 것을 섞어서 생각합니다. 마운트 네임스페이스는 파일시스템 뷰를 나눕니다. 커널을 나누지는 않습니다. 적대적인 워크로드를 같은 커널 위에 올려 두고 네임스페이스만으로 막을 생각이라면, 그건 다른 설계가 필요한 문제입니다.
인그레스 — 아웃바운드 연결 하나로 만드는 공개 서비스
가정용 회선에는 고정 IP도 없고, 라우터에 SSH나 애플리케이션 포트를 열고 싶지도 않습니다. 게다가 저자는 폰을 들고 나가서도 서버가 계속 살아 있기를 원했습니다.
HTTP 서비스는 Cloudflare Tunnel을 씁니다. cloudflared가 폰에서 바깥으로 연결 하나를 만들고, Cloudflare가 호스트명별로 그 통로에 요청을 밀어 넣고, Caddy가 루프백 서비스로 라우팅합니다.
Internet -> Cloudflare Tunnel -> Caddy on 127.0.0.1 -> application on 127.0.0.1
라우터에 인바운드 규칙이 없습니다. 그래서 폰을 다른 네트워크로 옮기면 터널이 재연결되고 호스트명이 그대로 따라옵니다. 관리 접근은 Tailscale이 같은 일을 합니다.
한 서비스만 다른 경로가 필요했습니다. Surf 백엔드는 지연에 민감하고, 자체 TLS를 종료하며, 접속하는 오래된 iPad가 서버 신원을 핀 고정합니다. 일반적인 Cloudflare Tunnel은 TLS를 Cloudflare에서 종료하는데, 핀 고정된 연결에는 그게 곧 실패입니다.
해법은 Surf의 TLS 스트림 전체를 평범한 WebSocket 안에 감싸는 것이었습니다. Cloudflare는 WebSocket을 보고 전달할 뿐이고, 실제 인증된 연결은 그 안에서 종단간 암호화된 채로 남습니다. 대가는 지연입니다. 집 밖에서는 대략 네트워크 왕복이 하나 더 붙고, iPad 연결은 처음 측정했을 때 60밀리초 정도였다고 적혀 있습니다.
TLS를 종료하는 프록시와 신원을 핀 고정하는 클라이언트가 만나면 항상 이 문제가 생깁니다. 감싸기는 그 충돌을 피하는 표준적인 회피책이고, 비용은 왕복 하나입니다.
재현 가능성 — 셸 히스토리가 아니라 Git
마지막으로 운영 관점의 교훈입니다.
저자는 "일주일이면 잊어버릴 명령어들로 조립된 애완동물 서버"를 원하지 않았습니다. 그래서 호스트 상태 전체를 Ansible로 관리합니다. 버전, 서비스 정의, 라우트, 전원 설정, 시크릿, 헬스체크가 전부 하나의 비공개 저장소에 있습니다.
배포 흐름의 각 단계에 검증이 붙어 있습니다.
release or OCI image
-> checksum/digest pinned in Git
-> Ansible over SSH
-> versioned files on the phone
-> atomic current symlink
-> runit service
-> local health check
-> public edge check
릴리스는 다이제스트나 체크섬으로 고정되어 버전 디렉터리에 설치되고, 원자적 current 심링크 뒤에 놓입니다. 체크섬이나 헬스체크가 실패하면 배포가 멈추고, 롤백은 고정 값을 되돌려 다시 적용하는 것입니다. 애플리케이션 데이터는 릴리스와 분리되어 있습니다.
시크릿 처리도 눈여겨볼 만합니다. 폰에는 Git 체크아웃이 없으므로 시크릿도 없습니다. Ansible Vault 값은 인프라 저장소에 암호화된 채로 있고, vault 비밀번호는 1Password SSH 에이전트에게 고정된 챌린지에 서명하게 해서 유도합니다. 개인 키는 1Password 안에 남고 폰은 그것에 접근할 필요가 없습니다. 배포 시점에 Ansible이 각 서비스가 필요한 런타임 값만 Termux의 사설 저장소에 렌더링합니다.
저자 자신이 붙인 단서도 그대로 옮겨 둡니다. 자동 오프디바이스 백업 없이 대체 불가능한 데이터를 여기 두지 않겠다고 했고, chroot를 적대적 워크로드 격리로 취급하지 않겠다고 했으며, 루팅이 신뢰 경계를 넓힌다는 점과 향후 안드로이드 업데이트가 새 문제를 만들 수 있다는 점을 인정합니다. 좋은 실험 기록의 조건은 이 단서들이 결론과 같은 크기로 적혀 있는 것입니다.
참고 자료
- my server is a phone now — seg6.space, 2026-08-04 (하드웨어 사양, 설정 목록, 지연 수치는 전부 이 글에서 옮긴 것입니다)
- PRoot 프로젝트
- Termux 위키 — Termux:Boot
- Cloudflare Tunnel 문서
- Hacker News 토론 스레드
현재 단락 (1/68)
2026년 8월 4일 seg6.space에 [my server is a phone now](https://seg6.space/posts/phone-server/)라는 글이 올라왔습...