본문으로 건너뛰기
5분 AI.
GITHUB TRENDING PHP DAILY #5 딥다이브 · 2026-10-23 분석

coolify 딥다이브
— 배포 버튼 뒤의 큐·SSH·컨테이너를 읽다

배포 플랫폼

빠른 판단

무엇
Git 연결·빌드·배포·DB·도메인·로그를 웹 화면에서 관리하는 셀프호스팅 PaaS
이럴 때
자체 서버의 앱·데이터베이스를 배포하고 SSH·Docker 기반 운영 작업을 공통 화면으로 관리할 때
Git 저장소를 연결하고 배포 버튼을 누르면, 누가 코드를 내려받고 이미지를 만들며 새 컨테이너로 바꿔 줄까요? Coolify는 그 과정을 내 서버에서 관리하는 셀프호스팅 PaaS입니다. Laravel·Livewire 화면에서 Redis 작업 큐와 SSH를 거쳐 Docker까지 이어지는 길을 따라가며, 작은 웹앱이 운영 도구로 커질 때 필요한 설계를 배웁니다. 저장소 coollabsio/coolify · ⭐63,525 · 포크 5,714 · PHP · Apache-2.0 · 최신 릴리스 v4.5.0. 수치는 2026-10-23 GitHub 기준, 코드 설명은 커밋 ade4bae 기준입니다.
목차
  1. 프로젝트 한줄 요약
  2. 왜 주목받는가
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 분석
  5. 디렉토리 구조 해부
  6. 학습 포인트 (기술별)
  7. 하드웨어 / 시스템 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한줄 요약

내 서버에 만드는 배포 관리실

Coolify는 서버·애플리케이션·데이터베이스·서비스를 웹 화면에서 배포하고 관리하는 오픈소스 플랫폼입니다. Heroku·Vercel·Netlify와 비슷한 배포 경험을 지향하지만, 실행 장소로 내가 관리하는 서버를 선택합니다. Coolify를 공부하면 웹 화면의 요청이 실제 서버의 상태 변경으로 이어지는 전 과정을 읽을 수 있습니다.

간단한 게시판은 DB에 글을 저장하면 일이 끝납니다. 배포 도구는 다릅니다. 요청을 저장한 뒤에도 다른 서버에 접속하고, 빌드를 기다리고, 컨테이너 상태를 확인해야 합니다. 중간에 연결이 끊기거나 사용자가 취소할 수 있고, 같은 배포 버튼이 두 번 눌릴 수도 있습니다. 이 저장소의 학습 가치는 그런 현실적인 문제를 다룬다는 데 있습니다.

용어부터
PaaS — 배포에 필요한 일을 묶어 주는 플랫폼
애플리케이션을 올릴 때 필요한 빌드·실행·도메인 연결·로그 확인을 한곳에서 다루는 서비스입니다. Coolify의 셀프호스팅 방식에서는 플랫폼 자체의 설치·업데이트·백업도 운영자가 맡습니다.
한 문장 비유

Coolify는 여러 주방에 주문을 전달하는 식당의 관리실입니다

웹 화면은 주문받는 창구, PostgreSQL은 주문 장부, Redis 큐는 대기표, 작업자는 요리 담당자입니다. SSH는 주방에 지시하는 통로이고, Docker는 요리를 정해진 환경에서 만드는 조리 시설에 해당합니다.

손님에게 주문표를 발급했다고 음식이 완성된 것은 아닙니다. 마찬가지로 배포 요청을 접수했다는 응답과 컨테이너가 정상 실행된다는 사실은 구별해야 합니다. 이 구별이 뒤에서 다룰 상태·큐·이벤트 설계의 출발점입니다.

사용자가 보는 흐름과 코드가 하는 일

사용자 행동내부에서 일어나는 일처음 읽을 위치
서버 등록SSH로 관리할 서버와 연결 정보를 모델로 보관합니다.app/Models/Server.php
앱 설정Git 출처·빌드 방식·환경변수·도메인을 연결합니다.app/Models/Application.php
배포 클릭권한과 사전 조건을 확인하고 배포 요청을 접수합니다.Heading::deploy()
진행 상황 확인작업자가 원격 명령을 수행하고 로그와 상태를 남깁니다.ApplicationDeploymentJob::handle()
도메인 접속프록시가 요청을 실행 중인 애플리케이션으로 보냅니다.bootstrap/helpers/proxy.php

처음부터 모든 서비스를 이해할 필요는 없습니다. app/Livewire/Project/Application/Heading.php의 deploy(), bootstrap/helpers/applications.php의 queue_application_deployment(), app/Jobs/ApplicationDeploymentJob.php의 handle()을 순서대로 읽는 것부터 시작하면 됩니다.

읽기 목표
배포 버튼을 세 문장으로 설명하기

화면은 권한과 설정을 검사한다. 접수 함수는 중복과 혼잡을 통제하며 배포 기록을 만든다. 작업자는 그 기록을 받아 서버에서 명령을 수행한다. 이 세 문장을 실제 함수에 대응시키면 큰 구조를 잡은 것입니다.

2왜 주목받는가

트렌딩 순위보다 오래 남는 학습 가치

2-1. 배포 편의와 서버 통제를 함께 원하는 수요

2026-10-23 기준 GitHub Trending PHP Daily #5, 스타 63,525개·포크 5,714개인 프로젝트입니다. 다만 순위와 누적 스타만으로 당일 인기가 오른 원인을 확정할 수는 없습니다. 아래는 README의 기능 설명과 구현에서 읽을 수 있는 관심 요인에 대한 해석입니다. 일간 스타 증가량이나 특정 사건의 영향은 확인된 수치로 제시하지 않습니다.

첫째, Git 저장소에서 배포까지 이어지는 편의를 자기 서버에 가져올 수 있습니다. 둘째, 정적 사이트뿐 아니라 데이터베이스와 여러 컨테이너가 묶인 서비스도 같은 관리 화면에서 다룹니다. 셋째, 웹 UI 외에 API와 MCP 진입점이 있어 자동화에서 접근할 수 있습니다. 각각은 README.md, 배포 작업자의 빌드 분기, routes/api.php와 routes/ai.php에서 교차 확인할 수 있습니다.

편한 배포 화면을 제공하면서도 실행 서버와 운영 설정을 직접 다룰 수 있다는 점이 Coolify의 핵심 매력입니다. 대신 그 통제권에는 서버 비용, 장애 대응, 비밀정보 관리라는 책임이 따라옵니다.

2-2. 배포 선택지를 같은 질문으로 비교하기

다음 표는 가격이나 특정 요금제의 기능 비교가 아니라, 대표적인 운영 방식의 차이를 비교합니다. 관리형 제품의 세부 기능은 바뀔 수 있으므로 여기서는 실행 위치와 운영 책임에 초점을 맞춥니다.

선택지배포 편의실행·운영의 중심Coolify에서 배울 점
Vercel / NetlifyGit 중심의 관리형 배포 경험제공사가 관리하는 배포 환경비슷한 사용자 경험을 내 서버용 제품으로 만드는 방법
Heroku애플리케이션 중심의 관리형 PaaS제공사의 실행 플랫폼빌드·실행·설정 변경을 하나의 도메인 모델로 묶는 방법
Docker Compose 직접 운영설정 파일과 명령을 직접 관리운영자가 서버와 배포 절차를 담당명령 실행에 권한·큐·로그·상태 화면을 덧붙이는 방법
Coolify웹 UI·API에서 Git·이미지·Compose를 관리셀프호스팅 시 플랫폼과 대상 서버를 운영자가 관리웹앱과 인프라 자동화 사이의 경계를 설계하는 방법
비유

완성된 건물에 입주하는 것과, 내 건물에 관리 시스템을 설치하는 것의 차이입니다. 두 번째 방식은 원하는 시설을 붙이기 쉽지만, 건물의 전기와 배관을 고칠 책임도 남습니다. 배포 버튼 하나가 그 책임까지 없애지는 않습니다.

2-3. README의 문구를 소스와 함께 읽기

