SSD Nodes Learn 8GB RAM — 연 $66
가이드 Matt Connor작성자 Matt Connor · 업데이트됨 2026-08-01

Docker Compose로 wg-easy WireGuard 구축하기

Docker Compose로 wg-easy를 실행하는 방법을 설명합니다. 포트, NET_ADMIN, 필수 sysctls와 휴대폰 QR 코드 등록을 다루며, Version 15에서 환경 변수가 관리자 패널로 이동한 변경점도 확인합니다.

구축하는 항목

wg-easy는 웹 인터페이스를 제공하는 WireGuard이며, 하나의 Docker 컨테이너로 실행됩니다. WireGuard 인터페이스를 대신 관리하고, 클라이언트를 생성할 수 있는 브라우저 UI를 추가합니다. 생성하는 모든 클라이언트에는 config 파일과 QR 코드가 제공됩니다. 따라서 휴대폰 카메라를 화면에 비추기만 하면 VPN에 연결할 수 있습니다.

터널 자체는 일반적인 WireGuard입니다. 커널 모듈이 패킷을 전달하므로 처리량은 수동으로 구성한 설정과 동일합니다. 대신 클라이언트 수명 주기를 관리할 수 있습니다. SSH로 config 파일을 편집하지 않고도 피어를 추가하고 비활성화하고 삭제할 수 있습니다. 반면 해당 config를 직접 제어할 수 없으며, 이 내용은 VPS에서 수동으로 WireGuard 설정하기에서 설명합니다.

공용 IPv4 주소가 있는 KVM VPS, Compose plugin이 포함된 Docker Engine, root 액세스 권한이 필요합니다. OpenVZ 또는 LXC처럼 호스트 커널을 공유하는 컨테이너 가상화 환경에서는 일반적으로 WireGuard 모듈을 로드할 수 없습니다. 따라서 컨테이너가 인터페이스를 활성화하지 못합니다.

Version 15에서는 설정이 환경 변수에서 이동했습니다

대부분의 가이드는 wg-easy 14를 기준으로 작성되었습니다. 이 버전에서는 WG_HOST에 서버 주소를 설정하고, PASSWORD_HASH에 관리자 암호의 bcrypt 해시를 설정했습니다. 두 값 모두 환경 변수로 설정했습니다. Version 15는 다시 작성된 버전입니다. 공식 마이그레이션 안내에는 v15가 v14와 동일한 환경 변수를 사용하지 않으며, 대부분의 환경 변수가 웹 UI의 관리자 패널로 이동했다고 명시되어 있습니다.

따라서 WG_HOSTPASSWORD_HASH는 더 이상 아무 작업도 수행하지 않습니다. 이전 compose 파일을 복사하면 컨테이너는 시작되지만 해당 줄을 무시합니다. 그런 다음 브라우저에서 관리자 계정을 생성하라는 메시지를 표시합니다. 이는 버그가 아닙니다. 새로운 설정 절차입니다.

2026년 7월 기준으로 고정해야 할 주요 태그는 15입니다. latest을 사용하지 말고 주요 버전을 고정해야 합니다. 주요 버전 업그레이드는 디스크에 저장된 설정 형식을 변경하므로 정상적으로 롤백되지 않습니다.

compose 파일

스택용 디렉터리를 만들고 공식 compose 파일을 해당 디렉터리에 작성합니다. 이 파일은 변경되지 않은 upstream 파일입니다.

sudo mkdir -p /etc/docker/containers/wg-easy
sudo curl -o /etc/docker/containers/wg-easy/docker-compose.yml \
  https://raw.githubusercontent.com/wg-easy/wg-easy/master/docker-compose.yml

내용은 다음과 같습니다.

volumes:
  etc_wireguard:

services:
  wg-easy:
    image: ghcr.io/wg-easy/wg-easy:15
    container_name: wg-easy
    networks:
      wg:
        ipv4_address: 10.42.42.42
        ipv6_address: fdcc:ad94:bacf:61a3::2a
    volumes:
      - etc_wireguard:/etc/wireguard
      - /lib/modules:/lib/modules:ro
    ports:
      - "51820:51820/udp"
      - "51821:51821/tcp"
    restart: unless-stopped
    cap_add:
      - NET_ADMIN
      - SYS_MODULE
    sysctls:
      - net.ipv4.ip_forward=1
      - net.ipv4.conf.all.src_valid_mark=1
      - net.ipv6.conf.all.disable_ipv6=0
      - net.ipv6.conf.all.forwarding=1
      - net.ipv6.conf.default.forwarding=1

