먼저 확인할 것: 패널 전용 이미지와 올인원 서버는 다릅니다
MetaCubeXD를 Docker로 설치할 때 가장 먼저 구분해야 할 것은 이미지의 역할입니다. MetaCubeX/metacubexd 공식 저장소가 안내하는 ghcr.io/metacubex/metacubexd:latest는 대시보드 정적 파일만 제공하는 패널 전용 이미지입니다. 보통 호스트의 8080을 컨테이너의 HTTP 포트 80에 연결하고, 사용자가 별도로 실행한 Mihomo의 API 주소와 Secret을 화면에 입력합니다. 이 이미지는 Mihomo를 포함하거나 시작·중지하지 않습니다.
이 글에서 설치할 ghcr.io/metacubex/metacubexd-server:latest는 구성이 다릅니다. 대시보드 UI, 프로필과 Mihomo 코어를 관리하는 제어 에이전트, 아키텍처에 맞는 Mihomo 코어를 하나의 서버 이미지에 묶은 공식 올인원 서버입니다. 따라서 기존 Mihomo 컨테이너에 화면만 붙이려는 사람은 패널 전용 이미지를, 프로필·Mihomo 코어·프록시를 한 컨테이너에서 관리하려는 사람은 올인원 서버를 선택해야 합니다.
이 글의 범위: 아래 단계는 올인원 서버 이미지에만 적용됩니다. 패널 전용 이미지의 8080:80 예시와 올인원 서버의 8080:8080을 섞으면 포트는 열려도 기대한 제어 에이전트와 Mihomo가 존재하지 않는 상태가 됩니다.
| 구분 | 패널 전용 | 올인원 서버 |
|---|---|---|
| 이미지 | metacubexd:latest | metacubexd-server:latest |
| Mihomo 코어 | 별도로 준비 | 이미지에 포함 |
| 프로필 관리 에이전트 | 없음 | 포함 |
| 대표 HTTP 매핑 | 8080:80 | 8080:8080 |
배포 전에 두 종류의 Secret과 영구 데이터 위치를 준비합니다
Docker와 Compose 플러그인이 정상 동작하는 Linux 호스트에서 빈 작업 디렉터리를 만들고, 그 안에 compose.yaml과 .env를 둡니다. 공식 올인원 Compose 예제는 CONTROL_TOKEN과 CLASH_SECRET을 서로 다른 강력한 임의 값으로 설정하도록 요구합니다. 예제의 change-me 문자열이나 동일한 값을 두 변수에 재사용하지 마십시오.
CONTROL_TOKEN은 /api/control/**에서 프로필과 Mihomo 코어를 관리하는 요청에 사용하는 Bearer 토큰입니다. 같은 출처의 올인원 대시보드는 이 값을 브라우저에 주입해 제어 에이전트를 호출하므로, 이 변수 하나만으로 대시보드 화면 자체가 외부에서 보호되는 것은 아닙니다. CLASH_SECRET은 Mihomo의 external-controller API를 인증하는 별도의 Secret이며, 대시보드에서 백엔드 엔드포인트를 연결할 때 Secret 칸에 입력하는 값입니다.
CONTROL_TOKEN=replace-with-a-strong-random-value-1
CLASH_SECRET=replace-with-a-different-random-value-2
TZ=Asia/Seoul
올인원 서버의 데이터는 Docker의 명명된 볼륨을 통해 /data에 저장됩니다. 여기에는 프로필, 현재 활성 설정, Mihomo 코어 다운로드와 geo 및 fake-IP 같은 런타임 캐시가 들어가며 쓰기 가능해야 합니다. 컨테이너를 새로 만들어도 이 볼륨을 유지하면 데이터를 이어서 사용할 수 있지만, docker compose down -v는 볼륨까지 삭제하므로 일반적인 재시작이나 업데이트 명령으로 사용하면 안 됩니다.
| 항목 | 역할 | 주의점 |
|---|---|---|
CONTROL_TOKEN | 제어 에이전트 API 인증 | 대시보드 공개를 막는 로그인 비밀번호로 간주하지 않음 |
CLASH_SECRET | Mihomo Clash API 인증 | UI 엔드포인트의 Secret과 일치해야 함 |
/data | 프로필·활성 설정·다운로드·캐시 | 쓰기 권한과 백업 필요 |
기본 프록시 전용 Compose를 작성하고 시작합니다
공식 구성은 기본적으로 프록시 전용 모드입니다. 컨테이너가 HTTP/SOCKS 혼합 프록시를 제공하지만 호스트의 전체 트래픽을 자동으로 가로채는 TUN은 아직 켜지지 않습니다. 처음에는 권한이 적고 되돌리기 쉬운 이 구성을 사용해 UI, API, 프로필과 명시적 프록시 연결을 먼저 검증하십시오.
services:
metacubexd:
image: ghcr.io/metacubex/metacubexd-server:latest
container_name: metacubexd
restart: unless-stopped
environment:
CONTROL_TOKEN: '${CONTROL_TOKEN:?Set CONTROL_TOKEN in .env}'
CLASH_SECRET: '${CLASH_SECRET:?Set CLASH_SECRET in .env}'
CONTROL_PORT: '8080'
CLASH_API_PORT: '9090'
MIXED_PORT: '7890'
TZ: '${TZ:-Asia/Seoul}'
ports:
- '8080:8080'
- '9090:9090'
- '7890:7890'
volumes:
- 'metacubexd-data:/data'
healthcheck:
test: ['CMD', 'wget', '-qO-', 'http://127.0.0.1:8080/api/control/health']
interval: 30s
timeout: 5s
start_period: 10s
retries: 3
volumes:
metacubexd-data: {}
Docker 포트 게시는 Mihomo의 수신 정책을 덮어쓰지 않습니다. Mihomo의 allow-lan 기본값은 false입니다. bridge 네트워크에서는 mixed 포트가 컨테이너 loopback에만 바인딩되므로 7890:7890만 게시한다고 호스트나 LAN에서 프록시에 접근할 수 있는 것은 아닙니다. 게시한 프록시를 사용하려면 현재 활성 프로필에 allow-lan: true를 명시하고, bind-address와 lan-allowed-ips를 필요한 네트워크로 제한하며, authentication, Docker 호스트 바인딩, 호스트 방화벽을 함께 적용하십시오. 프로필을 바꾼 후에는 이 값들을 다시 확인합니다.
docker compose config로 환경 변수와 들여쓰기를 먼저 확인한 뒤 docker compose up -d를 실행합니다. docker compose ps와 docker compose logs --tail=100 metacubexd에서 반복 재시작, 포트 충돌, /data 권한 오류가 없는지 봅니다. 호스트의 8080이 이미 사용 중이라면 매핑 왼쪽만 18080:8080처럼 바꿀 수 있습니다. 이때 브라우저와 호스트의 health 요청은 18080을 사용합니다. 9090이나 7890의 왼쪽을 바꾸면 대시보드 endpoint 또는 클라이언트에 새 호스트 포트를 사용합니다. 컨테이너 내부의 CONTROL_PORT를 바꾸려면 매핑 오른쪽과 healthcheck URL도 동시에 변경해야 합니다.
포트의 역할을 구분하세요: 8080은 대시보드와 제어 에이전트, 9090은 Mihomo Clash API 및 WebSocket, 7890은 HTTP와 SOCKS를 함께 받는 혼합 프록시입니다. 9090을 브라우저용 프록시로 설정하거나 7890을 대시보드 주소로 열어서는 안 됩니다.
대시보드, 제어 에이전트, Mihomo 코어와 실제 프록시를 따로 검증합니다
브라우저에서 http://<server-IP>:8080을 열고, Mihomo 엔드포인트를 http://<server-IP>:9090으로 지정합니다. Secret 칸에는 앞서 만든 CLASH_SECRET을 입력합니다. 올인원 대시보드는 동일 출처의 제어 에이전트를 호출할 때 CONTROL_TOKEN을 자동으로 사용하므로 일반적인 연결 화면에서 그 값을 Mihomo Secret 대신 입력하지 않습니다.
대시보드의 :8080과 Mihomo API의 :9090은 포트가 달라 브라우저에서 서로 다른 출처로 취급됩니다. 활성 프로필의 external-controller-cors에 대시보드의 정확한 출처를 허용하지 않으면 API가 직접 응답해도 UI 연결은 실패할 수 있습니다. 공개 페이지에서 사설 네트워크의 controller를 호출하는 경우 Private Network Access(PNA)도 확인하고, 의도한 구성에서만 allow-private-network: true를 사용합니다. 예를 들어 대시보드가 http://192.168.1.2:8080이라면 다음처럼 그 출처만 허용합니다.
external-controller-cors:
allow-private-network: true
allow-origins:
- 'http://192.168.1.2:8080'
HTTPS UI에는 Mixed Content 경계도 있습니다. HTTPS로 열린 페이지가 안전하지 않은 http://<server-IP>:9090 HTTP/WebSocket을 호출하면 브라우저가 보통 CORS를 평가하기 전에 요청을 차단합니다. 9090을 HTTPS/WSS 역방향 프록시 뒤에 두거나 Mihomo의 external-controller-tls와 유효한 인증서를 설정한 뒤, endpoint에 해당 https:// 주소를 사용하십시오. TLS와 Mixed Content를 먼저 해결한 뒤 CORS와 PNA를 조정합니다.
컨테이너의 상태 확인 기능이 호출하는 GET /api/control/health가 HTTP 200을 반환하면 대시보드 서버와 제어 에이전트가 응답한다는 뜻입니다. 공식 문서도 이 응답이 Mihomo 코어의 실행 상태를 보증하지 않는다고 명시합니다. 따라서 컨테이너가 정상으로 표시되어도 프로필 구문 오류, Mihomo 코어 시작 실패, 잘못된 엔드포인트 Secret, 사용할 수 없는 노드 또는 외부 연결 실패가 남아 있을 수 있습니다.
호스트 매핑을 18080:8080으로 바꾼 경우 호스트에서의 health 요청은 18080을 사용합니다. 컨테이너 내부의 CONTROL_PORT를 바꾼 경우에는 매핑 오른쪽과 Compose healthcheck URL도 함께 변경합니다.
docker compose ps에서 컨테이너 실행 상태와 상태 확인 결과를 봅니다.- 대시보드의 Kernel 화면에서 Mihomo 코어가 running 상태인지 확인하고, 실패하면 제어 에이전트의 Kernel 로그를 읽습니다.
- 프로필을 가져온 뒤 활성화 과정의 Mihomo 설정 검증이 통과하는지 확인합니다.
9090엔드포인트와CLASH_SECRET으로 Proxies·Connections·Logs 화면이 갱신되는지 확인합니다.- 테스트 장치의 HTTP 또는 SOCKS 프록시를
<server-IP>:7890으로 명시하고 실제 HTTPS 요청을 보냅니다. - Connections 또는 Clash 로그에서 그 요청이 나타나고 예상한 규칙과 노드를 사용했는지 확인합니다.
UI가 열리는 것, 상태 확인 엔드포인트가 200인 것, 실제 요청이 7890을 통해 성공하는 것은 서로 다른 증거입니다. 세 결과를 하나로 묶어 “Mihomo가 정상”이라고 판단하지 마십시오. 특히 외부 사이트 접속까지 확인하지 않으면 컨테이너 내부 프로세스만 살아 있고 프록시 데이터 경로는 실패하는 상황을 놓칠 수 있습니다.

게시한 8080·9090·7890 포트를 인터넷에 그대로 노출하지 않습니다
Compose의 8080:8080 같은 짧은 매핑은 기본적으로 호스트의 모든 인터페이스에 바인딩될 수 있습니다. 공인 IP, 포트 포워딩 또는 느슨한 클라우드 보안 그룹이 결합되면 대시보드, Mihomo 제어 API와 프록시가 인터넷에서 직접 접근 가능한 상태가 됩니다. CONTROL_TOKEN은 브라우저에 주입되어 같은 출처의 제어 에이전트 호출에 사용되므로 대시보드 공개 자체를 막는 경계가 아니며, CLASH_SECRET이 있어도 무차별 대입·취약점 탐색·자원 소모 위험까지 사라지지는 않습니다.
한 컴퓨터에서만 사용할 경우 호스트 매핑을 127.0.0.1:8080:8080, 127.0.0.1:9090:9090, 127.0.0.1:7890:7890처럼 루프백 주소로 제한할 수 있습니다. 신뢰할 수 있는 LAN 장치가 사용해야 한다면 호스트 방화벽이나 네트워크 ACL로 필요한 사설 대역만 허용하고, 외부 관리가 필요하면 인증과 TLS를 제공하는 역방향 프록시 또는 VPN 경계를 사용하십시오. 관리 화면뿐 아니라 9090과 7890도 같은 수준으로 보호해야 합니다.
Secret을 공개하지 마세요: .env, Compose 렌더링 결과, 브라우저 개발자 도구와 로그를 공유할 때 두 값을 가립니다. 저장소에 .env를 커밋하지 말고, 노출이 의심되면 두 값을 각각 새로 발급한 뒤 컨테이너를 다시 생성합니다.
방화벽을 설정한 뒤에는 허용된 장치와 허용되지 않은 네트워크 양쪽에서 확인해야 합니다. 내부에서 페이지가 열린다는 사실만으로 외부 차단이 입증되지는 않습니다. 라우터의 자동 포트 개방 기능과 클라우드 공급자의 별도 방화벽도 함께 점검하십시오.
Linux에서 TUN이 필요할 때만 호스트 네트워크와 권한을 추가합니다
기본 프록시 전용 구성이 정상이고 명시적 프록시를 사용할 수 없는 프로그램까지 호스트 수준에서 처리해야 할 때만 TUN을 검토합니다. 공식 올인원 문서의 TUN 예시는 Linux 전용 고급 재정의 구성이며, 컨테이너에 호스트 네트워크 네임스페이스, 네트워크 관리 권한인 NET_ADMIN과 TUN 장치를 제공합니다. Docker Desktop의 가상 머신 환경이나 다른 운영체제에 이 Linux 절차를 그대로 적용하면 같은 결과를 보장할 수 없습니다.
services:
metacubexd:
image: ghcr.io/metacubex/metacubexd-server:latest
restart: unless-stopped
network_mode: host
cap_add:
- NET_ADMIN
devices:
- '/dev/net/tun:/dev/net/tun'
environment:
CONTROL_TOKEN: '${CONTROL_TOKEN:?Set CONTROL_TOKEN in .env}'
CLASH_SECRET: '${CLASH_SECRET:?Set CLASH_SECRET in .env}'
CONTROL_PORT: '8080'
CLASH_API_PORT: '9090'
MIXED_PORT: '7890'
TZ: '${TZ:-Asia/Seoul}'
volumes:
- 'metacubexd-data:/data'
volumes:
metacubexd-data: {}
network_mode: host를 사용하면 Docker의 ports: 매핑은 무시됩니다. 따라서 기존 ports: 블록을 삭제하고, 서비스가 호스트의 8080, 9090, 7890에 직접 바인딩된다고 가정해 방화벽을 다시 설계해야 합니다. 호스트 네트워크를 추가한 뒤에도 기존 포트 게시 규칙이 계속 보호해 줄 것이라고 생각하면 안 됩니다.
컨테이너 권한만 추가해도 TUN이 자동으로 시작되지는 않습니다. 활성 Mihomo 프로필에 지원되는 tun: 설정이 있어야 하며, 적용 전에는 콘솔 접근과 복구 방법을 준비합니다. 먼저 기존 VPN과 충돌하는 경로가 없는지 확인하고, 적용 후에는 기본 경로, DNS, LAN 접근과 실제 외부 요청을 각각 검사합니다. 네트워크가 끊기면 새 구성을 계속 수정하기보다 컨테이너를 중지하고 검증된 프록시 전용 Compose로 되돌립니다.
권한은 필요한 경우에만: NET_ADMIN과 /dev/net/tun은 일반 대시보드나 혼합 프록시 사용에 필요하지 않습니다. 처음부터 추가하지 말고, 프록시 전용 기준선이 통과한 뒤 TUN이라는 한 변수만 변경하십시오.
latest 이미지 업데이트 전에 /data까지 복구할 수 있게 준비합니다
latest 태그는 특정 이미지 버전을 고정하지 않습니다. 새 빌드가 게시되면 같은 태그가 이전과 다른 이미지 다이제스트를 가리킬 수 있으므로, 오늘 정상인 Compose 파일을 나중에 다시 실행했을 때 다른 이미지가 내려올 수 있습니다. 운영 환경에서는 최초 배포가 검증된 시점의 이미지 ID와 다이제스트를 기록하고, 변경 통제가 필요하면 검증한 다이제스트로 이미지를 고정합니다. compose.yaml만 보관해서는 나중에 검증 당시와 같은 이미지를 다시 실행할 수 있다고 보장할 수 없습니다.
- 현재 Compose와 비밀 값을 제외한 설정 사본, 실행 중인 이미지 다이제스트를 기록합니다.
- 구독 Token과 Secret이 들어갈 수 있는
/data볼륨을 접근 제한된 위치에 백업하고 실제로 읽을 수 있는지 확인합니다. - 중요 작업이 없는 시간에
docker compose pull과docker compose up -d를 실행합니다. - 상태 확인 응답 200뿐 아니라 Mihomo 코어의 running 상태, 프로필 활성화,
9090연결과7890실제 프록시 요청까지 다시 검사합니다. - 문제가 생기면 새 컨테이너를 중지하고 기록한 이전 이미지 다이제스트로 Compose를 되돌린 뒤 재생성합니다.
- 데이터 형식 변경으로 이전 이미지가 기존 볼륨을 읽지 못할 때만 중지 상태에서 검증한
/data백업을 복원합니다.
일반적인 컨테이너 교체에서는 명명된 볼륨을 그대로 두고, docker compose down -v나 볼륨 정리 명령을 실행하지 않습니다. 롤백 도중에도 새 상태를 바로 덮어쓰지 말고 현재 볼륨의 사본을 하나 더 남기면 원인 조사와 재시도를 분리할 수 있습니다. 이미지 롤백과 데이터 롤백은 서로 다른 작업이므로 먼저 이전 이미지가 현재 데이터로 정상 기동하는지 확인한 뒤 필요한 경우에만 데이터까지 되돌립니다.
마지막으로 컨테이너 재시작, 호스트 재부팅과 실제 클라이언트 한 대의 연결을 확인합니다. 대시보드 접근 범위, 방화벽 규칙, 선택한 이미지 다이제스트, 볼륨 백업 시점과 복구 절차를 짧게 기록해 두면 latest 변경이나 TUN 실험 뒤에도 추측이 아닌 확인 가능한 기준으로 복구할 수 있습니다.
공식 자료와 버전 범위
- MetaCubeXD 공식 README
- MetaCubeXD 공식 Docker Compose 예제
- Mihomo 공식 전역 설정 문서
- MetaCubeXD 올인원 서버 supervisor 소스
버전 번호, 파일 이름, 화면 문구는 업데이트로 바뀔 수 있습니다. 작업 전에 연결된 공식 문서를 확인하세요.