README의 설명소스에서 확인한 근거해석할 때 주의할 점
360개 이상 원클릭 서비스templates/compose/ 바로 아래 .yaml 파일 382개파일에는 변형·비노출 템플릿도 있을 수 있어 메뉴에 표시되는 서비스 수와 같다고 단정하지 않습니다.
다양한 앱 배포decide_what_to_do()가 Docker 이미지·Compose·Dockerfile·static·Nixpacks·Railpack으로 분기빌드 방식마다 경로가 다릅니다. 한 방식의 성공이 모든 방식의 검증은 아닙니다.
Coolify 중단 후에도 리소스 유지관리 서버가 SSH로 대상 호스트의 컨테이너와 설정을 조작하는 구조컨테이너가 유지될 수 있다는 뜻입니다. 자동 배포·인증서 갱신·관리가 계속 보장된다는 뜻으로 넓히면 안 됩니다.
API·MCP와 연동Sanctum 기반 API와 Mcp::web('/mcp', ...) 등록MCP는 활성화·토큰·팀 검사를 거치는 관리 인터페이스입니다.

2-4. 초보자가 얻을 수 있는 세 가지 관점

비교의 한계
셀프호스팅의 장점을 전체 비용의 우위로 바꾸어 읽지 않기

Apache-2.0으로 공개된 소프트웨어라도 서버·스토리지·백업·관리 시간은 필요합니다. 이 글은 비용 실측이나 관리형 서비스의 성능 비교를 제공하지 않습니다. 선택의 기준은 자신의 운영 역량과 필요한 기능입니다.

3기술 스택 전체 지도

의존성 선언, 잠금 버전, 실제 사용 위치를 함께 보기

버전을 읽을 때는 세 층을 구별합니다. composer.json의 ^12.69.3은 허용 범위이고, composer.lock의 v12.69.3은 확정된 설치 버전입니다. Dockerfile의 베이스 이미지 태그는 실행 환경을 결정합니다. 셋은 같은 질문에 답하는 파일이 아닙니다.

3-1. 백엔드 — Laravel이 관리 화면과 작업자를 연결한다

구성선언 → 잠금 버전이 저장소에서의 역할
PHP^8.4 → 언어 조건composer.json의 하한입니다. 운영 이미지는 PHP 8.4 계열을 선택합니다.
Laravel^12.69.3 → v12.69.3라우팅·Eloquent 모델·권한·큐·이벤트의 기반
Livewire^3.8.10 → v3.8.10PHP 컴포넌트의 상태와 동작을 웹 화면에 연결
Horizon^5.50.0 → v5.50.0Redis 작업 큐의 워커 구성·관찰
Reverb^1.12.0 → v1.12.0실시간 상태 이벤트를 전달하는 WebSocket 서버
Sanctum^4.3.3 → v4.3.3API·MCP 진입점의 토큰 인증
Fortify^1.40.0 → v1.40.0사용자 인증 기능의 기반
Laravel Actions^2.12.0 → v2.12.0app/Actions/의 작업 단위 구성
Laravel MCP^1.0.1 → v1.0.1app/Mcp/의 관리 도구·리소스·프롬프트 제공
Symfony YAML^8.1.8 → v8.1.8Compose 내용을 구조화해서 다루는 데 사용

표의 패키지 버전은 모두 composer.json과 composer.lock 기준입니다. phpseclib/phpseclib도 3.0.57로 잠겨 있지만, 아래에서 추적하는 배포 명령 경로는 Process::start()와 SSH 명령 생성 도우미를 사용합니다. 의존성 목록만 보고 핵심 통신 경로를 추정하면 놓치는 부분입니다.

용어부터
의존성 잠금 파일
같은 패키지 이름이라도 설치 날짜마다 버전이 달라질 수 있습니다. 잠금 파일은 선택된 버전과 해시 등의 정보를 기록해 재현성을 높입니다. 언어 하한, 전체 패키지의 호환 조건, 실제 이미지의 PHP 버전은 함께 확인해야 합니다.

3-2. 프론트엔드 — Livewire 중심에 Svelte 캔버스를 더한다

구성package.json 값확인할 위치·역할
Vite8.3.4vite.config.js가 CSS·JS 진입점을 지정합니다.
Laravel Vite 연동3.1.3laravel-vite-plugin으로 서버 템플릿과 빌드 자산을 연결합니다.
Tailwind CSS4.3.3@tailwindcss/postcss도 4.3.3입니다.
Svelte5.57.2resources/js/canvas/의 배포·환경 캔버스에 사용합니다.
Svelte Vite 플러그인7.3.1캔버스 컴포넌트를 프론트엔드 자산으로 빌드합니다.
xterm@xterm/xterm 6.0.0브라우저 터미널, 크기 조절용 addon-fit 0.11.0 포함
가상 목록 도구@tanstack/virtual-core 3.17.11많은 항목을 표시하는 화면을 위한 의존성
브라우저 자동화playwright 1.62.1브라우저 기반 검증을 위한 의존성

프레임워크 이름이 여러 개 보여도, 어느 화면의 상태를 누가 소유하는지 찾으면 구조가 단순해집니다. 주된 페이지와 동작은 app/Livewire/와 resources/views/에 있고, Svelte는 특정 시각화 영역을 담당합니다. 전체를 Svelte 단일 페이지 앱이라고 부르면 실제 구조와 어긋납니다.

resources/js/app.js는 [data-deploy-canvas] 또는 [data-environment-canvas]가 있을 때 해당 모듈을 동적으로 불러옵니다. 첫 화면 로딩뿐 아니라 livewire:navigated 이벤트에도 이 검사를 실행합니다. 서버 중심 화면에 복잡한 클라이언트 컴포넌트를 붙이는 사례로 읽을 수 있습니다.

// resources/js/app.js — 동적 로딩 구조의 일부
const canvases = [
    {
        selector: '[data-deploy-canvas]',
        load: () => import('./canvas/deploy/index.js')
            .then((m) => m.initializeDeployCanvas())
    },
    // 환경 캔버스도 별도 항목으로 등록됩니다.
];
// 요소 존재 여부를 확인한 뒤 필요한 모듈만 불러옵니다.

3-3. 인프라·빌드 — 앱 하나 안에도 여러 프로세스가 있다

구성파일의 실제 설정읽어야 할 의미
운영 PHP 이미지8.4-fpm-nginx-alpine-v5.0.2docker/production/Dockerfile의 SERVERSIDEUP_PHP_VERSION
Node.js24.22.1정적 자산 빌드와 운영 터미널 서버에 사용
Nginx1.31.2-r1운영 이미지에서 패키지 버전을 명시
PostgreSQL서버 postgres:15-alpine, 클라이언트 18DB 서버 버전과 이미지 안의 백업·접속 도구 버전이 다릅니다.
Redisredis:7-alpine작업 큐·캐시 등의 기반. Compose 태그는 패치까지 고정하지 않습니다.
리버스 프록시traefik:v3.7bootstrap/helpers/proxy.php의 생성 설정 경로에서 확인
웹 터미널 서버ws 8.21.3, node-pty 1.1.0docker/coolify-terminal/package.json에 별도 의존성 명시
보조 도구 이미지helper 1.0.18, sentinel 1.1.2versions.json의 참조 버전

운영 Dockerfile은 Composer 의존성, 프론트엔드 자산, MinIO 클라이언트, 터미널 의존성을 별도 단계에서 준비한 뒤 최종 이미지에 복사합니다. 특히 node-pty는 네이티브 모듈이므로 운영 Node.js의 ABI와 libc에 맞춰 다시 빌드합니다. 단순히 JavaScript 파일만 복사하면 끝나지 않는 사례입니다.

.github/workflows/coolify-main-build.yml은 AMD64와 ARM 계열 러너에서 이미지를 각각 빌드하고 Docker Hub·GHCR에 올린 뒤 멀티아키텍처 매니페스트를 만듭니다. 버전 계산 단계의 php:8.2-alpine3.16은 버전 문자열을 읽는 도구일 뿐, Coolify 본체가 PHP 8.2에서 실행된다는 근거가 아닙니다.

문서와 코드의 차이
Laravel 11이라는 설명은 현재 잠금 버전과 다릅니다

TECH_STACK.md는 Laravel 11을 적고 있지만 composer.lock은 12.69.3입니다. AGENTS.md에는 PHP 8.5 안내가 있으나 composer.json의 조건은 ^8.4, 운영 Dockerfile의 선택은 8.4 계열입니다. 이 글의 버전표는 실제 의존성·빌드 파일을 기준으로 합니다.

Laravel 버전과 폴더 구조도 같은 것이 아닙니다. app/Http/Kernel.php·app/Console/Kernel.php를 유지하는 구조이므로 최신 입문 예제의 파일 배치와 다를 수 있습니다.

4아키텍처 심화 분석

접수·실행·상태·권한이라는 네 가지 경계

4-0. 전체 그림 — 관리 경로와 서비스 요청 경로

