배경
내 헤드리스 Mac mini를 24/7로 운영하면서, 두 가지 문제를 마주했다.
첫 번째는 상시 대시보드다. 헤드리스 서버에는 모니터가 없으니 현재 상태를 한눈에 볼 방법이 없었다. (웹 대시보드가 있기는 했지만, 생각보다 접근성이 떨어짐) 세션 현황, cron 실행 결과, 날씨, 하드웨어 상태 등을 항상 표시해두는 전용 디스플레이가 필요했다.
두 번째는 오디오다. 알람, BGM 재생 등 오디오 출력 기능이 있는데, macOS에서 잠금 화면이 활성화되거나 디스플레이가 슬립되면 pygame 기반 오디오 재생이 실패할 가능성이 있다. macOS의 OS 수준 제약이라 소프트웨어로 우회하기 어렵다.
결론은 독립적인 I/O 관리 장치가 필요하다는 것이었다. Mac mini를 보조할 Raspberry Pi 4를 추가하기로 했다.
구성 목표
- 24/7 가동되는 키오스크 디스플레이 (대시보드)
- 독립 Audio Server (Mac에서 WebSocket으로 제어)
- Mac mini와 CAT6 LAN 직결 (Wi-Fi 불필요)
하드웨어 구성

Mac mini (M4)가 Wi-Fi로 인터넷에 연결되고, Pi는 Mac과 LAN 직결이다. Pi는 Mac을 통해 NAT로 인터넷에 접근한다. Wi-Fi 모듈은 쓰지 않는다.
OS 설치
OS 선택: Lite를 선택한 이유
Desktop 대신 Raspberry Pi OS Lite (64-bit, Trixie) 를 선택했다. 이유는 명확하다.
키오스크 디스플레이의 최우선 조건은 알림, 패널, 시스템 팝업 등 외부 interrupt가 없는 것이다. Desktop 환경 위에서 Chromium 키오스크를 띄우면 시스템 알림이나 업데이트 팝업이 끼어들 수 있다. Lite + cage (단일 앱 전용 Wayland 컴포지터)를 쓰면 구조적으로 단 하나의 앱만 풀스크린으로 표시되므로, interrupt 요소가 들어올 방법 자체가 없다.
리소스 측면에서도 Lite는 약 50MB RAM을 사용하는 반면 Desktop은 300MB+다. 24/7 구동 환경에서 유리하다.
플래싱
# SD 카드 확인
diskutil list
# 이미지 다운로드 및 SHA256 검증
curl -L -o setup/assets/raspios-lite-arm64.img.xz \
"https://downloads.raspberrypi.com/raspios_lite_arm64/images/..."
shasum -a 256 setup/assets/raspios-lite-arm64.img.xz
# 압축 해제 및 플래싱
xz -dk setup/assets/raspios-lite-arm64.img.xz
diskutil unmountDisk /dev/disk5
sudo dd if=setup/assets/raspios-lite-arm64.img of=/dev/rdisk5 bs=4m status=progress
/dev/rdisk5vs/dev/disk5raw disk(
rdisk)를 사용하면 block device보다 쓰기 속도가 훨씬 빠르다.
플래싱 후 bootfs 파티션이 자동 마운트된다. cloud-init은 첫 부팅에만 실행되므로 여기서 초기 설정을 전부 넣어야 한다.
user-data 파일에 사용자, SSH 키, 실행 명령을 설정했다.
#cloud-config
hostname: rpi-kiosk
ssh_pwauth: true
users:
- name: piuser
gecos: Pi User
groups: sudo, audio, video, render, input, plugdev, netdev
shell: /bin/bash
sudo: ALL=(ALL) NOPASSWD:ALL
lock_passwd: false
passwd: "$6$salt$hash..."
ssh_authorized_keys:
- <Mac의 SSH 공개키>
runcmd:
- systemctl enable ssh
- systemctl start ssh트러블슈팅: Pi 4 하드웨어 불량
첫 번째로 꺼낸 Pi 41가 부팅되지 않았다.
- 빨간 전원 LED ON, 초록 LED는 항상 solid (깜빡임 없음)
- HDMI 출력 없음 (
hdmi_safe=1적용해도 동일) - 네트워크 응답 없음
시도한 것:
- 순정 Trixie 이미지 → 실패
- 순정 Bookworm 이미지 → 실패
- EEPROM recovery 이미지 → 실패
결론: SD 카드 슬롯 하드웨어 불량. 새 Pi 4로 교체 후 정상 부팅 확인. 정상 Pi는 부팅 시 초록 LED가 깜빡이고, 모니터에 무지개 화면 → 콘솔 순으로 표시된다.
트러블슈팅: cloud-init SSH 설정
1. SSH 서비스 미시작
Trixie의 cloud-init은 ssh_pwauth: true만으로는 SSH 서비스가 자동 시작되지 않는다. runcmd에 명시적으로 추가해야 한다.
runcmd:
- systemctl enable ssh
- systemctl start ssh2. 비밀번호 해시 깨짐
passwd 필드의 SHA-512 해시에는 $ 문자가 포함된다. YAML에서 이를 그대로 쓰면 변수 치환으로 해석되어 해시가 깨진다.
# Bad — $ 문자가 YAML에서 해석됨
passwd: $6$salt$hash...
# Good — 쌍따옴표로 감싸야 함
passwd: "$6$salt$hash..."3. 인터페이스명 혼재
Trixie(Debian 13)에서 이더넷 인터페이스가 eth0 또는 end0일 수 있다. network-config에 양쪽을 모두 명시하고 optional: true로 설정해야 부팅 지연이 없다.
network:
version: 2
ethernets:
eth0:
dhcp4: true
optional: true
end0:
dhcp4: true
optional: true네트워크 설정
목표 구성은 다음과 같다.
| 인터페이스 | IP |
|---|---|
Mac en0 (Ethernet) | 192.168.4.1/24 |
Pi eth0 | 192.168.4.2/24 |
Mac이 게이트웨이 역할을 하며 Pi는 Mac의 Wi-Fi를 통해 NAT로 인터넷에 접근한다.
Mac 측 설정
# en0에 고정 IP 할당
sudo ifconfig en0 192.168.4.1 netmask 255.255.255.0 up
# IP 포워딩 활성화
sudo sysctl -w net.inet.ip.forwarding=1
# NAT 규칙 (pf)
echo 'nat on en1 from 192.168.4.0/24 to any -> (en1)' | sudo tee /tmp/pf-pi.conf
sudo pfctl -N -f /tmp/pf-pi.conf -e
# 검증
sudo pfctl -s nat
# nat on en1 inet from 192.168.4.0/24 to any -> (en1) round-robin이 설정은 재부팅 시 초기화된다. Mac이 재부팅될 때마다 수동으로 실행하거나, LaunchDaemon으로 자동화해야 한다.
Pi 측 설정
Pi에 SSH로 접속해 NetworkManager로 고정 IP를 설정한다. nmcli를 사용해야 한다. netplan 파일을 직접 수정하면 NetworkManager가 덮어쓴다.
sudo nmcli connection modify netplan-eth0 \
ipv4.method manual \
ipv4.addresses 192.168.4.2/24 \
ipv4.gateway 192.168.4.1 \
ipv4.dns '8.8.8.8 8.8.4.4'
sudo nmcli connection up netplan-eth0Pi 측 NetworkManager 설정은 재부팅 후에도 유지된다.
Mac 네트워크 자동화 (LaunchDaemon)
Mac 재부팅 시 en0 IP, IP 포워딩, NAT가 모두 초기화된다. LaunchDaemon으로 부팅 시 자동 적용한다.
#!/bin/bash
ifconfig en0 192.168.4.1 netmask 255.255.255.0 up
sysctl -w net.inet.ip.forwarding=1
echo "nat on en1 from 192.168.4.0/24 to any -> (en1)" > /tmp/pf-pi.conf
pfctl -N -f /tmp/pf-pi.conf -e 2>/dev/null || true<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "...">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.pi-station.network</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>~/pi-station/setup/pi-network.sh</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>sudo cp setup/com.pi-station.network.plist /Library/LaunchDaemons/
sudo launchctl bootstrap system /Library/LaunchDaemons/com.pi-station.network.plist이제 Mac 재부팅 후에도 Pi와의 연결이 자동으로 복구된다.
키오스크 환경 설정
대시보드를 항상 표시할 키오스크 환경을 구성한다. 구성 요소는 단순하다.
- cage: 단일 앱 전용 Wayland 컴포지터
- Chromium: kiosk 모드로 대시보드 URL을 표시
sudo apt install -y cage chromium wtype패키지명 주의
Debian Trixie에서는
chromium-browser가 아닌chromium이다.chromium-browser는 존재하지 않는다.
시도 1: systemd 서비스 방식 — 실패
처음에는 systemd 서비스로 cage를 직접 실행하려 했다. 직관적이고 관리가 편해 보였기 때문이다.
[Service]
Type=simple
ExecStart=/usr/bin/cage -- chromium --kiosk http://...결과: cage 프로세스는 시작되지만 Chromium을 fork하지 못한다. Tasks: 0 상태로 멈춘다.
원인은 TTY 세션과 DRM 장치 접근 권한 문제다. systemd 서비스는 정상적인 로그인 세션 컨텍스트가 없어서, DRM(Direct Rendering Manager)에 대한 권한을 얻지 못한다. TTYPath, StandardInput=tty-force, PAMName=login 등을 설정해봤지만 해결되지 않았다.
시도 2: autologin + bash_profile 방식 — 성공
autologin으로 tty1에 piuser를 자동 로그인하고, ~/.bash_profile에서 cage를 시작하는 방식이다. 정상적인 로그인 세션을 통해 cage가 실행되므로 DRM 권한 문제가 없다.
# tty1 autologin 설정
sudo mkdir -p /etc/systemd/system/getty@tty1.service.d
sudo tee /etc/systemd/system/getty@tty1.service.d/override.conf << 'EOF'
[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin piuser --noclear %I $TERM
EOF
sudo systemctl daemon-reloadif [ "$(tty)" = "/dev/tty1" ]; then
export WLR_LIBINPUT_NO_DEVICES=1
(sleep 20 && while true; do WAYLAND_DISPLAY=wayland-0 wtype -k Escape; sleep 30; done) &
exec cage -s -- chromium \
--disable-wayland-ime \
--kiosk \
--no-first-run \
--disable-translate \
--noerrdialogs \
--disable-infobars \
--disable-session-crashed-bubble \
http://192.168.4.1/pi-dashboard
fiexec로 실행하므로 cage가 login 셸을 대체한다. cage가 종료되면 getty가 재로그인하고, bash_profile이 다시 실행되어 cage가 재시작된다. 자동 복구 구조다.
| 옵션 | 설명 |
|---|---|
WLR_LIBINPUT_NO_DEVICES=1 | 입력 장치 없어도 cage 시작 허용 |
cage -s | 마지막 앱 종료 시 cage도 종료 (getty 재시작 유도) |
--kiosk | Chromium 전체화면, UI 요소 숨김 |
--disable-wayland-ime | Wayland IME 바인딩 비활성화 |
트러블슈팅: IME 언어 toast
키오스크를 켜면 Chromium 좌상단에 “English”라는 언어 표시 토스트가 나타났다. Chromium이 Wayland의 zwp_text_input 프로토콜에 바인딩하면서 발생하는 것이다.
시도한 것:
--disable-features=ImeThread,VirtualKeyboard,WaylandTextInputV1,WaylandTextInputV3→ 효과 없음--disable-virtual-keyboard→ 효과 없음- Chromium 정책 (
TouchVirtualKeyboardEnabled,VirtualKeyboardEnabled) → 효과 없음 /etc/chromium.d/00-rpi-vars에서--force-renderer-accessibility제거 → 효과 없음- Chromium 프로필 초기화 → 효과 없음
--disable-wayland-ime→ toast는 여전히 뜨지만 Escape 키로 닫을 수 있음
--disable-wayland-ime을 적용하면 toast가 완전히 없어지지는 않지만, Escape 키로 닫힌다는 것을 발견했다. 그리고 toast는 주기적으로 재출현한다.
해결: wtype -k Escape로 toast를 주기적으로 자동으로 닫는다. bash_profile에서 cage 시작 전 백그라운드 루프를 돌린다.
(sleep 20 && while true; do WAYLAND_DISPLAY=wayland-0 wtype -k Escape; sleep 30; done) &20초 대기 후(Chromium 초기화 완료 대기) 30초 간격으로 Escape를 누른다.
트러블슈팅: 마우스 커서 숨기기
키오스크에 마우스 커서가 표시되는 문제가 있었다. cage에는 커서 숨김 옵션이 없고, XCURSOR_SIZE=1이나 CSS cursor: none은 Wayland 환경에서 효과가 없었다.
해결: Adwaita 커서 테마의 모든 커서 파일을 1×1 투명 Xcursor로 교체한다.
# 1×1 투명 PNG 생성
python3 -c "
import struct, zlib
def chunk(t, d):
c = t + d
return struct.pack('>I', len(d)) + c + struct.pack('>I', zlib.crc32(c) & 0xffffffff)
with open('/tmp/1x1.png', 'wb') as f:
f.write(b'\x89PNG\r\n\x1a\n')
f.write(chunk(b'IHDR', struct.pack('>IIBBBBB', 1, 1, 8, 6, 0, 0, 0)))
f.write(chunk(b'IDAT', zlib.compress(b'\x00\x00\x00\x00\x00')))
f.write(chunk(b'IEND', b''))
"
# Xcursor 파일 생성
echo "1 0 0 /tmp/1x1.png" > /tmp/transparent.cfg
xcursorgen /tmp/transparent.cfg /tmp/transparent
# Adwaita 커서 교체 (백업 후)
sudo cp -r /usr/share/icons/Adwaita/cursors /usr/share/icons/Adwaita/cursors.bak
for f in /usr/share/icons/Adwaita/cursors/*; do
sudo cp /tmp/transparent "$f"
done주의
이 작업 후에는 마우스를 연결해도 커서가 보이지 않는다. 원복이 필요하면
cursors.bak을 복원해야 한다.
마무리
설정이 완료된 후 전체 스택은 다음과 같이 동작한다.
- Pi 부팅 → tty1 autologin → cage 시작 → Chromium 키오스크
- Mac 재부팅 → LaunchDaemon → en0 IP + IP forwarding + NAT 자동 설정
- 대시보드 → Mac에서 pm2로 구동 중인
pi-dashboard-api,pi-dashboard-app을 Caddy 리버스 프록시로 통합, Pi 키오스크에서http://192.168.4.1/pi-dashboard로 접근
Mac 재부팅 후에는 ./init.sh 한 줄로 전체 상태를 복구할 수 있다.
./init.sh # 전부 (네트워크 + 대시보드 + 오디오)
./init.sh -n # 네트워크만
./init.sh -c # 상태 확인만설정 과정에서 가장 시간을 많이 쓴 부분은 역설적으로 Pi 하드웨어 불량(교체 필요)과 Chromium IME toast였다. OS, 네트워크, kiosk 설정 자체는 문서화가 잘 되어 있어서 시행착오가 있었어도 방향을 잡기는 어렵지 않았다. 다만 cage + Wayland 조합에서 systemd 서비스 방식이 동작하지 않는 부분은 레퍼런스가 거의 없어서 직접 삽질로 알아낸 부분이다.
Footnotes
-
번개장터에서 중고로 구매했는데… 참으로 운이 없었다. ↩