networks:
  wg:
    driver: bridge
    enable_ipv6: true
    ipam:
      driver: default
      config:
        - subnet: 10.42.42.0/24
        - subnet: fdcc:ad94:bacf:61a3::/64

etc_wireguard는 서버 key와 생성하는 모든 client를 저장하는 named volume입니다. 이 volume을 백업해야 합니다. 그렇지 않으면 다시 빌드할 때 모든 peer가 삭제됩니다. 해당 파일을 host 파일 시스템에서 확인하려면 bind mount로 바꾸십시오. 이 작업을 수행하기 전에 bind mount와 named volume의 차이를 읽으십시오. 권한 동작이 서로 다르기 때문입니다.

NET_ADMIN, SYS_MODULE 및 sysctl이 필요한 이유

컨테이너는 기본적으로 네트워크 스택에 접근할 수 없습니다. 각 설정은 특정 차단 하나를 해제합니다.

NET_ADMIN을 사용하면 컨테이너가 wg0 인터페이스를 생성하고, 주소를 할당하며, 라우트를 기록할 수 있습니다. 이 설정이 없으면 인터페이스를 활성화하는 과정에서 ip link add wg0 type wireguardOperation not permitted을 반환하므로 컨테이너가 시작된 후 종료됩니다.

SYS_MODULE과 읽기 전용 /lib/modules 마운트를 함께 사용하면 호스트에서 WireGuard 커널 모듈을 아직 로드하지 않은 경우에도 컨테이너가 해당 모듈을 로드할 수 있습니다. 모듈은 이미지 내부가 아니라 호스트 커널에 있으므로 호스트 디렉터리를 컨테이너에서 볼 수 있어야 합니다. 최신 커널에서는 일반적으로 모듈이 커널에 기본 포함되어 있으며, 호스트에서 sudo modprobe wireguard && echo ok를 사용하여 확인할 수 있습니다.

net.ipv4.ip_forward=1은 시스템 자체로 전송되지 않은 패킷을 커널이 전달하도록 설정합니다. 이 설정이 없으면 클라이언트가 연결되고 핸드셰이크도 성공하지만, 인터넷으로 향하는 모든 패킷이 삭제됩니다. 따라서 VPN이 연결된 것처럼 보여도 ping 1.1.1.1이 시간 초과됩니다.

net.ipv4.conf.all.src_valid_mark=1은 많은 사용자가 의외로 생각하는 설정입니다. WireGuard는 자체 발신 패킷에 표시를 지정하여 해당 패킷이 터널로 다시 라우팅되지 않도록 합니다. 엄격한 역방향 경로 필터링은 소스 주소가 예상 경로와 일치하지 않는 패킷을 감지하여 삭제합니다. 이 sysctl은 커널이 표시가 지정된 패킷을 수락하도록 하며, 이를 통해 전체 터널이 자체적으로 중단되는 것을 방지합니다.

시작하고 관리자 계정을 생성합니다

cd /etc/docker/containers/wg-easy
sudo docker compose up -d
sudo docker compose logs -f

startstop이 아니라 docker compose updocker compose down를 사용합니다. Upstream에서는 다른 설정으로 생성된 컨테이너에서 start을 실행하면 네트워크가 일관되지 않은 상태가 된다고 경고합니다. 재부팅 후 스택을 다시 시작하려면 restart: unless-stopped이 이미 이를 처리합니다. compose 서비스의 부팅 동작에서는 해당 정책이 보장하는 내용과 보장하지 않는 내용을 설명합니다.

웹 UI는 TCP 51821에서 연결을 수신합니다. 처음 방문하면 관리자 계정을 생성하고 클라이언트가 서버에 연결할 때 사용할 호스트 주소를 확인하는 설정 페이지가 표시됩니다. 이 호스트 주소는 모든 클라이언트 설정의 Endpoint 줄에 들어가므로 VPS의 공인 IP 또는 DNS 이름이어야 합니다. 주소가 잘못되면 휴대폰에 제공한 QR code가 연결할 수 없는 주소를 가리키므로 handshake가 완료되지 않습니다.