Coolify 관리 화면에 접속하는 요청과, 배포된 앱에 접속하는 요청은 다른 길을 갑니다. 아래 그림은 일반적인 Docker 기반 배포를 설명하기 위한 축약도입니다. 관리 서버와 대상 서버를 한 호스트에 둘 수도 있지만 역할은 나누어 이해하는 편이 좋습니다.

[운영자 브라우저]                         [Git 제공자 / 자동화]
       | 관리 HTTP·WebSocket                  | 웹훅 / API
       v                                      v
+-------------------- Coolify 관리 영역 ---------------------+
| Nginx + PHP/Laravel                                        |
|   Livewire Heading::deploy()                              |
|       | 권한·설정 검사                                    |
|       v                                                   |
|   queue_application_deployment() ---> PostgreSQL           |
|       |                              배포 기록·설정       |
|       v                                                   |
|   Redis 큐 ---> Horizon 워커                               |
|                   ApplicationDeploymentJob                |
|                        |                                  |
|   Reverb <--- 팀 전용 이벤트·상태 갱신                     |
|   Node 터미널 서버는 별도 WebSocket 경로                   |
+------------------------|----------------------------------+
                         | SSH 명령
                         v
+-------------------- 배포 대상 서버 ------------------------+
| Docker / Compose                                         |
|  보조 빌드 컨테이너 -> 앱 이미지 -> 앱 컨테이너           |
|                                        |                 |
|                  볼륨·환경변수·헬스체크 |                 |
| Traefik 등 프록시 ---------------------+                 |
+------^---------------------------------------------------+
       | HTTPS
[배포된 서비스를 사용하는 방문자]

제어하는 쪽을 제어 영역, 실제 앱 트래픽이 흐르는 쪽을 서비스 실행 영역이라고 생각해 봅시다. 관리 UI가 느린 현상과 앱 자체가 느린 현상은 원인이 다를 수 있습니다. 그림에서 어느 경로의 문제인지 표시하는 것만으로 장애 분석의 첫 질문이 달라집니다.

4-1. 패턴 ① 트랜잭션으로 접수하고 큐에서 실행한다

Heading::deploy()는 $this->authorize('deploy', $this->application)로 권한을 확인합니다. Compose 원본의 존재, 여러 서버 또는 빌드 서버 사용 시 이미지 이름 같은 조건도 검사합니다. 검사를 통과하면 배포 식별자를 만들고 접수 함수에 넘깁니다.

접수 함수인 queue_application_deployment()는 DB 트랜잭션 안에서 애플리케이션 행과 서버 행을 잠급니다. 대기 중인 배포가 없을 때는 잠글 대기 행 자체가 없기 때문에, 항상 존재하는 모델의 행을 기준으로 삼습니다. 동시에 두 요청이 들어오는 상황을 생각하면 이 선택의 이유가 드러납니다.

// bootstrap/helpers/applications.php — 트랜잭션 안의 일부
Application::query()
    ->whereKey($application_id)
    ->lockForUpdate()
    ->firstOrFail();
Server::query()
    ->whereKey($server_id)
    ->lockForUpdate()
    ->firstOrFail();

// 실제 코드는 서버 설정을 읽고 대기 수를 셉니다.
// deployment_queue_limit 미설정 시 기본값은 25입니다.
  1. 혼잡 검사: 해당 서버의 queued 배포 수와 큐 한도를 비교합니다. 한도는 동시 실행 수와 다른 개념입니다.
  2. 중복 검사: 앱·커밋·PR 번호·이미지 태그가 같은 진행 중/대기 중 배포를 찾습니다.
  3. 예외 처리: 강제 재빌드·롤백·강제 시작 조건에서는 중복 검사 결과를 그대로 적용하지 않을 수 있습니다.
  4. 기록 생성: ApplicationDeploymentQueue에 요청과 대상 서버 정보를 저장합니다.
  5. 시작 결정: 잠금을 유지한 상태에서 next_queuable()로 시작 가능 여부를 판단합니다.
  6. 작업 전달: 시작 대상으로 정해진 배포만 ApplicationDeploymentJob::dispatch()로 보냅니다.
용어부터
트랜잭션과 행 잠금
트랜잭션은 여러 DB 변경을 한 묶음으로 확정하거나 되돌립니다. 행 잠금은 같은 행을 바꾸는 다른 트랜잭션이 기다리게 만듭니다. 여기서는 “아직 배포가 없네”라는 판단을 두 요청이 동시에 내리는 문제를 줄이는 데 쓰입니다.

config/queue.php의 기본 연결은 redis이고, Redis 연결에는 after_commit: true가 설정되어 있습니다. deployment_queue()는 셀프호스팅에서 high, Cloud에서 deployments를 반환합니다. 따라서 배포 큐의 이름도 운영 모드와 함께 읽어야 합니다.

응답의 status: queued는 접수 결과입니다. 접수 함수는 바로 시작하도록 결정한 배포 기록을 이미 in_progress로 바꿀 수도 있습니다. API 응답 문자열과 DB 상태가 항상 같은 순간을 표현한다고 가정하면 잘못 읽게 됩니다.

이 설계를 “정확히 한 번 실행 보장”으로 확대해서 설명하면 안 됩니다. DB 기록과 Redis 작업 전달은 서로 다른 시스템의 일입니다. 코드에는 작업 전달 실패를 처리하는 fail_undispatchable_deployment()가 있으며, 관련 회귀 사례는 tests/Feature/ImmediateDeploymentDispatchFailureTest.php에서 읽을 수 있습니다.

4-2. 패턴 ② 빌드 전략 선택과 원격 실행의 책임을 나눈다

ApplicationDeploymentJob::decide_what_to_do()는 설정에 따라 여러 배포 경로를 고릅니다. 독립적인 전략 클래스 묶음으로 완전히 분리된 형태는 아니며, 큰 작업자 내부의 조건 분기와 전용 메서드로 구현되어 있습니다. “전략을 선택한다”는 설계 의도와 실제 코드 모양을 구별해서 읽어야 합니다.

// decide_what_to_do()의 경로를 학습용으로 축약
restart_only       -> just_restart()
dockerimage        -> deploy_dockerimage_buildpack()
PR 배포 검사      -> deploy_pull_request()
직접 작성 Dockerfile -> deploy_simple_dockerfile()
dockercompose      -> deploy_docker_compose_buildpack()
dockerfile         -> deploy_dockerfile_buildpack()
static             -> deploy_static_buildpack()
nixpacks           -> deploy_nixpacks_buildpack()
railpack           -> deploy_railpack_buildpack()
지원하지 않는 값  -> DeploymentException

분기의 순서도 동작의 일부입니다. 재시작 전용·Docker 이미지 분기 다음에 PR 여부를 검사하고, 나머지 빌드 방식으로 넘어갑니다. 직접 작성한 Dockerfile도 별도 경로를 가집니다. app/Enums/BuildPackTypes.php에는 dockerimage 항목이 없지만 실제 작업자에는 그 처리 분기가 있습니다. 열거형 하나만으로 모든 지원 방식을 파악할 수 없는 구체적인 예입니다.

명령 수행의 공통 부분은 app/Traits/ExecuteRemoteCommand.php의 execute_remote_command()로 모읍니다. 서버 모델 타입 확인, 비루트 사용자의 sudo 처리, 취소 확인, SSH 실패 재시도, 출력 기록을 배포 단계와 분리하려는 구조입니다. PHP의 trait은 클래스 여러 곳에서 메서드 묶음을 재사용하는 장치입니다.

// ExecuteRemoteCommand::executeCommandWithProcess()의 일부
$remote_command = $input === null
    ? SshMultiplexingHelper::generateSshCommand($this->server, $command)
    : SshMultiplexingHelper::generateSshStdinCommand($this->server, $command);

// 생성한 명령은 Laravel Process로 시작합니다.
// 실제 코드에는 timeout, idleTimeout, input과 출력 콜백이 붙습니다.
// stdout·stderr를 배포 로그로 남기고 종료 코드를 확인합니다.

작업자 속성의 기본 $tries = 1과 명령 실행 도중의 재시도는 다른 층입니다. 작업 전체를 무조건 반복하지 않아도, 일시적인 SSH 연결 실패만 골라 재시도할 수 있습니다. 모든 실패를 재시도하면 이미 수행한 데이터 변경을 중복 수행할 수 있다는 점을 함께 생각해 봅시다.

