# ChatGPT Web + GitHub + Self-hosted Runner: 어떤 기기에서든 개발하고 GitHub로 통합 배포하기

> Canonical: https://vxi.me/ko/posts/chatgpt-web-github-self-hosted-runner/
> Locale: ko-kr
> Published: 2026-10-07
> Updated: 2026-10-07
> Markdown: https://vxi.me/ko/posts/chatgpt-web-github-self-hosted-runner.md

예전에는 server project를 개발하고 배포할 때 거의 항상 같은 PC가 필요했습니다. local에서 repository를 pull하고 Codex나 IDE로 수정한 뒤 `git push`, terminal에서 SSH로 server에 접속해 `git pull`, build, restart, log 확인까지 이어졌습니다. 이 방식 자체가 나쁜 것은 아닙니다. 오히려 단순하고 이해하기 쉽습니다.

문제는 project를 오래 운영하기 시작할 때 생깁니다. 개발 환경, local checkout, SSH session, production release 경로가 전부 한 대의 PC에 묶이기 쉽습니다.

최근 제 blog deployment를 다시 정리하면서 production 경로를 아래처럼 줄였습니다.

```text
browser / local development / cloud coding agent
                        ↓
                      GitHub
                        ↓
                 GitHub Actions
                        ↓
               Self-hosted Runner
                        ↓
                build / validate
                        ↓
                 release directory
                        ↓
                  current symlink
                        ↓
                    production
```

제가 이 구조에서 중요하게 보는 것은 “이제 local PC가 필요 없다”가 아닙니다. **개발 진입점은 바뀌어도 production으로 들어가는 출구는 가능한 한 하나로 유지하는 것**입니다. Mac에서 작업해도 되고, phone으로 task를 확인해도 되고, browser에서 cloud coding task를 이어가도 됩니다. 오래 남아야 하는 것은 GitHub의 code state, review history, 그리고 하나의 deterministic production workflow입니다.

이번에 메모를 글로 옮기면서 GitHub와 OpenAI의 현재 documentation, 그리고 실제로 실행 중인 제 workflow를 다시 확인했습니다. 그 과정에서 두 가지 표현을 줄여야 했습니다. 일반 ChatGPT GitHub connection이 곧 repository write 권한을 뜻하지는 않습니다. 그리고 지금 deployment도 “어떤 failure든 자동 rollback”하는 구조는 아닙니다.

GitHub의 현재 Self-hosted Runner reference는 여기에서 확인할 수 있습니다: https://docs.github.com/en/actions/reference/runners/self-hosted-runners

## Self-hosted Runner는 GitHub가 server에 SSH로 들어가는 구조가 아니다

처음에는 “push가 GitHub에 도착하면 GitHub가 VPS에 remote login해서 script를 실행한다”고 생각하기 쉽습니다. 실제 model은 다릅니다.

GitHub 공식 documentation에 따르면 Self-hosted Runner application은 사용자가 관리하는 machine에서 실행되고, Runner 쪽에서 **GitHub로 outbound connection을 만들고** job assignment를 받습니다. host는 outbound HTTPS에 접근할 수 있어야 하고, 핵심 통신은 port 443을 사용합니다.

VPS를 Runner로 등록하면 개념적으로는 장시간 online 상태의 worker에 가깝습니다.

```text
GitHub
  ↑  Runner가 connection을 생성
  │
Self-hosted Runner on VPS
  │
  ├─ checkout
  ├─ npm ci
  ├─ test / build
  └─ local deploy
```

workflow에서 어떤 worker가 job을 받을지 결정하는 것은 runner label입니다. 공개 예시에서는 제 구성을 이렇게 단순화할 수 있습니다.

```yaml
jobs:
  deploy:
    runs-on: [self-hosted, linux, prod]
```

