<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Docker on 얼쑤 · Team Edo</title><link>https://team-edo.github.io/tags/docker/</link><description>Recent content in Docker on 얼쑤 · Team Edo</description><generator>Hugo</generator><language>ko-kr</language><lastBuildDate>Mon, 09 Nov 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://team-edo.github.io/tags/docker/index.xml" rel="self" type="application/rss+xml"/><item><title>컨테이너에서만 타임아웃 난 MySQL SSH 터널 추적기</title><link>https://team-edo.github.io/posts/2026/11/ssh-tunnel-container-mysql-timeout/</link><pubDate>Mon, 09 Nov 2026 00:00:00 +0000</pubDate><guid>https://team-edo.github.io/posts/2026/11/ssh-tunnel-container-mysql-timeout/</guid><description>&lt;p>MySQL 복제를 준비하면서 원본 DB에 닿는 경로를 SSH 터널로 제한했다. 원본 서버의 전달 포트는 루프백에만 열었다. 대기 서버에서는 SSH 클라이언트가 전용 브리지의 호스트 주소에 포트를 열어 복제 컨테이너의 연결을 받도록 했다.&lt;/p>
&lt;p>대기 서버 호스트에서 터널 포트에 접속하자 MySQL이 첫 응답을 보냈다. 같은 포트에 &lt;strong>복제 컨테이너에서&lt;/strong> 접속하자 &lt;code>ERROR 2003&lt;/code>과 함께 연결 시간이 초과됐다.&lt;/p>
&lt;h2 id="응답이-끊긴-구간-찾기">응답이 끊긴 구간 찾기&lt;/h2>
&lt;p>연결 경로를 세 위치에서 비교했다.&lt;/p></description></item><item><title>Docker 로그 6종 중 4종만 파일에 남은 이유</title><link>https://team-edo.github.io/posts/2026/11/docker-log-delivery/</link><pubDate>Mon, 09 Nov 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/11/docker-log-delivery/</guid><description>&lt;p>11월 9일 얼쑤의 운영 구성을 정리하면서, 나중에 문제가 생기면 각 프로그램의 기록을 어디에서 읽을 수 있는지 확인했습니다. Docker에는 로그 여섯 종류를 보내도록 설정돼 있었습니다. 그런데 점검일의 저장 폴더에서 찾을 수 있는 건 네 종류뿐이었습니다. 빠진 두 종류는 프로그램이 실행 중이었는데도 사후에 기록을 읽을 곳이 없었습니다.&lt;/p>
&lt;h2 id="로그가-지나가는-세-곳을-확인했습니다">로그가 지나가는 세 곳을 확인했습니다&lt;/h2>
&lt;p>컨테이너에서 실행되는 프로그램은 일을 하다가 필요한 내용을 출력합니다. Docker의 &lt;strong>로그 드라이버&lt;/strong>는 그 출력을 어디로 보낼지 정합니다. 얼쑤에서 확인한 여섯 프로그램은 모두 &lt;code>syslog&lt;/code> 방식으로 로그를 보냅니다. 같은 서버에서 기록을 받는 &lt;code>rsyslog&lt;/code>는 태그(출처 이름표)를 보고 어떤 기록을 파일로 남길지 결정합니다.&lt;/p></description></item><item><title>Docker 용량 조회 하나가 점검표의 네 칸을 비웠다</title><link>https://team-edo.github.io/posts/2026/11/docker-disk-stats-timeout/</link><pubDate>Mon, 09 Nov 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/11/docker-disk-stats-timeout/</guid><description>&lt;p>얼쑤 운영 점검에서 Docker가 응답하는데도 채팅 점검표의 Docker 상태와 저장공간이 ‘확인 불가’로 나왔습니다. 수집 순서와 코드가 오류를 처리하는 범위를 확인하니, 이미 읽은 값들이 마지막 조회의 시간 초과에 함께 지워지고 있었습니다.&lt;/p>
&lt;h2 id="저장공간에-대한-두-질문">저장공간에 대한 두 질문&lt;/h2>
&lt;p>저장공간에는 두 종류의 숫자가 있었습니다. &lt;strong>사용 중인 공간&lt;/strong>은 이미지, 컨테이너에 새로 쓴 파일, 볼륨, 빌드 캐시 등이 지금 차지한 양입니다. &lt;strong>정리 가능 공간&lt;/strong>은 Docker가 정리하면 확보할 수 있다고 보고한 양입니다. 서버 디스크 전체에 지금 남은 공간은 파일시스템에서 확인하는 별도 숫자입니다.&lt;/p></description></item><item><title>읽기 전용 컨테이너에서 Gunicorn이 기동 오류를 남긴 이유</title><link>https://team-edo.github.io/posts/2026/11/gunicorn-control-socket-read-only-rootfs/</link><pubDate>Mon, 09 Nov 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/11/gunicorn-control-socket-read-only-rootfs/</guid><description>&lt;p>얼쑤 기술 블로그의 편집 API를 점검하던 중 Gunicorn 기동 로그에서 같은 오류가 반복되는 것을 발견했습니다. 편집 설정과 조회수 API는 계속 HTTP 200을 돌려줬는데, 컨테이너를 시작할 때마다 &lt;code>/.gunicorn&lt;/code>을 만들 수 없다는 메시지가 한 번씩 남았습니다. Gunicorn이 시작하면서 어떤 작업에 실패했는지부터 살펴봤습니다.&lt;/p>
&lt;h2 id="요청은-처리됐는데-어떤-소켓이-실패했을까">요청은 처리됐는데 어떤 소켓이 실패했을까&lt;/h2>
&lt;p>Gunicorn은 HTTP 요청을 받는 리스너 외에 프로세스 관리를 위한 Unix 소켓을 엽니다. &lt;code>gunicornc&lt;/code>는 이 제어 소켓으로 워커 상태를 조회하거나 재시작을 요청할 수 있습니다. 소켓 경로를 지정하지 않으면 Gunicorn은 &lt;code>XDG_RUNTIME_DIR&lt;/code>을 우선 사용하고, 그 값이 없으면 홈 아래의 &lt;code>.gunicorn&lt;/code> 디렉터리를 사용합니다. &lt;a href="https://gunicorn.org/reference/settings/#control_socket">Gunicorn 제어 소켓 설정&lt;/a>&lt;/p></description></item><item><title>테스트한 JAR을 이미지에 그대로 넣기</title><link>https://team-edo.github.io/posts/2026/11/reuse-verified-jar-in-image/</link><pubDate>Fri, 06 Nov 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/11/reuse-verified-jar-in-image/</guid><description>&lt;p>얼쑤의 백엔드 CI는 Jenkins에서 Gradle로 검사하고 실행 JAR을 만든 뒤, Docker로 배포 후보 이미지를 빌드했습니다. 오래 걸린 빌드의 로그를 보니 Dockerfile이 소스와 Gradle Wrapper를 받아 &lt;code>bootJar&lt;/code>를 한 번 더 실행하고 있었습니다. Docker 레이어 캐시가 적중하지 않은 한 실행에서 이미지 안의 재빌드만 1분 51초가 걸렸습니다.&lt;/p>
&lt;h2 id="실행할-jar을-먼저-확정했다">실행할 JAR 먼저 확정&lt;/h2>
&lt;p>배포 후보 이미지의 역할을 확인해 보니, 배포 도구는 후보를 띄워 확인한 뒤 그 안에서 JAR을 꺼내 운영 실행 이미지에 연결하고 있었습니다. 후보 안에서 JAR을 다시 만들어야 할 이유가 없어, Jenkins가 이미 검증한 실행 JAR을 이미지에 넣도록 바꿨습니다.&lt;/p></description></item><item><title>파일 교체 뒤에도 남은 컨테이너 설정</title><link>https://team-edo.github.io/posts/2026/11/reload-file-bind-mount-in-place/</link><pubDate>Wed, 04 Nov 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/11/reload-file-bind-mount-in-place/</guid><description>&lt;p>운영 Nginx 설정에 압축 옵션을 반영할 때, 설정 파일을 읽는 쪽이 어디인지부터 확인했습니다. Nginx 컨테이너는 호스트의 설정 디렉터리 전체 대신 설정 파일 하나를 읽기 전용으로 연결하고 있었습니다. 호스트에서 경로가 같은 파일을 새로 만들어 넣어도, 실행 중인 컨테이너는 이전 파일을 계속 볼 수 있는 구성입니다.&lt;/p>
&lt;h2 id="이름을-유지하는-것과-파일을-유지하는-것">같은 이름과 같은 파일의 차이&lt;/h2>
&lt;p>Linux에서 경로는 파일을 찾는 이름입니다. 파일 하나를 bind mount하면 마운트는 그때 찾은 파일의 실체에 연결됩니다. 임시 파일을 만든 뒤 &lt;code>rename&lt;/code>으로 기존 이름을 교체하면 호스트에서 같은 경로를 열 때 새 파일을 읽습니다. 컨테이너가 이미 연결한 파일은 그대로 남습니다. 반대로 기존 파일을 열어 내용을 덮어쓰면 inode를 유지하므로 컨테이너에서도 같은 파일의 바뀐 내용을 읽습니다.&lt;/p></description></item><item><title>Docker가 Mac의 여유 공간과 파일을 못 본 이유</title><link>https://team-edo.github.io/posts/2026/10/docker-macos-vm-boundary/</link><pubDate>Wed, 28 Oct 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/10/docker-macos-vm-boundary/</guid><description>&lt;p>Mac에서 분석 서버 이미지를 만들 때 &lt;code>apt&lt;/code>가 패키지를 내려받을 공간이 부족하다고 했습니다. Mac에는 약 &lt;strong>20 GiB&lt;/strong>가 남아 있었습니다. 컨테이너 안에서 &lt;code>df -h /&lt;/code>를 실행하자 가상 디스크 파일시스템은 &lt;strong>40G&lt;/strong> 규모였고, 사용 가능량 &lt;strong>0&lt;/strong>, 사용률 **100%**로 표시됐습니다. 호스트의 남은 용량과 Linux 컨테이너가 실제로 쓸 수 있는 용량이 따로 움직이고 있었습니다.&lt;/p>
&lt;h2 id="검증에-필요한-이미지-수를-줄였다">실행 이미지 하나로 검증&lt;/h2>
&lt;p>당시에는 실행 이미지와 별도로 테스트용 빌드 단계까지 유지하려 했습니다. 로컬 Docker 디스크가 부족해 비슷한 크기의 이미지를 추가로 만들기 어려웠습니다. 테스트 전용 단계를 빼고, 실행 이미지에서 일회용 컨테이너를 띄워 개발 의존성을 받아 검사하는 방식으로 바꿨습니다. 검증 기록에서 실행 이미지는 약 &lt;strong>1.65 GB&lt;/strong>였고, 그 이미지로 컨테이너를 만들어 실행한 테스트 &lt;strong>63개&lt;/strong>가 통과했습니다. 디스크를 정리한 뒤 실패했던 리뷰의 빌드를 같은 조건으로 다시 돌렸다는 기록은 없습니다.&lt;/p></description></item><item><title>FFmpeg를 유지하며 Python 3.12로 맞추기</title><link>https://team-edo.github.io/posts/2026/10/pin-managed-python-in-shared-base-image/</link><pubDate>Wed, 28 Oct 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/10/pin-managed-python-in-shared-base-image/</guid><description>&lt;p>녹음 업로드는 백엔드에서 검사하고, 채점은 Python 분석 서비스에서 실행합니다. 두 서비스에서 같은 FFmpeg 패키지를 쓰기 위해 분석 이미지의 기반을 백엔드와 같은 Ubuntu &lt;strong>26.04&lt;/strong>로 맞췄습니다. 당시 기반 이미지의 시스템 Python은 &lt;strong>3.14&lt;/strong>였고, 분석 프로젝트는 &lt;strong>3.12&lt;/strong>를 사용했습니다. Python 버전을 맞추려고 기반 이미지를 바꾸면 함께 맞춰 둔 FFmpeg 패키지부터 다시 확인해야 했습니다.&lt;/p>
&lt;h2 id="시스템-python과-프로젝트-python을-분리했다">시스템 Python과 프로젝트 Python 분리&lt;/h2>
&lt;p>이미지에 uv를 넣고 &lt;code>UV_PYTHON_PREFERENCE=only-managed&lt;/code>로 설정했습니다. 프로젝트의 &lt;code>.python-version&lt;/code>에 있는 버전을 읽어 uv가 관리하는 Python을 설치하게 했습니다. 런타임 계정도 읽을 수 있는 공용 위치에 설치하고, 앱이 시작될 때는 그 Python으로 만든 가상 환경을 사용하도록 했습니다. uv를 가져오는 이미지도 다이제스트로 지정해 빌드가 참조하는 도구의 버전을 고정했습니다. &lt;a href="https://docs.astral.sh/uv/concepts/python-versions/#adjusting-python-version-preferences">uv의 관리형 Python 설정&lt;/a>&lt;/p></description></item><item><title>컨테이너 기동 후 첫 분석 요청이 500인 이유</title><link>https://team-edo.github.io/posts/2026/10/numba-cache-nonroot-first-request/</link><pubDate>Wed, 28 Oct 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/10/numba-cache-nonroot-first-request/</guid><description>&lt;p>얼쑤의 녹음 분석 API를 Docker 이미지로 만들었습니다. 이미지 안에서 root로 실행한 테스트는 통과했고, 기본 실행 명령으로 서버도 떴습니다. 그런데 기본 계정으로 뜬 서버에 처음 녹음 분석을 요청하자 HTTP 500이 돌아왔습니다. 로그에는 &lt;code>RuntimeError&lt;/code>가 남았습니다. 서버가 요청을 받을 수 있게 된 시점과 실제로 분석 코드를 불러오는 시점 사이에 차이가 있었습니다.&lt;/p>
&lt;h2 id="분석-코드를-불러올-때-캐시가-필요했다">모듈을 불러올 때 필요한 Numba 캐시&lt;/h2>
&lt;p>분석에 쓰는 librosa에는 Numba가 JIT 컴파일하고 결과를 캐시하는 함수가 있습니다. 이번에는 분석 요청에서 관련 모듈을 불러올 때 Numba가 캐시 위치를 찾지 못해 &lt;code>cannot cache function … no locator available&lt;/code> 오류를 냈습니다. 요청 전체를 보내지 않고 같은 이미지에서 &lt;code>import librosa.core.notation&lt;/code>만 실행해도 이 오류를 확인할 수 있었습니다.&lt;/p></description></item></channel></rss>