출력은 도착한 조각 그대로 로그에 넣지 않고 완성된 줄을 모읍니다. 비밀값이 두 출력 조각에 나뉘어 도착했을 때도 마스킹할 수 있도록 RunRemoteProcess::takeCompleteOutput()을 사용합니다. 배포 작업은 ShouldBeEncrypted도 구현하지만, 큐 페이로드 암호화와 로그 비밀값 마스킹은 각각 필요한 보호입니다.

비유

요리 종류에 따라 레시피는 달라도 주문 번호 확인, 취소 확인, 작업 기록은 공통입니다. 빌드별 메서드는 레시피, 원격 명령 trait은 공통 작업 절차에 가깝습니다. 다만 공통 절차가 커지면 숨은 의존성도 늘어나므로 사용하는 속성을 함께 읽어야 합니다.

4-3. 패턴 ③ 종료 상태를 먼저 보존하고 부수 효과를 격리한다

배포 기록의 상태는 app/Enums/ApplicationDeploymentStatus.php에 정의되어 있습니다. 아래는 기본적인 진행 관계를 설명한 그림이며, 모든 예외 경로를 열거한 것은 아닙니다. 취소는 실행 전후의 여러 지점에서 확인됩니다.

queued ---------> in_progress ---------> finished
  |                    |
  |                    +--------------> failed
  |                    |
  +--------------------+--------------> cancelled-by-user

finished / failed / cancelled-by-user = 종료 상태
그다음 알림 실패가 발생해도 배포 결과를 함부로 덮어쓰지 않기

transitionToStatus()는 이미 종료 상태인지 확인한 뒤 상태를 저장합니다. 이어지는 handleStatusTransition()과 queue_next_deployment()의 실패는 각각 잡아서 기록합니다. 새 버전이 이미 배포되었는데 알림 전송 실패 때문에 전체 배포를 실패로 바꾸면 운영자가 잘못된 판단을 내릴 수 있기 때문입니다.

주된 작업의 결과와 그 결과를 알리는 작업의 실패를 분리해야, 운영 화면이 현실을 더 정확히 설명합니다. 상태를 저장하는 단계와 알림·이벤트·다음 작업 시작을 한 덩어리의 성공으로 취급하지 않는 것이 핵심입니다.

tests/Feature/ApplicationDeploymentStatusSideEffectsTest.php는 이 의도를 보여 줍니다. 성공 후 이벤트 처리에서 예외가 나도록 만들고, 현재 배포는 finished를 유지하며 다음 배포는 in_progress가 되는지 확인합니다. 파일 이름을 읽는 데서 그치지 말고, 어떤 실패를 의도적으로 주입하는지 보세요.

롤링 업데이트라는 이름에도 조건이 있다

같은 작업자의 rolling_update()에서 일반적인 경로는 새 컨테이너 시작, 헬스체크, 기존 컨테이너 정리 순서입니다. 그러나 호스트 포트 직접 매핑, 일관된 컨테이너 이름, PR 배포, 고정 IP 옵션 등의 조건이 있으면 먼저 기존 컨테이너를 멈추는 경로로 들어갑니다. Swarm은 별도의 docker stack deploy 경로입니다.

health_check()는 헬스체크가 비활성화되고 사용자 정의 검사도 없으면 정상 여부 플래그를 바로 설정할 수 있습니다. 따라서 “배포가 끝났다”, “컨테이너 검사에 통과했다”, “서비스의 실제 기능이 정상이다”는 서로 다른 수준의 판단입니다.

운영 해석
모든 배포가 무중단이라고 읽으면 안 됩니다

포트·이름 충돌 때문에 두 컨테이너를 동시에 띄울 수 없는 경우가 있습니다. 헬스체크도 앱의 결제·로그인·DB 쓰기까지 자동 검증하지는 않습니다. 먼저 중지하는 분기와 검사 생략 조건을 실제 요구사항에 대입해 봐야 합니다.

4-4. 패턴 ④ 팀을 권한과 이벤트 전달의 경계로 삼는다

ApplicationPolicy::deploy()는 대상 애플리케이션의 팀을 구하고 isAdminOfTeam($teamId)로 그 팀의 admin 또는 owner 역할인지 확인합니다. 중요한 기준은 “현재 브라우저에서 선택한 팀의 관리자냐”가 아니라 “이 애플리케이션이 속한 팀의 관리자냐”입니다. 사용자가 다른 탭에서 팀을 바꾸어도 리소스의 소유 관계는 바뀌지 않습니다.

// app/Policies/ApplicationPolicy.php
public function deploy(User $user, Application $application): bool
{
    $teamId = $this->getTeamId($application);

    return $teamId !== null && $user->isAdminOfTeam($teamId);
}

실시간 이벤트도 같은 경계를 가집니다. app/Traits/BroadcastsToTeam.php는 명시적으로 전달받은 팀 ID로 PrivateChannel("team.{$this->teamId}")을 만듭니다. routes/channels.php는 구독하려는 사용자가 그 팀에 속하는지 검사합니다. 채널 이름만 비공개처럼 지어 놓는 것으로는 접근 통제가 되지 않습니다.

ListensToTeamChannel은 처음 연결한 팀 ID를 #[Locked] 속성으로 보관하고, 세션 팀이 바뀌어 이전 팀의 이벤트가 들어오면 ignoreStaleTeamChannelEvent()로 처리해 렌더링을 건너뜁니다. 이 장치는 오래 열린 화면과 바뀐 세션 상태 사이의 차이를 다룹니다.

Heading::getListeners()는 ServiceStatusChanged를 받아 checkStatus()를 호출하고, 그 메서드는 GetContainersStatus 작업을 전달할 수 있습니다. 이벤트는 “이 화면을 다시 확인할 이유가 생겼다”는 신호가 될 수 있으며, 언제나 완전한 최신 데이터 자체를 싣는 것은 아닙니다.

경계확인할 구현학습 질문
버튼 동작Heading::deploy()의 authorize()화면에서 버튼을 숨겨도 서버 검사는 왜 필요한가?
리소스 소유ApplicationPolicy세션 팀과 대상 리소스의 팀이 다르면 누구 기준인가?
이벤트 구독routes/channels.php누가 특정 팀의 상태를 들을 수 있는가?
오래된 화면ListensToTeamChannel다른 탭의 팀 변경이 열린 화면에 어떤 영향을 주는가?
외부 자동화routes/api.php, routes/ai.php토큰 인증·팀 검사·능력 검사는 어느 진입점에 붙는가?

일반 API에는 api.ability:read·write·deploy 같은 능력 검사가 붙습니다. MCP 등록에는 활성화·IP 허용·Sanctum 인증·토큰 팀·팀별 활성화 미들웨어가 있습니다. 진입점별 검사가 같다고 추정하지 말고 각 라우트와 실제 처리 코드를 따라가야 합니다.

5디렉토리 구조 해부

폴더를 외우기보다 요청이 지나가는 길을 만들기
coolify/
├── app/
│   ├── Actions/          단일 목적의 도메인 작업
│   ├── Console/          명령과 스케줄 진입점
│   ├── Enums/            배포 상태·빌드 방식 등의 이름
│   ├── Events/           상태 변경 알림
│   ├── Http/             API 컨트롤러·미들웨어
│   ├── Jobs/             큐에서 실행할 작업
│   ├── Livewire/         PHP 기반 화면 컴포넌트
│   ├── Mcp/              관리 도구·리소스·프롬프트
│   ├── Models/           데이터와 관계
│   ├── Policies/         리소스별 접근 규칙
│   ├── Services/         여러 단계를 묶는 업무 로직
│   └── Traits/           원격 실행·팀 채널 등의 공통 기능
├── bootstrap/
│   ├── includeHelpers.php
│   └── helpers/          배포 접수·Compose 파싱·SSH·프록시
├── config/               큐·Horizon·인증·DB 설정
├── database/             마이그레이션·팩토리·초기 데이터
├── resources/
│   ├── views/            Blade 화면과 UI 조각
│   ├── css/              Tailwind 기반 스타일
│   └── js/               터미널·로그·Svelte 캔버스
├── routes/               웹·API·채널·MCP 등록
├── templates/compose/    서비스별 Compose 템플릿
├── docker/               운영·개발·보조 이미지·터미널
├── scripts/              설치·업그레이드·개발 보조 도구
├── tests/                단위·기능·브라우저 검증과 고정 입력
├── .github/workflows/    이미지 빌드·릴리스 등의 자동화
├── composer.json / composer.lock
├── package.json / package-lock.json
├── docker-compose.yml / docker-compose.prod.yml
└── versions.json         릴리스·보조 이미지 버전 정보