이 방식은 예전 SSH/SCP deployment와 꽤 다릅니다. external CI에서 build하면 결과물을 다시 VPS로 보내는 계층이 필요합니다. 반면 Runner가 VPS 위에 있으면 job 자체가 그 machine에서 실행되고, build 뒤에 local deploy script를 바로 호출할 수 있습니다. CI에서 server로 들어가기 위한 SSH key, SCP path, remote shell command를 별도로 유지할 필요가 줄어듭니다.

저에게 GitHub는 control plane이자 durable state이고, VPS는 execution plane입니다. 신뢰할 수 있는 Runner가 online이라면 어느 device에서 code change를 시작했는지보다, production이 항상 같은 workflow를 거친다는 점이 더 중요합니다.

이 구조에서는 repository를 진짜 source of truth로 보기 쉬워집니다. Runner workspace는 “server의 main code”가 아니라 한 번의 job을 위한 임시 execution workspace입니다. 지속적으로 추적할 것은 commit, PR, workflow run, release입니다.

## Browser를 개발 진입점으로 쓸 수 있지만, “GitHub 연결”과 “GitHub에 쓰기”는 다르다

이 부분은 제품 설명만 보면 쉽게 섞입니다.

OpenAI의 현재 GitHub connection documentation은 일반 ChatGPT GitHub app과 write-capable coding workflow를 구분합니다. 기본 connection은 사용자가 허용한 repository를 읽고, 검색하고, 분석하는 용도입니다. Help Center도 GitHub에 code를 직접 생성·편집·push하려면 Codex 같은 write-capable workflow를 사용해야 한다고 설명합니다.

공식 설명：https://help.openai.com/en/articles/11145903-connecting-github-to-chatgpt

그래서 저는 architecture를 아래처럼 그리지 않습니다.

```text
ChatGPT Web
   ↓
GitHub 직접 수정
```

더 정확한 흐름은 이쪽입니다.

```text
browser / phone / desktop
           ↓
write-capable coding workflow
Codex Cloud / controlled agent
           ↓
branch / commit / PR
           ↓
GitHub
```

Codex Cloud가 있어야 “내 PC가 없어도 개발을 이어간다”는 말이 실용적으로 됩니다. OpenAI의 현재 documentation에 따르면 cloud task는 OpenAI가 관리하는 computer에서 실행되고, 지원되는 desktop, web, mobile surface에서 시작하거나 이어갈 수 있습니다. local PC가 sleep 상태여도 cloud task는 계속 진행될 수 있습니다.

참고：https://help.openai.com/en/articles/11369540-using-codex-with-your-chatgpt-plan

다만 이것을 “web에는 token limit이 없다”거나 “usage limit이 없다”고 쓰지는 않습니다. plan, workspace policy, product limit은 바뀔 수 있습니다. 오래 남는 장점은 더 단순합니다. **code state가 한 대의 Mac terminal session 안에만 있지 않아도 됩니다.**

그래서 지금은 Codex를 하나의 model window라기보다 개발 진입점 중 하나로 봅니다. device를 바꿔도 작업을 이어갈 수 있는 이유는 repository, environment, task state, verification chain이 남기 때문입니다.

이 변화가 궁금하다면 이전에 쓴 [새 Codex가 왜 개발 환경처럼 느껴지는지](/posts/what-changed-in-the-new-codex/)도 이어서 볼 수 있습니다.

## Production 출구를 GitHub 하나로 만든 뒤에야 “어떤 기기에서든 개발”을 신뢰하게 됐다

browser에서 code를 수정할 수 있는 것만으로는 충분하지 않았습니다. 마지막 release 단계가 여전히 “Mac으로 돌아가 SSH로 server에 들어가 몇 줄 실행”이라면 production dependency는 local PC에 남아 있습니다.

현재 제 blog setup에서 더 중요한 부분은 production이 신뢰된 `main` update만 하나의 workflow로 받는다는 점입니다.

지금 실제로 실행 중인 workflow를 다시 확인하면 대략 다음 순서입니다.

```text
main push
  ↓
checkout
  ↓
runtime / directory checks
  ↓
npm ci
  ↓
syntax checks
  ↓
build
  ↓
capture previous release
  ↓
local atomic deploy
  ↓
post-deploy SEO verification
  ↓
submit sitemap
```

