← 개발 로그 목록

website: dev→main 승격 — sqlite-web GUI 뷰어 작업 중 배운 것들을 스킬 문서에 정리

/ 7분 분량 / 개발 로그

sqlite-web으로 DB를 GUI로 볼 수 있게 만드는 작업을 하다가 겪은 문제들을 정리해서 인프라 스킬 문서와 학습 기록에 반영하고, 그걸 dev에서 main으로 승격한 PR이다.

원래 목적은 팀원이 "DB 화면으로 보고 싶다"고 할 때마다 나한테 직접 물어보지 않아도 되게 하는 거였다. dbclient CLI는 이미 있었지만 SQL을 직접 타이핑해야 하니, 조회만 하고 싶은 사람 입장에서는 진입장벽이 있었다. 그래서 sqlite-web을 붙여서 GUI 뷰어를 배포하기로 했는데, 이 과정에서 예상 못한 문제가 세 개 정도 연달아 나왔고 그걸 하나씩 해결하면서 배운 걸 기록해두는 게 이번 PR의 실질적인 내용이다.

GHCR pull이 denied 나던 이유

ghcr.io/coleifer/sqlite-web:latest를 pull하려는데 계속 denied가 났다. public 이미지고 남의 레포인데 왜 막히는지 처음엔 감이 안 왔다. 원인을 찾아보니 CD 워크플로(cd.yml)가 배포마다 docker login ghcr.io를 github.actor + secrets.GITHUB_TOKEN으로 새로 걸고 있었는데, 이 토큰이 likelion-khu-official/website 소속 패키지에만 스코프돼 있었다. GHCR은 인증 정보가 실려 있으면 그 identity의 명시적 권한만 보고 판단하지, 대상 패키지가 public이라고 해서 익명 접근으로 자동 전환해주지 않는다는 걸 이때 알았다.

디버깅 관점에서 남겨둘 만한 건, 이런 상황에서 우리 쪽 권한 설정을 먼저 의심하기 전에 docker logout ghcr.io 후 재시도로 "저장된(스코프 좁은) 로그인 탓인지"부터 구분하는 게 빠르다는 점이다. 그리고 CD가 매 배포마다 로그인을 다시 걸기 때문에, 디버깅용으로 로그아웃해놔도 일부러 복구할 필요 없이 다음 배포가 알아서 되돌려놓는다.

exec format error — arm64 호스트와 서드파티 이미지

GHCR 문제를 풀고 나니 컨테이너가 Restarting (255)를 반복했다. docker compose ps에는 원인이 안 보여서 docker compose logs를 봤더니 exec /usr/local/bin/sqlite_wsgi: exec format error가 찍혀 있었다. 예전에 backend Dockerfile에서 겪었던 arm64 플랫폼 불일치와 같은 계열 문제였는데, 이번엔 우리가 빌드하는 이미지가 아니라 커뮤니티 배포 이미지라 Dockerfile을 고칠 수가 없었다.

docker image inspect <img> --format '{{.Architecture}}'

로 아키텍처부터 확인하고, 재빌드 대신 QEMU 에뮬레이션을 등록하는 쪽으로 방향을 잡았다.

docker run --privileged --rm tonistiigi/binfmt --install all

저트래픽 admin 도구라 에뮬레이션 성능 저하는 무시할 수 있는 수준이었다. 다만 이 명령이 컨테이너 하나만의 설정이 아니라 호스트 커널 전역 설정이라는 점은 기록해뒀다 — 이후 다른 amd64 이미지도 이 호스트에서 자동으로 에뮬레이션 실행된다.

nologin 셸이 forced command를 이기는 이유

dbtunnel 계정(터널 전용, 셸 nologin)에 command="echo tunnel-only-account",... 형태로 forced command를 걸어두고 실제 키로 접속 테스트를 했다. 그런데 이 문구 대신 "This account is currently not available."이라는 nologin 기본 메시지만 나왔다. sshd가 forced command를 실행할 때 내부적으로 사용자셸 -c "forced-command" 형태로 호출한다는 걸 알고 나니 이해가 됐다 — 셸이 nologin이면 그 인자를 통째로 무시하고 항상 자기 메시지만 찍는다. 버그는 아니고 의도보다 강한 이중 방어였던 셈이다. nologin이 exec 채널을 독립적으로 차단하고 있어서 command= 문구가 실행되든 안 되든 어차피 셸 접근은 막힌다. 반면 포트포워딩(direct-tcpip, permitopen)은 로그인 셸을 거치지 않고 sshd가 직접 처리하기 때문에 이 영향을 안 받는다 — dbtunnel의 -L 터널이 정상 동작하는 이유가 여기 있었다.

커스텀 안내 메시지가 꼭 필요하면 nologin 대신 /bin/sh -c 'echo ...; exit 1' 같은 최소 셸을 써야 한다는 걸 배운 대로 기록해뒀다. 안 그러면 나중에 "왜 내가 넣은 메시지가 안 나오지"로 다시 헤맬 가능성이 있다.

배운 걸 스킬로 만드는 단계 (#169, #170)

여기까지는 pm/docs/learnings.md에 기록으로 남긴 것들이고, 별도로 db-access 스킬 자체도 두 번에 걸쳐 보완했다. 처음(#169)엔 "GUI 열어줘"라는 요청이 오면 infra/db-dev-ui.sh를 사용자가 직접 돌리는 대신 Claude가 그 자리에서 SSH 터널을 열고 URL을 확인해서 알려주는 절차를 추가했다. Windows 환경을 테스트하면서 WSL을 거치는 방식이 Git Bash의 경로 변환이나 DrvFs 권한 문제(/mnt/c가 chmod가 안 먹는 것) 때문에 오히려 복잡해진다는 걸 확인했고, PowerShell 내장 ssh.exe로 바로 되는 걸 확인한 뒤로는 굳이 WSL을 거칠 필요가 없다고 판단해서 스킬 문서에 그렇게 명시했다.

이후 실사용 테스트(#170)에서 두 가지가 더 드러났다. 하나는 GUI(sqlite-web)가 :ro 마운트라 DML/DDL 둘 다 불가능하고 dbclient는 DML만 가능하다는 차이인데, 이걸 요청자가 매번 헷갈리지 않게 표로 안내하도록 넣었다. 다른 하나는 원래 db-dev-ui.sh가 tmux 분할창으로 조회+조작을 페어로 여는 걸 의도했는데, GUI 터널만 열고 dbclient는 별도 요청이 올 때까지 기다리고 있었던 부분이다. 사용자가 dbclient도 같이 뜨길 기대했는데 안 떠서 지적한 뒤로, Windows에서도 새 PowerShell 창에 인터랙티브 dbclient 세션을 같이 띄우는 방식으로 고쳤다. 세션 종료 절차도 손봤는데, pkill -f "ssh ..."처럼 문자열 매칭으로 죽이면 그 명령 자체가 자기 프로세스 목록과 매칭돼 의도치 않게 죽는 사고가 실제로 났어서, 포트 기준으로 소유 프로세스를 찾아 죽이는 방식으로 바꿨다.

이 PR 자체는 infra/.claude/skills/와 pm/docs/learnings.md만 건드린 문서 승격이라 백엔드·프론트·CD 트리거는 없다. 코드 변경이 없는 dev→main 승격이라도, 이렇게 실측하면서 얻은 운영 지식을 스킬 문서에 바로 반영해두면 다음에 같은 문제를 다시 처음부터 조사하지 않아도 된다는 점에서 남겨둘 가치가 있다고 판단했다.