5-1. 주요 폴더를 읽는 순서

순서읽을 위치그다음 연결할 위치
1. 진입점routes/, app/Livewire/어떤 사용자 동작이 어떤 메서드를 호출하는지 찾습니다.
2. 접수와 데이터bootstrap/helpers/applications.php, app/Models/배포 기록에 저장하는 필드와 모델 관계를 확인합니다.
3. 실제 작업app/Jobs/, app/Traits/원격 명령과 실패·취소 처리까지 이어 봅니다.
4. 사용자 피드백app/Events/, resources/views/상태와 로그가 어떤 방식으로 보이는지 확인합니다.
5. 회귀 사례tests/Feature/예외 상황을 만든 뒤 무엇이 유지되어야 하는지 읽습니다.

bootstrap/includeHelpers.php는 Composer의 autoload.files에 등록되어 있습니다. 그래서 클래스에 선언되지 않은 queue_application_deployment() 같은 전역 함수를 호출할 수 있습니다. “정의가 클래스 파일에 안 보인다”면 helper 폴더를 검색해 보세요.

BaseModel::boot()는 새 모델을 만들 때 uuid가 없으면 new_public_id()로 채웁니다. 공개 식별자가 있어도 권한 검사는 여전히 필요합니다. 식별자를 추측하기 어렵게 만드는 것과 다른 팀의 리소스에 접근하지 못하게 하는 것은 서로 다른 문제입니다.

5-2. 도메인 관계를 단순화해서 그려 보기

Team
├── Project
│   └── Environment
│       ├── Application
│       │   ├── EnvironmentVariable
│       │   └── ApplicationDeploymentQueue
│       ├── Service
│       └── 독립 데이터베이스 리소스
└── Server
    └── Destination (예: StandaloneDocker)
             ^
             | 애플리케이션 등의 배포 대상 연결
             +--------------------------------------

이 그림은 모든 외래키를 표시한 ERD가 아닙니다. 프로젝트·환경은 논리적인 정리 기준이고, 서버·배포 대상은 실행 위치라는 차이를 보여 줍니다. 같은 폴더에 묶인다고 같은 서버에서 실행되는 것은 아닙니다.

5-3. 직접 센 코드 규모와 숫자의 함정

커밋 ade4bae의 Git 추적 파일 5,020개를 대상으로 확장자별 물리적 줄 수를 집계했습니다. 공백·주석을 포함하며, .blade.php는 일반 PHP와 분리했습니다. 생성물과 배포용 외부 코드를 일괄 제거한 순수 구현량이 아니라, 저장소에 들어 있는 텍스트의 규모입니다.

분류파일 수줄 수해석
PHP2,900476,888Blade 제외. 테스트 PHP 227,186줄 포함
Blade51049,267PHP와 HTML이 섞이는 화면 템플릿
JavaScript14063,314public/의 배포용 파일 54,139줄 포함
Svelte202,593주로 캔버스 화면 조각
CSS77,470스타일 파일
Shell245,555.sh 확장자만 집계
YAML44071,366Compose·CI·테스트 고정 입력 등을 포함

일반 PHP 중 테스트 밖은 249,702줄, 테스트 안은 227,186줄입니다. JavaScript는 public/ 밖 33개 파일·9,175줄이며, 큰 비중을 차지하는 외부 배포 파일에는 Monaco의 tsWorker.js가 있습니다. 숫자만 보고 직접 작성한 프론트엔드가 6만 줄이라고 해석하면 과장입니다.

반대로 Shell 수치에는 확장자가 없는 scripts/dev 같은 실행 파일이 빠집니다. 별도 .ts 파일이 집계되지 않았다고 TypeScript 관련 도구가 없다는 뜻도 아닙니다. 언어별 줄 수는 탐색 지도이며, 난도·성능·품질을 직접 증명하는 점수가 아닙니다.

# 저장소 루트에서 읽기만 하는 탐색 예시
git ls-files '*.php' '*.blade.php'
rg --files app/Jobs app/Traits tests/Feature
rg -n 'function deploy|queue_application_deployment' app bootstrap
# 실제 줄 수 집계는 추적 파일을 읽어 확장자별로 분리합니다.
# 디렉토리 전체를 단순 합산하면 외부 코드·테스트가 함께 잡힙니다.

6학습 포인트 (기술별)

기술 이름마다 하나의 질문과 작은 산출물 만들기

큰 저장소를 공부할 때는 기술을 모두 익히려 하기보다, 한 가지 동작을 여러 기술의 경계까지 추적하는 편이 효과적입니다. 각 기술에서 “무엇을 입력받고, 무엇을 바꾸며, 실패를 어디로 전달하는가”를 같은 형식으로 적어 보세요.

6-1. PHP·Laravel — 문법에서 업무 규칙으로

ApplicationPolicy::deploy()의 인자·반환형, ApplicationDeploymentStatus의 문자열 열거형, ApplicationDeploymentJob의 인터페이스와 trait을 읽습니다. 클래스 선언만 살펴봐도 “큐에 들어가는 작업”, “암호화할 작업”, “여러 기능을 조합한 작업자”라는 성격을 알 수 있습니다.

작은 실습: 작업자의 handle()에서 직접 호출하는 메서드 5개를 골라 입력·DB 변경·원격 명령·예외 여부를 표로 정리합니다. 반환값이 없는 메서드라도 상태를 바꾸거나 외부 명령을 수행할 수 있다는 점을 표시하세요.

6-2. Livewire·Blade — 화면의 버튼과 서버의 메서드 연결

Heading.php에서 deploy()·restart()·stop()을 비교합니다. 화면의 반응성은 JavaScript 상태만으로 만들어지지 않습니다. 서버 컴포넌트가 모델과 동작을 보유하고, 브라우저의 요청과 이벤트에 따라 다시 렌더링할 수 있습니다.

작은 실습: 관련 Blade 파일에서 wire:click이나 이벤트 사용 위치를 찾고, 클릭에서 권한 검사까지 화살표를 그립니다. 버튼 표시 조건과 서버에서 수행하는 권한 검사를 각각 다른 색으로 표시해 봅니다.

용어부터
서버 상태와 브라우저 상태
배포가 끝났다는 사실은 서버의 기록과 실제 컨테이너 상태에 있습니다. 브라우저는 그 사실을 표시하는 복사본을 가집니다. 이벤트 지연·탭 전환·네트워크 끊김이 생기면 복사본이 오래될 수 있으므로, 갱신 경로를 설계해야 합니다.

6-3. PostgreSQL·Eloquent — 관계와 동시성 함께 읽기

Application과 ApplicationDeploymentQueue의 관계를 먼저 찾고, 접수 함수의 조회 조건을 읽습니다. 같은 커밋이라도 PR 번호나 이미지 태그가 다르면 다른 배포로 취급될 수 있습니다. 업무상 “같은 요청”의 기준을 DB 조회 조건으로 표현하는 사례입니다.

작은 실습: 두 사용자가 같은 앱의 배포 버튼을 거의 동시에 누르는 상황을 시점 1~6으로 나누어 적습니다. 행 잠금이 있을 때와 없을 때, 두 요청이 기존 배포를 조회하는 순서가 어떻게 달라지는지 비교합니다.

6-4. Redis·Horizon — 대기표와 실행 인력 구별

config/queue.php, config/horizon.php, deployment_queue()를 연결해서 읽습니다. 셀프호스팅 Horizon의 기본 최소 프로세스는 1, 최대 프로세스는 4이며 환경변수로 바꿀 수 있습니다. 워커 설정의 기본 memory는 128MB입니다. 이 숫자는 호스트 전체 메모리 요구량이 아닙니다.

작은 실습: 큐 대기 한도, 서버의 동시 빌드 제한, 워커 수를 서로 다른 칸에 적습니다. 하나를 2배로 늘렸을 때 처리량이 반드시 2배가 되지 않는 이유를 빌드 CPU·디스크·SSH 작업 관점에서 설명합니다.

6-5. Docker·Compose — 설정 파일을 생성하는 제품

templates/compose/uptime-kuma.yaml을 읽고 이미지·환경변수·볼륨·헬스체크를 구분합니다. SERVICE_URL_UPTIMEKUMA_3001처럼 Coolify가 해석하는 규약도 있습니다. YAML을 문법적으로 읽는 단계와 플랫폼의 규칙을 적용하는 단계는 다릅니다.