이건 diagram만 있는 구조가 아닙니다. 2026년 10월 7일 완료된 production Actions run을 확인했고, `Build`, `Atomic Deploy`, `Post-Deploy SEO Verification`, sitemap submission이 모두 성공했습니다. 저에게 중요한 것은 production release가 실제로 “어떤 local PC의 SSH 작업”에서 repository workflow로 옮겨갔다는 점입니다.

deploy script의 layout도 단순합니다.

```text
/srv/app/
├─ releases/
│  ├─ 20261007-abc1234/
│  └─ 20261007-def5678/
└─ current -> releases/20261007-def5678/
```

새 build는 먼저 독립된 release directory에 들어갑니다. 그다음 temporary symlink를 `current`로 atomic하게 교체합니다. Nginx 같은 Web Server는 transient build workspace가 아니라 `current`만 계속 바라보면 됩니다.

원래 메모에는 이 구조를 “실패하면 자동 rollback”이라고 쓰기 쉬웠습니다. 하지만 실제 workflow의 boundary는 더 구체적입니다.

- build 또는 pre-deploy validation이 실패하면 production switch 자체가 일어나지 않습니다.
- deploy 뒤 SEO verification이 실패하면 workflow에 `current`를 previous release로 되돌리는 명시적 로직이 있습니다.
- 그렇다고 symlink 전환 뒤 발생할 수 있는 모든 failure에 automatic rollback이 존재한다고 말할 수는 없습니다.

운영에서는 이 차이가 중요합니다. architecture diagram이 코드보다 강해 보이면 안 됩니다. 앞으로 recovery를 더 강화하려면 failure class별로 경로를 직접 구현해야지, “atomic deploy”라는 말만으로 안전을 추정하면 안 됩니다.

예전에 쓴 [Markdown + GitHub Actions 자동 publishing workflow](/posts/markdown-blog-publish-workflow/)는 “content를 어떻게 자동으로 올릴까”라는 작은 문제를 다뤘습니다. 이번 구조는 더 넓습니다. 개발을 어디서 시작하든 production에 들어갈 수 있는 경로는 무엇인가를 정하는 문제입니다.

## Self-hosted Runner의 가장 큰 함정은 실제로 내 machine에서 code를 실행한다는 것

Runner를 VPS에 두면 편한 이유는 job이 local filesystem, network, deploy path에 직접 접근할 수 있기 때문입니다. 위험도 같은 이유에서 생깁니다.

GitHub security documentation은 이 부분을 분명하게 설명합니다. Self-hosted Runner는 GitHub-hosted Runner처럼 job마다 새 disposable VM이 보장되지 않습니다. 신뢰할 수 없는 workflow code가 host를 persistent하게 compromise할 수 있습니다. 그래서 GitHub는 public repository에서 Self-hosted Runner 사용을 강하게 권장하지 않고, private/internal repository에서도 fork와 PR 경로를 주의하라고 설명합니다.

공식 security guidance：https://docs.github.com/en/actions/reference/security/secure-use

이 내용을 보고 제 workflow도 다시 확인했습니다. production deployment는 신뢰된 `main` push로 시작되므로 이 boundary는 유지하고 싶습니다. 하지만 현재 content PR validation도 같은 Self-hosted Runner에서 실행됩니다.

지금처럼 private repository이고 작성자가 신뢰된 범위라면 risk를 관리할 수 있습니다. 반대로 앞으로 repository를 public으로 바꾸거나 더 많은 외부 contributor의 PR을 받게 된다면, PR code를 production runner에서 실행하지 않을 겁니다.

그때는 아래처럼 분리하는 편이 낫습니다.

```text
untrusted / public PR
        ↓
GitHub-hosted runner
또는 isolated disposable validation runner

protected main
        ↓
production Self-hosted Runner
        ↓
deploy
```

team이 커진다면 production Runner group도 실제로 필요한 repository와 workflow만 사용하도록 제한하는 편이 좋습니다.