해당 포트와 관련해 한 가지 더 확인할 사항이 있습니다. wg-easy 15에서는 INSECURE=true을 설정하지 않으면 일반 HTTP 연결을 거부합니다. 신뢰할 수 없는 인증서를 사용하는 HTTPS로 연결하거나, 앞단의 reverse proxy에서 TLS를 종료하는 방식은 모두 사용할 수 있습니다. 기본 설정으로 http://을 통해 연결하는 방식은 사용할 수 없습니다.

UI 포트를 인터넷에 공개하지 않기

compose 파일은 모든 인터페이스에서 51821을 공개합니다. 이 포트는 트래픽을 라우팅할 수 있는 서버의 로그인 페이지이므로 외부에 공개해서는 안 됩니다. Docker에서 포트를 공개하면 DOCKER 체인에 규칙이 기록됩니다. 이 체인은 ufw보다 먼저 평가되므로 ufw deny 규칙만으로는 해당 포트를 닫을 수 없습니다. 이 문제는 별도로 이해할 가치가 있으며, Docker에서 공개한 포트가 ufw 규칙을 무시하는 이유에서 전체 내용을 설명합니다.

간단한 해결 방법은 UI를 loopback에 바인딩하고 SSH 터널을 통해 접속하는 것입니다.

    ports:
      - "51820:51820/udp"
      - "127.0.0.1:51821:51821/tcp"
    environment:
      - INSECURE=true

그런 다음 노트북에서 다음을 실행합니다.

ssh -L 51821:127.0.0.1:51821 youruser@your.server.address

노트북의 브라우저에서 http://127.0.0.1:51821을 엽니다. 트래픽은 SSH로 암호화됩니다. 다른 호스트에는 해당 포트가 응답하지 않습니다. 일반 HTTP 홉이 loopback 인터페이스를 벗어나지 않으므로 이 경우 INSECURE=true은 안전합니다.

UDP 51820을 열고 두 방화벽을 모두 확인합니다

WireGuard 자체는 인터넷에서 UDP 51820에 연결할 수 있어야 합니다. Docker가 이 포트를 게시하지만, 많은 제공업체는 VPS 앞에 별도의 네트워크 방화벽을 배치하며 Docker는 이를 인식하지 못합니다. 두 위치에서 모두 포트를 엽니다. ufw로 호스트 방화벽을 관리한다면, 직접 nftables 규칙을 작성하는 것보다 VPS의 기본 ufw 규칙을 사용하는 편이 더 간단합니다.

컨테이너가 실제로 수신 대기 중인지 확인합니다.

sudo ss -ulnp | grep 51820

수신 대기 중인 UDP 소켓이 표시되어야 합니다. 해당 줄에 아무것도 표시되지 않으면 컨테이너가 인터페이스를 시작하지 못한 것입니다. sudo docker compose logs wg-easy에서 원인을 확인할 수 있습니다.

클라이언트를 생성하고 휴대폰에서 스캔하기

UI에서 클라이언트를 생성하고 나중에 알아볼 수 있는 이름을 지정합니다. 예를 들어 클라이언트가 속한 장치 이름을 사용할 수 있습니다. wg-easy는 사용 가능한 다음 터널 주소를 할당하고 키 쌍을 자동으로 생성합니다. 각 클라이언트 행에는 QR 코드와 다운로드 가능한 .conf 파일이 있습니다.

휴대폰에 공식 WireGuard 앱을 설치하고 QR 코드에서 터널을 추가하도록 선택합니다. 그런 다음 화면에 표시된 코드에 카메라를 가져갑니다. 입력한 이름으로 터널이 표시됩니다. 터널을 켜면 UI의 클라이언트 행에 전송 카운터와 최근 handshake 시간이 표시되기 시작합니다.

활성화한 후에도 handshake가 표시되지 않는 클라이언트는 서버에 전혀 도달하지 못하고 있는 것입니다. 이 경우 provider firewall 또는 구성에 포함된 endpoint 주소에서 UDP 51820을 확인해야 합니다. handshake는 표시되지만 인터넷이 작동하지 않는 클라이언트는 forwarding 또는 DNS 문제일 가능성이 높습니다.