# templates/compose/uptime-kuma.yaml — 설정 부분 발췌
services:
  uptime-kuma:
    image: louislam/uptime-kuma:2
    environment:
      - SERVICE_URL_UPTIMEKUMA_3001
    volumes:
      - uptime-kuma-data:/app/data
    healthcheck:
      test: ["CMD-SHELL", "extra/healthcheck"]
      interval: 5s
      timeout: 5s
      retries: 10

작은 실습: 템플릿을 실행하지 않고 “이미지를 바꾸면 사라질 데이터는 어디에 남겨야 하나?”를 설명합니다. 그다음 bootstrap/helpers/parsers.php의 serviceParser()가 services·volumes·networks를 나누는 부분을 찾습니다.

serviceParser()는 문자열 변환만 하는 순수 함수가 아닙니다. 리소스의 환경변수 관계를 조회·수정하는 코드도 있습니다. 함수를 실험할 때는 “파서니까 외부 상태를 안 바꾸겠지”라고 추정하지 않는 습관이 필요합니다.

6-6. Svelte·Vite — 필요한 영역에만 복잡한 화면 붙이기

resources/js/canvas/deploy/index.js와 DeployCanvas.svelte를 읽습니다. 그다음 resources/js/app.js의 동적 import, vite.config.js의 Svelte 플러그인으로 돌아옵니다. 컴포넌트 문법을 외우기 전에 로딩 시점과 마운트 책임을 파악하는 연습입니다.

작은 실습: 첫 페이지 진입과 Livewire 페이지 이동의 이벤트 순서를 두 열로 적습니다. 캔버스가 없는 페이지에서 모듈을 불러오지 않는 조건과, 이후 페이지 이동에서 캔버스를 찾는 조건을 표시합니다.

6-7. Pest — 실제 실패 사례를 학습 문제로 바꾸기

개발 의존성의 Pest는 v4.7.8로 잠겨 있습니다. ApplicationDeploymentStatusSideEffectsTest.php의 Process::fake()·Queue::fake()·Notification::fake()를 찾아보세요. 실제 SSH·작업 실행·알림 전송을 대신하여 검증 대상을 좁히는 장치입니다.

작은 실습: tests/Feature/ApiDeploymentPermissionsTest.php에서 쓰기 전용 토큰과 배포 권한이 있는 토큰의 기대 결과를 비교합니다. 같은 요청 본문이라도 토큰 능력에 따라 결과가 달라져야 한다는 규칙을 한국어 문장으로 바꿔 적습니다.

학습을 남기는 방법
함수 하나마다 네 칸을 채우기

입력, 바꾸는 상태, 실패 조건, 호출자를 기록하세요. 예를 들어 deploy()는 사용자 요청을 받아 권한을 확인하고 배포를 접수하며, execute_remote_command()는 서버·명령을 받아 원격 프로세스와 로그에 영향을 줍니다. 이름이 비슷해 보여도 책임은 다릅니다.

7하드웨어 / 시스템 요구사항

소스 읽기, 관리 서버, 앱 빌드의 자원을 나누어 계산하기

소스만 읽는 학습에는 PHP·Docker·DB 실행 환경이 필요하지 않습니다. 텍스트 편집기와 검색 도구면 이 글의 실습을 진행할 수 있습니다. 실제 설치를 준비할 때는 Coolify 관리 서버의 자원과 그 서버에서 수행할 빌드·앱·데이터베이스의 자원을 별도로 계산해야 합니다.

관리 화면을 띄우는 데 필요한 자원과 애플리케이션을 빌드하고 운영하는 데 필요한 자원은 서로 다릅니다. 작은 관리 서버가 가볍게 동작해도 같은 서버에서 큰 프론트엔드 빌드나 데이터베이스를 함께 돌리면 메모리·디스크가 부족할 수 있습니다.

7-1. 설치·빌드 파일에서 확인되는 조건

항목확인되는 값·조건출처와 해석
호스트 OSLinux 배포판별 설치 분기scripts/install.sh에 Ubuntu·Debian·Fedora·Alpine 등 분기. 목록만으로 모든 버전의 검증을 뜻하지는 않습니다.
설치 권한root 권한으로 실행(필요 시 sudo 사용)설치 스크립트 시작 부분의 EUID 검사
Docker서버 주 버전 24 이상MIN_DOCKER_VERSION=24. 더 낮으면 오류로 종료합니다.
디스크 총량30GB 기준REQUIRED_TOTAL_SPACE=30. 부족하면 경고합니다.
디스크 여유20GB 기준REQUIRED_AVAILABLE_SPACE=20. 총량과 별도로 검사합니다.
CPU 아키텍처AMD64·ARM64 계열 이미지 빌드메인 이미지 CI의 두 아키텍처 빌드·매니페스트 단계
원격 제어대상 호스트의 SSH·Docker 실행 가능 환경배포 작업자의 SSH 명령·Docker 명령 경로
관리 DB·큐PostgreSQL 15·Redis 7 이미지docker-compose.yml의 기본 구성

디스크 값은 설치 스크립트의 검사 기준입니다. 해당 분기는 경고를 출력하고 잠시 기다리므로, “그 미만에서는 무조건 설치 불가”라는 하드 제한으로 읽으면 안 됩니다. 반대로 경고를 통과했다고 앞으로 생길 이미지·로그·백업의 공간까지 충분한 것도 아닙니다.

7-2. CPU·메모리는 실습 목적에 맞춰 예산을 잡는다

이 글에서 제안하는 작은 격리 실습 환경의 출발 예산은 2 vCPU·4GB RAM·40GB 디스크입니다. 이는 공식 최소 사양이나 성능 보증이 아닌 학습용 제안입니다. 실제 앱의 언어·빌드 크기·동시 작업·DB 데이터량에 따라 더 큰 자원이 필요합니다.

학습·운영 형태필요 자원의 판단 기준
코드 읽기만저장소를 열 수 있는 일반 개발용 컴퓨터. 컨테이너 실행 불필요
작은 앱 한 개 실습관리 프로세스 외에 빌드 순간의 최대 메모리와 이미지 공간을 더해 계산
앱·DB 함께 운영DB 메모리·지속 데이터·백업·이전 이미지 보관량을 추가
여러 앱 동시 빌드동시 빌드 수에 따른 CPU·RAM·디스크 입출력 병목을 관찰
빌드 서버 분리selectBuildServer() 경로와 이미지 레지스트리 설정 조건을 확인

운영 Compose의 PHP_MEMORY_LIMIT 기본값 256M, Horizon 워커의 memory 128MB는 개별 프로세스 계층의 설정입니다. PHP-FPM·워커·Reverb·Node·DB·Redis가 합쳐지는 호스트의 총 메모리와 같지 않습니다.

7-3. 포트와 지속 데이터를 확인한다

역할설정·일반 경로확인 지점
관리 HTTP호스트 기본 8000 → 컨테이너 8080APP_PORT로 변경 가능
상태 WebSocket기본 6001Reverb, 기존 설정과의 호환 변수도 포함
터미널 WebSocket6002Node 터미널 서버
앱 접속·인증서일반적인 프록시 경로 80·443실제 공개 경로는 프록시·방화벽 구성에 따라 결정
SSH대상 서버에 설정한 포트기본 22라는 관례와 사용자 지정 설정을 구분
플랫폼 상태coolify-db·coolify-redis 볼륨플랫폼의 DB·큐 저장소와 앱 데이터 볼륨은 별개

docker-compose.prod.yml은 /data/coolify/ 아래의 SSH·애플리케이션·데이터베이스·서비스·백업·이미지 경로를 바인드 마운트합니다. source/.env도 읽기 전용으로 컨테이너에 연결합니다. 컨테이너 이미지를 다시 받는 것과 설정·키·DB·앱 데이터를 복구하는 것은 다른 작업입니다.

시스템 계획
플랫폼의 백업과 앱 데이터의 백업을 따로 생각하기

Coolify 설정을 복구해도 앱 DB의 데이터가 자동으로 되살아나는 것은 아닙니다. 관리 DB, 환경 설정과 필요한 키, 리소스별 지속 데이터, 백업 저장 위치를 나누어 목록화하고 복구 순서를 설명할 수 있어야 합니다.

8직접 해볼 수 있는 실습 과제

설치 없이도 가능한 다섯 가지 소스 분석 과제

아래 과제는 저장소 루트에서 파일을 읽고 메모·도표·의사코드를 만드는 방식입니다. 애플리케이션 실행이나 설치, 실제 서버 연결은 요구하지 않습니다. 각 과제는 이해했다는 느낌 대신 다른 사람이 검토할 수 있는 작은 결과물을 남기도록 설계했습니다.

