배경
제 헤드리스 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 서비스 방식이 동작하지 않는 부분은 레퍼런스가 거의 없어서 직접 삽질로 알아낸 부분입니다.
남은 과제 - 유선 네트워크 전환
솔직히 말하면, 처음엔 Mac Mini 서버가 Wi-Fi만으로도 네트워크 수요와 SLO를 충분히 감당했습니다. 그런데 기능이 하나둘 고도화되면서 결국 유선 연결이 필요한 수준까지 와버렸습니다. 문제는 Mac Mini의 포트를 이미 Pi와의 연결에 쓰고 있다는 점입니다. 결국 스위치 기반으로 연결을 다시 짜는 재설계가 필요한데, 시간이 안 나서 아직 손을 못 대고 있습니다. 진행하게 되면 따로 정리하겠습니다.
Footnotes
-
번개장터에서 중고로 구매했는데… 참으로 운이 없었습니다. ↩