데스크톱에서는 .conf 파일을 다운로드하고 다시 입력하지 말고 WireGuard 클라이언트로 가져옵니다. 이 파일의 private key는 한 번만 생성되고 한 번만 표시됩니다. 이 파일은 SSH private key와 같은 방식으로 관리합니다.

UI를 넘어설 시점

피어가 사람과 휴대폰인 동안에는 wg-easy가 적합한 도구입니다. UI를 사용하는 편이 구성 파일을 편집하는 것보다 빠릅니다. 분실한 휴대폰의 액세스를 해지하는 작업도 한 번 클릭하면 됩니다.

UI에서 모델링하지 않는 기능이 필요해지면 한계에 도달합니다. 피어의 AllowedIPs가 단일 주소가 아니라 원격 서브넷 전체를 대상으로 하는 사이트 간 라우팅이 보통 처음 마주치는 한계입니다. 피어별 라우팅 규칙을 사용하는 분할 터널이나 프로비저닝 도구에서 생성한 구성도 다음 단계의 요구 사항입니다. 이 시점에는 직접 작성하는 설정이 더 어려운 것이 아니라 방식이 다를 뿐입니다. 일반 WireGuard 가이드에서는 wg0.conf를 사용해 동일한 터널을 구축하는 방법을 보여 줍니다. 제어 플레인을 직접 실행하는 것 자체를 중단하려면 WireGuard와 Tailscale 비교에서 관리형 옵션을 설명합니다.

위의 compose 구문에서 WireGuard 부분보다 파일 형식이 더 낯설었다면 VPS에서 Docker Compose 기초에서 파일 형식과 일상적인 명령을 설명합니다.

FAQ

wg-easy가 WG_HOST와 PASSWORD_HASH를 무시하는 이유는 무엇입니까?

이 변수는 wg-easy 14에 해당합니다. 버전 15는 다시 작성되었으며, upstream에서 거의 모든 설정을 웹 UI의 관리자 패널로 옮겼습니다. 컨테이너는 두 변수 중 어느 것도 읽지 않으므로 정상적으로 시작한 후 첫 방문 시 관리자 계정을 만들도록 요청합니다. 설정 페이지에서 클라이언트가 사용할 호스트 주소를 설정합니다.

호스트 커널에 이미 WireGuard가 있는 경우에도 SYS_MODULE이 필요합니까?

아닙니다. SYS_MODULE/lib/modules 마운트는 호스트에 모듈이 없을 때 컨테이너가 모듈을 로드할 수 있도록 합니다. sudo modprobe wireguard가 호스트에서 이미 성공하는 경우 이 capability는 사용되지 않습니다. 이를 제거하는 것은 합리적인 보안 강화 조치이며, 어느 경우든 NET_ADMIN는 여전히 필요합니다.

클라이언트는 연결되지만 인터넷이 되지 않습니다. 무엇이 문제입니까?

트래픽 없이 핸드셰이크만 이루어지는 경우는 거의 항상 forwarding 문제입니다. 직접 편집한 사본에서는 이 설정이 자주 누락되므로 compose 파일에 net.ipv4.ip_forward=1net.ipv4.conf.all.src_valid_mark=1가 여전히 있는지 확인합니다. forwarding이 활성화되어 있다면 클라이언트가 받은 DNS 서버를 확인합니다. 모든 트래픽을 VPN으로 보내면서 더 이상 도달할 수 없는 DNS 서버를 지정한 터널은 브라우저에서 연결이 끊긴 것처럼 보입니다.

클라이언트를 어떻게 백업합니까?

모든 데이터는 etc_wireguard named volume의 wg0.json 파일에 저장됩니다. UI에는 동일한 데이터를 내보내는 백업 버튼도 있습니다. 업그레이드하기 전에 해당 파일을 서버 외부의 위치에 복사합니다. 복원은 새 컨테이너의 설정 단계에서 파일을 업로드하여 수행합니다.

reverse proxy 뒤에서 wg-easy를 실행할 수 있습니까?

예. TCP 51821 앞에 proxy를 배치하고 그곳에서 TLS를 종료한 다음, 컨테이너에 INSECURE=true를 설정하여 proxy에서 전달하는 일반 HTTP 요청을 수락하도록 합니다. VPN 트래픽은 UDP이며 HTTP proxy를 통과하지 않으므로 UDP 51820은 직접 publish된 상태로 유지합니다.