과제 1 · 입문

배포 버튼에서 작업자까지 연결하기 권장 30~45분

목표: 화면 처리와 배포 실행이 어디서 나뉘는지 설명합니다.

  1. Heading::deploy()에서 권한 검사와 접수 함수 호출을 찾습니다.
  2. queue_application_deployment()에서 기록 생성과 dispatch()를 찾습니다.
  3. 작업자 생성자·handle()·decide_what_to_do()를 연결합니다.

제출물: 파일 경로·함수명이 붙은 6칸 흐름도와 실패 지점 3개.

완료 기준: “배포 버튼을 눌렀을 때 웹 요청 안에서 빌드가 끝나는가?”에 근거를 들어 답할 수 있습니다.

# 읽기 전용 탐색 명령
rg -n 'function deploy|authorize|queue_application_deployment' \
  app/Livewire/Project/Application/Heading.php
rg -n 'DB::transaction|lockForUpdate|dispatch' \
  bootstrap/helpers/applications.php
rg -n 'function __construct|function handle|decide_what_to_do' \
  app/Jobs/ApplicationDeploymentJob.php
과제 2 · 입문

서비스 템플릿 한 장 해설하기 권장 30분

목표: Compose의 이미지·환경변수·볼륨·헬스체크를 분리합니다.

  1. templates/compose/uptime-kuma.yaml에서 네 종류의 설정을 표시합니다.
  2. SERVICE_URL_UPTIMEKUMA_3001을 일반 문자열과 구별해야 하는 이유를 적습니다.
  3. 컨테이너를 교체했을 때 유지해야 할 경로와, 헬스체크 명령이 이미지에 요구하는 것을 설명합니다.

제출물: 설정 항목 4개를 설명하는 표와 “설정이 올바른 것”·“서비스가 정상인 것”의 차이 2문장.

완료 기준: 볼륨이 왜 필요한지, 헬스체크 간격 5초가 무엇을 뜻하는지 설명할 수 있습니다.

과제 3 · 중급

두 번 누른 배포 버튼의 결과 예측하기 권장 60분

목표: 중복 요청 판정과 큐 한도를 별개로 이해합니다.

  1. 접수 함수의 중복 조회 조건을 한 줄씩 한국어로 바꿉니다.
  2. 같은 커밋, 다른 PR 번호, 강제 재빌드, 꽉 찬 큐의 네 상황을 표에 적습니다.
  3. tests/Feature/QueueApplicationDeploymentCommitTest.php에서 대응하는 조건을 찾아 예측을 수정합니다.

제출물: 입력 조건·검사 순서·접수 결과·작업 전달 여부가 있는 결정표.

완료 기준: 큐가 꽉 찬 검사와 중복 검사 중 무엇이 먼저인지, 그 순서가 응답에 주는 영향을 설명할 수 있습니다.

과제 4 · 중급

다른 팀 관리자와 쓰기 전용 토큰 분석하기 권장 60~90분

목표: 사용자 역할·리소스 팀·API 토큰 능력을 혼동하지 않습니다.

  1. ApplicationPolicy::deploy()의 팀 판정 기준을 찾습니다.
  2. routes/api.php의 POST /api/v1/deploy 능력 검사를 찾습니다.
  3. tests/Feature/ApiDeploymentPermissionsTest.php에서 쓰기 권한만 가진 토큰의 거절 사례를 읽습니다.
  4. 다른 팀 관리자라는 조건만으로 대상 앱의 배포를 허용할 수 있는지 근거를 적습니다.

제출물: 팀 소속·역할·토큰 능력을 구분한 접근 판단표. 각 행에는 관련 검사 위치를 붙입니다.

완료 기준: 인증 성공과 배포 허용이 다른 판단이라는 것을 설명할 수 있습니다.

과제 5 · 고급

성공 뒤에 실패가 오는 배포 설계하기 권장 90분 이상

목표: 상태 저장·알림·다음 작업 시작의 실패를 따로 다룹니다.

  1. transitionToStatus()에서 각각의 예외 처리 범위를 표시합니다.
  2. 배포 성공 뒤 이벤트 실패, 다음 작업 전달 실패, 사용자의 취소를 시간 순서로 그립니다.
  3. ApplicationDeploymentStatusSideEffectsTest.php의 기대 결과와 비교합니다.
  4. 두 시스템 사이에서 상태와 큐 작업이 어긋났을 때 탐지·복구할 설계를 의사코드로 제안합니다.

제출물: 상태 전이표, 실패 시나리오 3개, 재시도할 때 중복 부작용을 줄일 방법 한 가지.

완료 기준: 현재 구현이 하는 일과 자신이 제안한 개선을 명확히 구별하며, 보장하지 않는 부분도 적습니다.

자기 점검
답에 함수 이름과 조건식이 들어 있는가?

“안전하게 처리한다” 대신 “대상 앱의 팀을 구한 뒤 그 팀의 관리자 여부를 확인한다”라고 써 보세요. 추상적인 평가를 실제 코드가 하는 행동으로 바꾸는 연습이 코드 리뷰와 설계 설명에도 도움이 됩니다.

9관련 기술 심화 학습 로드맵

한 주에 한 경계를 설명하는 6주 과정

PHP 기초 문법과 HTTP 요청·응답의 의미를 알고 있다는 전제의 로드맵입니다. 익숙하지 않다면 1주차 전에 클래스·배열·예외·JSON을 먼저 익혀도 좋습니다. 매주 새 기술을 설치하는 대신, 같은 배포 흐름을 더 정확한 그림으로 고쳐 나가는 것을 목표로 삼으세요.

주차핵심 주제읽을 코드주간 산출물
1주차PHP 객체·예외·열거형, HTTP와 인증ApplicationDeploymentStatus, ApplicationPolicy, routes/api.php입력·출력·실패 조건을 적은 함수 카드 5장
2주차Laravel 모델 관계·Livewire 화면 동작Application, Heading::deploy(), 관련 Blade배포 버튼에서 DB 기록까지 이어지는 흐름도
3주차트랜잭션·행 잠금·중복 요청·큐queue_application_deployment(), config/queue.php, deployment_queue()동시 요청 두 개의 시간표와 큐 접수 결정표
4주차Docker 이미지·볼륨·Compose·SSHrolling_update(), ExecuteRemoteCommand, 서비스 템플릿배포 방식 비교표와 데이터 지속성 지도
5주차상태 전이·이벤트·팀별 접근 제어transitionToStatus(), BroadcastsToTeam, routes/channels.php종료 상태를 보존해야 하는 실패 사례 3개
6주차빌드 재현성·회귀 테스트·배포 산출물운영 Dockerfile, 메인 빌드 CI, ServiceParserReferenceTest.php코드 변경 없이 작성하는 개선 제안서 한 장

9-1. 주차별 확인 질문

  1. 1주차: 사용자가 로그인했다는 사실과 대상 앱을 배포할 권한은 어떻게 다른가?
  2. 2주차: 화면 컴포넌트가 직접 수행하는 일과 작업자에게 넘기는 일은 무엇인가?
  3. 3주차: 대기 중인 배포가 한 개도 없을 때 무엇을 잠글 수 있는가?
  4. 4주차: 새 버전과 기존 버전을 동시에 실행할 수 없는 조건은 무엇인가?
  5. 5주차: 이벤트 전송 실패가 이미 성공한 배포의 상태를 바꾸면 어떤 오해가 생기는가?
  6. 6주차: 패키지 잠금 파일과 컨테이너 이미지 태그는 재현성을 어디까지 높여 주는가?

9-2. 관심에 따른 다음 갈림길

관심 분야이 저장소에서 더 읽을 곳다음에 익힐 개념
백엔드접수 함수·모델·정책·API 컨트롤러리소스 단위 권한, 중복 처리, DB와 큐 사이의 일관성
프론트엔드Livewire·Blade·Svelte 캔버스·터미널 UI화면 상태 소유권, 동적 로딩, 페이지 이동 후 초기화
인프라Dockerfile·Compose·프록시 생성·CI이미지 수명, 네트워크, 지속 데이터, 멀티아키텍처 배포
테스트·품질배포 실패 회귀 테스트·파서 기준 테스트외부 효과 대체, 고정 입력, 기대 결과의 변경 검토
관리 자동화routes/ai.php, app/Mcp/도구 권한, 팀 경계, 조회와 변경의 구별
공부 방법 비유