“AI에 VPS SSH key를 주지 않아도 된다”는 분명한 개선이지만, 그것만으로 security boundary가 끝나지는 않습니다. Agent가 workflow를 수정할 수 있고, 그 workflow가 production host에서 임의의 shell을 실행할 수 있다면 privilege는 사라진 것이 아니라 입구만 바뀐 것입니다.

진짜로 지켜야 할 것은 누가 workflow를 수정할 수 있는지, 누가 `main`에 merge할 수 있는지, 어떤 event가 production runner에 도달하는지, runner user가 어떤 directory에 write할 수 있는지, Secrets가 어느 정도의 권한을 갖는지입니다.

## Local Codex와 SSH는 사라진 것이 아니라 더 맞는 역할로 이동했다

이 구조를 쓴다고 local Codex, local checkout, SSH가 낡은 도구가 되는 것은 아닙니다.

이 도구들은 browser control plane이 약한 영역, 즉 지금 이 순간의 machine state를 다룹니다.

현재 저는 역할을 이렇게 나눕니다.

| 상황 | Cloud / GitHub workflow | Local Codex + SSH |
| --- | --- | --- |
| 여러 device에서 coding task 이어가기 | 잘 맞음 | 현재 machine에 의존 |
| code history / PR review | 잘 맞음 | GitHub가 canonical |
| 표준 build / test / deploy | 잘 맞음 | 매번 manual로 하지 않음 |
| `docker ps` / `journalctl` 확인 | 간접적 | 잘 맞음 |
| database / disk / network 현장 진단 | 간접적 | 잘 맞음 |
| production 긴급 대응 | 보조 가능 | SSH가 직접적 |
| 감사 가능한 release | run과 commit이 남음 | manual SSH는 약함 |

browser나 phone은 control plane으로 잘 맞습니다. 무엇을 바꿀지 결정하고, diff를 보고, CI를 확인하고, 이미 정의된 workflow를 trigger하는 곳입니다. 반면 local terminal은 observability/debug plane에 가깝습니다. 지금 어떤 process가 멈췄는지, 어떤 container가 계속 restart하는지, disk가 왜 가득 찼는지, Nginx가 실제로 어떤 config를 읽는지 확인하는 곳입니다.

SSH도 없앨 필요는 없습니다. 저는 break-glass path로 낮추는 것이 더 자연스럽다고 봅니다. 일상 release는 SSH에 의존하지 않되, production이 실제로 고장났을 때는 직접 들어갈 수 있어야 합니다. 긴급 대응으로 server에서 한 수정도 결국 GitHub에 code나 workflow state로 되돌려서 server에만 존재하는 변경이 남지 않게 해야 합니다.

이미 local Markdown + GitHub Actions를 쓰고 있다면 [이전 publishing workflow](/posts/markdown-blog-publish-workflow/)가 더 단순한 시작점입니다. static site와 traditional CMS를 비교하는 단계라면 [Astro vs WordPress](/posts/astro-vs-wordpress-markdown-github-actions-blog/)가 더 가까운 이야기입니다.

지금 제가 중요하게 보는 것은 이 네 역할을 분리하는 것입니다.

```text
browser / Codex / local IDE   → development entry
GitHub                        → durable code + review state
Self-hosted Runner            → trusted automated execution
SSH                           → deep diagnosis + emergency access
```

device는 바뀔 수 있고 Agent도 바뀔 수 있습니다. production server도 나중에는 교체될 수 있습니다. code state, deployment rule, verification logic이 GitHub에 남아 있다면 전체 workflow가 한 대의 PC와 함께 사라지지는 않습니다.

제가 ChatGPT/Codex + GitHub + Self-hosted Runner 조합에서 가장 크게 느끼는 가치는 여기입니다. terminal을 숨기는 것이 아니라, “이 작업은 반드시 이 PC에서 직접 해야 한다”는 단계를 일상 운영에서 최대한 줄이는 것입니다.
