
Ulak는 사용자가 관리하는 서버에서 로컬 프로젝트의 Docker 작업을 실행하는 도구다. 편집기와 소스 체크아웃은 컴퓨터에 둔 채 필요한 파일을 동기화하고, SSH로 Docker 명령을 실행하며, 공개된 포트를 localhost로 전달한다. 빌드와 컨테이너는 서버의 CPU, 메모리, 디스크, 아키텍처를 사용한다.
CI 상태 · crates.io 패키지 · 라이선스
Ulak을 사용하면 로컬 Docker 데몬이나 Docker Desktop이 필요 없다. Ulak은 서버에 아무것도 설치하지 않으며 서버에서 네트워크 포트를 열지도 않는다. macOS와 Linux를 지원한다.
local checkout ⇄ sync over SSH ⇄ remote workspace
│
└── Docker / Compose
localhost ◀──── SSH tunnels ───────────┘
시작하기
Ulak을 설치한 뒤 Compose 파일이 있는 프로젝트에서 다음 명령을 실행한다.
cd ~/projects/my-app
ulak init my-server
ulak doctor
ulak docker compose up -d
my-server에는 SSH 별칭, 호스트 이름 또는 user@hostname 형식을 쓸 수 있다. 기본 설정에서는 터미널에서 처음 일반 명령을 실행할 때 실시간 동기화와 포트 전달을 맡는 로컬 백그라운드 서비스를 설치한다. 설정하거나 끄는 방법은 백그라운드 서비스 안내를 참고하면 된다.
init은 선택 사항인ulak.toml을 만든다. 파일을 직접 작성해도 된다.doctor는 컴퓨터와 서버, Compose 모델에 지정된 경로를 확인한다.up -d는 프로젝트를 동기화하고 서버에서 스택을 시작한다.
스택이 실행되면 Ulak은 원격 작업 공간을 계속 동기화한다. 컨테이너가 만든 파일(예: 생성된 코드나 마이그레이션 파일)은 로컬 체크아웃으로 돌아온다. 공개한 포트는 localhost에서 이용할 수 있다.
익숙한 Docker 명령 앞에 ulak을 붙여 사용한다.
ulak status
ulak docker compose logs -f
ulak docker compose exec app sh
ulak docker compose down
설치 조건
컴퓨터
macOS 또는 Linux, OpenSSH, rsync 3.2 이상이 필요하다. 애플에 기본 포함된 openrsync는 지원하지 않으므로 macOS에서는 Homebrew로 rsync를 설치해야 한다.
미리 빌드된 릴리스를 설치할 수 있다.
curl --proto '=https' --tlsv1.2 -LsSf \
https://github.com/syy/ulak/releases/latest/download/ulak-installer.sh | sh
Homebrew를 사용하면 rsync도 설치된다. Cargo로 설치하려면 Rust 1.89 이상이 필요하다.
brew install syy/tap/ulak
# Or build from source:
cargo install ulak
서버
서버에는 키 기반 SSH 접속, Docker Engine, rsync 3.2 이상, Python 3.7 이상이 필요하다. Compose 프로젝트에는 Docker Compose v2도 필요하다. SSH 사용자는 sudo 없이 Docker를 실행할 수 있어야 한다.
ulak doctor로 요구 사항을 확인하고, 빠진 도구를 설치하는 명령을 확인할 수 있다. 저장소를 복제한 경우 설정 전에 다음 명령으로 서버를 검사할 수 있다.
ssh my-server 'bash -s' < scripts/check-server.sh
Ulak은 Python 보조 프로그램을 SSH로 전송해 실행하며 서버에 설치하지 않는다. Python 3.9 이상을 쓰는 Linux 서버에서는 서버 쪽 파일 변경을 감시하는 기능을 사용할 수 있다. 그 밖의 지원 서버에서는 주기적으로 파일을 비교한다.
명령은 서버별 SSH 연결 하나를 공유한다. 각 명령은 Ulak이 서버에서 실행하는 보조 프로그램을 통해 해당 연결의 채널로 동작한다. 따라서 병렬 명령이 sshd의 MaxSessions 제한에 걸리지 않으며, 명령을 호출한 프로세스가 중단되면 서버 쪽 명령도 중단된다. 명령은 해당 연결의 로그인 환경, 그룹, 전달된 에이전트를 사용한다. 보조 프로그램은 10분마다 새 프로그램으로 교체되며 계정에 그룹이 추가되면 즉시 교체된다. 그 밖의 로그인 환경 변경은 최대 10분 안에 명령에 반영된다. 서버가 유닉스 소켓 전달(AllowStreamLocalForwarding)을 허용하지 않으면 일반 SSH 세션을 사용한다. 어떤 방식을 쓰는지는 ulak doctor에서 확인할 수 있다.
SSH Docker 컨텍스트와의 차이
DOCKER_HOST=ssh:// 방식은 Docker API 연결을 옮기지만, 바인드 마운트는 여전히 서버 파일 시스템의 경로를 가리킨다. Ulak은 필요한 파일을 먼저 동기화한 뒤 원격 작업 공간에서 Compose를 실행한다. 따라서 상대 경로 바인드 마운트, env_file, configs, 빌드 컨텍스트, ../shared 같은 형제 디렉터리 경로가 기존 의미를 유지한다.
파일 동기화와 충돌
Ulak은 서버의 docker compose config에 프로젝트에 필요한 로컬 경로를 묻고, 기본적으로 저장소 전체가 아니라 그 경로와 형제 디렉터리를 복사한다.
로컬에서 바꾼 파일은 서버로 전송하고, 컨테이너가 만들거나 수정한 파일은 로컬로 가져온다. 마지막으로 서로 내용이 일치했던 시점 이후 양쪽에서 파일을 다르게 수정했다면 Ulak은 양쪽 사본을 보존하고 충돌을 알린다. 파일을 자동 병합하지는 않는다.
공통으로 일치했던 내용의 기록이 없을 때는 기본값인 [sync] no_history = "send"가 적용된다. 이 설정은 경고를 표시한 뒤 서버의 다른 사본을 로컬 사본으로 바꾼다. 양쪽 사본을 모두 유지하려면 값을 "hold"로 설정한다. 충돌 해결, 삭제 규칙, 전송 제한은 동기화 안내에 설명돼 있다.
Docker 명령과 프로젝트 스크립트
지원되는 Docker 명령 앞에 ulak을 붙인다.
ulak docker build -t api .
ulak docker run --rm image
ulak docker cp app:/tmp/out.txt .
ulak docker compose -f compose.yaml -f compose.dev.yaml -p my-app up -d
ulak docker --help 또는 Docker 명령 안내에서 지원 명령을 확인할 수 있다. 안전하게 전달할 수 없는 명령은 거부된다.
docker를 호출하는 기존 프로젝트 스크립트는 임시 shim을 통해 실행할 수 있다.
ulak shim run -- ./scripts/start.sh
지속적으로 쓸 shim은 ulak shim install, ulak shim status, ulak shim uninstall로 관리한다. 설치할 때 셸 프로필을 바꾸기 전에 확인을 요청하며, 제거할 때는 Ulak이 추가한 블록만 지운다.
터미널 대시보드
ulak ui는 서버의 Docker 현황을 터미널 전체 화면으로 보여준다. Compose 프로젝트별 컨테이너와 실시간 CPU·메모리 사용량, 로그, 통계, 이미지, 볼륨, 네트워크를 확인할 수 있고 시작·중지·재시작·삭제도 한 번의 키 입력으로 실행할 수 있다.
cd ~/projects/my-app
ulak ui
대시보드는 베타 버전으로 키와 화면 배치가 바뀔 수 있다. 삭제 등 파괴적인 작업은 확인을 요청하며 기본 선택은 ‘아니요’다. 각 키의 기능과 서버에서 실행되는 작업은 대시보드 안내에서 확인할 수 있다.
주요 명령
| 명령 | 기능 |
|---|---|
ulak init [host] | 선택 사항인 프로젝트 설정을 만든다. |
ulak doctor [stack] | 로컬 도구, 서버, Compose 참조를 확인한다. |
ulak docker <command> | 지원되는 Docker 명령을 서버에서 실행한다. |
ulak sync [stack] | 현재 프로젝트를 한 번 동기화한다. |
ulak status [stack] | 작업 공간, 스택, 서비스, 전달된 포트를 표시한다. |
ulak ui | 컨테이너, 로그, 이미지, 볼륨, 네트워크를 보여주는 대시보드(베타)를 연다. |
ulak config | 적용되는 설정과 각 설정의 출처를 보여준다. |
ulak shim ... | 프로젝트 스크립트가 원격 Docker 데몬을 사용하도록 한다. |
ulak clean [stack] | 해당 스택이 사용하지 않는 선택된 원격 작업 공간을 제거한다. |
ulak clean --forget-destination <ssh-host> | 더 이상 존재하지 않는 서버에 체크아웃이 등록했던 항목을 서버에 접속하지 않고 정리한다. |
ulak service ... | 로컬 백그라운드 서비스를 설치·제거하거나 실행한다. |
ulak completions <shell> | Bash, Zsh, Fish 자동 완성을 만든다. |
[stack]은 Compose 프로젝트 이름이다. 하나의 체크아웃에서 여러 스택을 쓰는 방법은 스택 안내를 참고하면 된다.
ulak sync --dry-run은 업로드와 원격 삭제를 미리 보여주지만 다운로드는 미리 보여주지 않는다. 이 명령도 서버에 접속하며, 표시되는 업로드 목록은 실제 동기화 결과와 다를 수 있다. 자세한 내용은 미리 보기 안내에 있다.
host를 바꾸더라도 관리 명령은 실행 중인 스택에 기록된 서버를 계속 대상으로 삼을 수 있다. 서버 선택과 더 이상 존재하지 않는 서버의 정리는 스택 서버 변경 안내를 참고하면 된다.
설정
설정은 다음 순서로 적용되며 뒤의 항목이 앞의 항목을 덮어쓴다.
- 전역 설정:
~/.config/ulak/config.toml - 프로젝트 설정:
ulak.toml - 체크아웃 설정:
ulak.local.toml - 환경 변수
- 명령줄 옵션
프로젝트와 체크아웃 설정 파일은 Compose 파일 옆에 둔다. 목록 설정은 이전 목록을 대체한다. ulak config로 최종 설정을 확인할 수 있고, 스크립트에서는 ulak config --json을 사용할 수 있다. 처음으로 일반 대화형 명령을 실행하면 주석이 모두 포함된 전역 설정 파일이 만들어진다. 이전 프로젝트 설정 경로인 .config/ulak.toml은 무시된다.
host = "my-server"
[sync]
exclude = ["node_modules", ".venv", "target"]
protect = ["data/", "uploads/"]
max_delete = 25
[sync]의 경로는 스택이 프로젝트 디렉터리 밖의 항목을 마운트하더라도 Compose 프로젝트 디렉터리를 기준으로 한다. 형제 폴더는 ../web/uploads/처럼 지정한다. node_modules처럼 슬래시가 없는 패턴은 전송되는 모든 폴더의 모든 깊이에서 일치한다.
[sync] include는 제외 규칙이나 .dockerignore 때문에 빠질 경로를 전송 대상으로 포함한다. 기본값은 .env이며, 직접 설정하면 기본값을 대체한다. .env를 로컬에만 두려면 이를 제외하고 include에도 넣지 않는다.
서버 소유 데이터 보호
[sync] protect에 지정한 경로는 업로드·다운로드·삭제하지 않는다. 데이터베이스 파일이나 업로드 파일에 사용해야 하며, 다시 가져오려는 소스 코드나 생성 파일에는 사용하지 않는다. 쓰기 가능한 마운트 중 보호가 필요할 수 있는 항목은 ulak doctor가 제안한다.
모든 설정, SSH 키, 에이전트 설정은 환경 변수 안내를 참고하면 된다.
백그라운드 서비스
처음으로 일반 대화형 명령을 실행하면 macOS에서는 launchd, Linux에서는 systemd를 이용해 로컬 사용자 서비스를 설치한다. 실행 중인 스택이 있는 동안 파일 동기화와 localhost 포트 연결을 유지하고, 연결이 끊기면 다시 연결한다. 비대화형 실행이나 서버에는 자동으로 설치하지 않는다.
자동 설치를 끄려면 전역 설정에 다음을 지정한다.
[service]
auto = false
| 명령 | 기능 |
|---|---|
ulak service run --foreground | 연속 동기화와 터널을 직접 실행한다. |
ulak service install | 서비스를 설치하거나 갱신한다. 업그레이드 후 실행한다. |
ulak service uninstall | 설치된 서비스를 중지하고 제거한다. |
나중에 터미널 명령을 실행해도 서비스를 다시 설치하지 않도록 auto = false를 유지한다. 작은 변경은 지속적인 SSH 연결을 이용하고 큰 전송은 rsync를 사용한다. 서버의 변경 사항 감지와 전체 비교는 동기화 안내를 참고하면 된다.
포트와 접근 범위
서버의 루프백 주소에 포트를 게시하려면 다음처럼 설정한다.
services:
web:
ports:
- "127.0.0.1:8080:80"
스택과 백그라운드 서비스가 실행 중이면 http://localhost:8080에서 접속할 수 있다. 로컬 포트가 이미 사용 중이면 해당 포트에 대해 알리며, 다른 포트는 계속 연다.
"8080:80"은 서버의 모든 인터페이스에 포트를 게시한다. localhost 터널을 사용해도 서버 포트 자체가 비공개가 되는 것은 아니며, Docker의 포트 규칙은 UFW를 우회할 수 있다. ulak doctor는 외부에 공개되는 바인딩을 경고한다.
포트 전달은 TCP만 지원하며 서버에서 컴퓨터 방향으로 동작한다. UDP와 network_mode: host 포트의 자동 전달은 지원하지 않는다. 컨테이너는 이 터널을 통해 컴퓨터에서 실행 중인 서비스에 접속할 수 없다.
데이터 안전과 삭제
- Ulak은
rsync --delete를 사용하지 않으며 가져오기 과정에서 로컬 파일을 삭제하지 않는다. - 원격 파일은 추적 중인 파일이 로컬에 없을 때만 삭제 대상이 된다. 마지막으로 합의한 내용 이후 서버에서 수정된 파일은 보존해 다시 가져온다.
- 합의한 내용이 없으면 기록된 전송 내역이 서버 파일을 삭제할 수 있다. 서버에 도착하지 않은 것으로 확인된 전송은 예외다. 자세한 내용은 삭제 안전 규칙을 참고하면 된다.
- 보호 경로와 소유자가 확인되지 않은 서버 생성 파일은 삭제하지 않는다.
- 원격 파일을 25개보다 많이 삭제하려면 기본적으로 확인이 필요하다.
sync.max_delete또는--max-delete로 한도를 바꿀 수 있다.
작업 공간은 클라이언트와 체크아웃별로 분리되며 변경 중에는 잠긴다. Ulak은 식별자 충돌도 확인한다. SSH 호스트 키 확인은 계속 활성화된다.
알려진 제한
- Compose의 절대 경로와
~경로는 서버를 기준으로 한다.ulak doctor가 이를 확인한다. - 충돌은 수동으로 해결해야 한다. Ulak은 파일을 병합하거나 충돌 파일을 만들지 않는다.
- rsync는 파일을 쓰기 전에 비교하지만, 비교 중 서버에서 파일을 수정하면 그 변경이 덮어써질 수 있다. 컨테이너가 계속 덮어쓰는 파일은 로컬에서 동시에 편집하지 않는 것이 좋다.
- 다이제스트 캐시는 파일 시스템 시계가 일관된다고 가정한다. 시계 오차가 있는 네트워크 파일 시스템에서는 변경 사항을 놓칠 수 있다.
충돌 해결, 경고 확인, 다이제스트 캐시 초기화는 동기화 안내를 참고하면 된다.
에이전트에서 Ulak 사용하기
ULAK_HOST를 설정하고 환경 변수를 통해 SSH 인증 정보를 제공한다. 작업 공간의 네임스페이스와 UUID를 안정적으로 유지하고, 삭제 추적과 공통 내용 기록을 위해 상태 디렉터리를 보존해야 한다. 기록이 없으면 기본값인 sync.no_history = "send"가 서버의 다른 사본을 대체할 수 있다.
식별 정보, 인증 정보, 호스트 키 확인은 에이전트 설정 안내를 참고하면 된다.
데모
저장소를 복제한 뒤 다음 명령으로 데모를 실행할 수 있다.
cd examples/demo
ulak init my-server
ulak doctor
ulak docker compose up -d --build
ulak docker compose logs prober
# Open http://localhost:8080
ulak docker compose down
ulak clean
기여와 라이선스
검사 항목과 커밋 형식은 CONTRIBUTING.md, 설계 안내는 AGENTS.md에서 확인할 수 있다. 취약점을 비공개로 신고하려면 SECURITY.md를 참고한다. 릴리스 내역은 CHANGELOG.md에 있다.
라이선스는 선택에 따라 Apache-2.0 또는 MIT다.
댓글 (0)
로그인하면 이 기사에 내 생각을 남길 수 있어요