도시의 모든 건물 이름을 외우기보다 집에서 학교까지의 길을 먼저 익히는 방식입니다. 배포 버튼에서 컨테이너까지 가는 길을 한 번 알면, 새 파일을 발견했을 때 그 길의 어느 구간에 속하는지 판단하기가 쉬워집니다.

10핵심 키워드 사전

용어를 실제 동작과 연결해서 기억하기
용어쉬운 설명
PaaS빌드·실행·도메인·로그 같은 배포 작업을 묶어 제공하는 플랫폼입니다.
셀프호스팅내가 관리하는 서버에서 소프트웨어를 실행하는 방식입니다. 업데이트·백업의 책임도 함께 가집니다.
제어 영역무엇을 어느 서버에 배포할지 결정하고 지시하는 부분입니다. Coolify 관리 앱을 여기에 대응시킬 수 있습니다.
리소스관리 대상의 한 단위입니다. 앱·데이터베이스·서비스 등이 해당합니다.
환경프로젝트 안에서 리소스를 구분하는 논리적 묶음입니다. 예를 들어 개발·운영 환경을 나눌 수 있습니다.
배포 대상앱을 실제로 실행할 위치와 연결된 설정입니다. 논리적인 프로젝트 구분과는 역할이 다릅니다.
EloquentDB 행과 관계를 PHP 모델로 다루게 하는 Laravel의 ORM입니다.
LivewirePHP 컴포넌트의 상태·메서드를 브라우저의 동작과 연결하는 도구입니다.
BladePHP 데이터와 화면 마크업을 조합해 HTML을 만드는 Laravel 템플릿입니다.
작업 큐시간이 걸리는 일을 대기시켰다가 워커가 꺼내 처리하는 구조입니다.
워커큐의 작업을 실제로 수행하는 프로세스입니다. 대기표 자체와 일하는 프로세스는 다릅니다.
HorizonLaravel의 Redis 큐 워커를 구성하고 상태를 관찰하는 도구입니다.
트랜잭션여러 DB 변경을 하나의 확정·취소 단위로 묶습니다. 원격 서버의 명령까지 자동으로 되돌리지는 않습니다.
행 잠금동일한 DB 행을 바꾸는 다른 트랜잭션을 기다리게 하는 동시성 제어입니다.
멱등성같은 작업을 반복해도 의도한 결과가 중복해서 늘어나지 않는 성질입니다. 중복 검사 하나로 시스템 전체의 멱등성이 완성되지는 않습니다.
상태 전이대기·실행·완료 같은 단계 사이를 이동하는 규칙입니다. 종료 상태를 보호하는 것도 규칙의 일부입니다.
부수 효과주된 결과와 함께 발생하는 외부 변화입니다. 여기서는 알림·이벤트 전송 등이 대표적입니다.
SSH원격 서버에 암호화된 연결을 만들고 명령을 실행하는 통로입니다.
traitPHP 클래스에서 여러 메서드를 묶어 재사용하는 장치입니다. 사용하는 클래스의 속성에 의존할 수 있습니다.
빌드팩프로젝트에서 실행 이미지를 만드는 방식을 제공하는 도구·규칙입니다. Coolify에는 Nixpacks·Railpack 경로가 있습니다.
이미지 / 컨테이너이미지는 실행 재료의 묶음, 컨테이너는 그 이미지를 실제로 실행한 인스턴스입니다.
Docker Compose여러 서비스의 이미지·환경변수·볼륨·네트워크를 YAML로 기술하는 도구입니다.
볼륨컨테이너 교체와 별개로 데이터를 남기기 위한 저장 공간입니다.
리버스 프록시방문자의 요청을 받아 도메인·경로 등의 규칙에 맞는 내부 서비스로 전달하는 앞문입니다.
헬스체크정해진 명령·요청으로 상태를 판단하는 검사입니다. 검사하지 않은 업무 기능의 정상 동작까지 증명하지 않습니다.
롤링 업데이트새 버전을 준비한 뒤 기존 버전을 정리하는 교체 방식입니다. 이 저장소에서는 설정에 따라 먼저 중지하는 경로도 있습니다.
WebSocket / Reverb연결을 유지하며 이벤트를 주고받는 통신 방식과, 그 방식을 지원하는 Laravel 서버입니다.
정책 / 능력정책은 사용자가 대상 리소스에 할 수 있는 일을, 토큰 능력은 그 토큰에 허용한 작업 범위를 표현합니다.
멀티테넌시여러 팀이 같은 플랫폼을 사용하면서도 서로의 리소스·정보에 접근하지 않도록 경계를 두는 구조입니다.
MCP클라이언트가 서버의 도구·리소스 등을 이용하도록 연결하는 규약입니다. 관리 기능을 노출할 때도 인증과 권한이 필요합니다.
골든 테스트정해 둔 기준 출력과 현재 출력을 비교하는 검증입니다. 출력 변경이 의도된 것인지 검토하는 과정이 중요합니다.
멀티스테이지 빌드준비·컴파일 단계를 나누고 필요한 결과만 최종 이미지에 옮기는 Docker 빌드 방식입니다.
매니페스트아키텍처별 이미지를 하나의 태그로 선택할 수 있게 연결하는 이미지 목록 정보입니다.

용어를 외운 뒤에는 다시 소스의 행동으로 바꾸어 말해 보세요. “비동기 배포”라면 누가 요청을 기록하고 누가 나중에 실행하는지, “팀 격리”라면 어느 조회·정책·채널 검사에서 경계를 확인하는지까지 설명할 수 있어야 합니다.

11참고 링크

공식 자료와 커밋에 고정된 읽기 출발점

11-1. 프로젝트 공식 자료

11-2. 소스 직접 읽기 — 배포 요청의 여정

11-3. 실행 환경과 관련 기술의 공식 문서

소스 링크는 커밋에 고정되어 있고, 일반 공식 문서는 시간이 지나며 바뀔 수 있습니다. 문서 예제를 적용할 때는 위 기술 스택 표의 버전과 해당 문서의 대상 버전을 먼저 맞추세요.

11-4. 현재 상태 (2026-10-23 확인)

항목값기준
저장소coollabsio/coolify공식 GitHub
선정 순위GitHub Trending PHP Daily #52026-10-23 순위 기록
스타 / 포크63,525 / 5,7142026-10-23 GitHub 기준
주 언어PHPGitHub 언어 메타데이터
생성일2021-01-25GitHub 기준
최신 릴리스v4.5.0 · 2026-10-23GitHub 기준. versions.json의 coolify.v4.version 값도 4.5.0
최근 push2026-10-23GitHub 기준
최근 커밋ade4bae · 2026-10-23
ci: remove temporary release image rebuild workflow
분석 기준 HEAD의 실제 커밋 기록
코드 규모추적 파일 5,020개
PHP 476,888줄 + Blade 49,267줄
JavaScript 63,314줄 + Svelte 2,593줄
공백·주석 포함 직접 집계. 테스트·외부 배포 코드 포함, 상세는 5절
서비스 템플릿templates/compose/*.yaml 382개원클릭 서비스의 실제 표시 개수와는 구분
라이선스Apache-2.0LICENSE와 composer.json
비교할 버전Laravel 12.69.3 · Livewire 3.8.10
운영 PHP 이미지 8.4 계열
의존성 잠금·Dockerfile 기준
활용 전에 알아둘 한계
이 글의 범위와 실제 운영에서 추가로 확인할 것

설명은 커밋 ade4bae의 코드 기준입니다. 실제 설치·빌드·배포·복구의 성공 여부와 성능은 실행 환경에 따라 달라집니다. 테스트 파일의 기대 결과는 설계 의도를 읽는 근거이며, 실제 실행 결과와는 구별해야 합니다.

단일 커밋의 코드 규모와 당일 순위만으로 장기간의 개발 속도나 트렌딩에 오른 원인을 판단할 수는 없습니다. 스타·포크·릴리스는 2026-10-23 GitHub 기준이며 이후 달라질 수 있습니다.

README의 “모든 앱”, “360개 이상 서비스”, “Coolify 없이도 리소스 유지” 같은 설명은 적용 조건과 구별해서 읽어야 합니다. 빌드 방식·프록시·포트·헬스체크·데이터 지속성에 따라 실제 동작이 달라집니다.

이 글은 보안 감사를 대신하지 않습니다. 실제 운영에서는 관리 권한과 SSH 키의 범위, 토큰 능력, 네트워크 경로, 데이터 백업·복구 절차를 자신의 구성에 맞춰 확인해야 합니다. 소프트웨어의 라이선스와 서버 운영 비용도 별개의 항목입니다.