# 배포

저장소는 콘솔의 배포 화면에 있는 웹훅 주소를 GitHub에 등록하고 **첫 push를 하면** 연결돼요.
그 뒤로는 push마다 빌드하고 배포해요.

```
list_apps    연결된 저장소, 프로덕션 브랜치, 빌드할 폴더
update_app   프로덕션 브랜치, 빌드할 폴더, 프레임워크 프리셋, 빌드·출력·설치 명령, 실행 사용자(run_as) 바꾸기
delete_app   앱 하나 지우기: 주소를 내리고 배포를 지워요 (confirm에 앱 이름)
```

- **프로덕션 브랜치**에 push하면 프로젝트의 기본 주소가 바뀌어요. 다른 브랜치는 브랜치 이름이 붙은 프리뷰 주소로 가요.
  새 저장소는 GitHub 저장소의 기본 브랜치로 시작해요.
- **모노레포**라면 `root_directory`에 앱 폴더(예: `apps/web`)를 넣으세요. 저장소 전체는 `.`이에요.
- 프레임워크는 빌드할 때 자동으로 감지해요. 감지가 틀리면 `framework`로 프리셋을 고르고, 필요하면 `build_command` · `output_directory` · `install_command`로 덮어쓰세요(콘솔의 빌드 설정과 같은 값). null은 프리셋 기본값, 빈 명령은 그 단계를 건너뛰어요.
  저장소의 `blackwall.json`에 같은 항목이 있으면 그 값이 먼저이고, 빌드 로그에 값마다 어디서 왔는지 적혀요.
- **백그라운드 작업자**(대기열 작업, 챗봇, 거래소 연결 유지처럼 웹 요청 없이 도는 서버)는 `blackwall.json`에 `"worker": true`를 넣고 package.json의 `start`로 실행할 명령을 정하세요. 포트를 열지 않아도 되고, 계속 켜 두며 멈추면 다시 시작해요. 웹 요청에는 404로 답해요.
- **GPU 작업과 GPU 서버**: Pro 이상에서 GPU를 초 단위로 빌려요. `create_gpu_job`은 공개 이미지에서 명령을 끝까지 돌리고(학습, 일괄 작업) 끝나면 GPU를 돌려줘요. 결과는 프로젝트 저장소 같은 바깥에 직접 올려야 남아요. `get_gpu_job`으로 상태와 출력 끝부분을, `cancel_gpu_job`으로 멈춰요. `create_gpu_server`는 모델을 띄워 두고 `https://<ref>.<zone>/gpu/v1/servers/<이름>/`으로 secret 키를 붙여 불러요. 꺼져 있으면 첫 요청에 켜지고(켜지는 동안 503), 정한 시간 동안 요청이 없으면 꺼져요. `stop_gpu_server`·`delete_gpu_server`로 멈추거나 지워요. `create_gpu_volume`으로 한 데이터센터에 GPU 저장소를 만들어 작업·서버에 `volume`으로 주면 `/workspace`에 붙어서, 모델을 한 번만 받아 두면 돼요(있는 동안 1GB·월 요금, `delete_gpu_volume`으로 지워요). `region`(auto·asia·us·eu)으로 빌릴 지역을 고르고, 저장소를 주면 그 데이터센터에서 빌려요. 한국에서 가까운 곳은 AP-JP-1(일본)이에요. 입력한 이미지·명령·환경 변수와 저장소 파일은 RunPod의 데이터센터에서 처리돼요. 요금과 쓰는 수는 `list_gpu`에 나와요. RTX 4090 1시간 4,500원부터, GPU를 빌린 때부터 돌려줄 때까지(이미지 받는 시간 포함) 초 단위로 청구해요.
- **MySQL 데이터베이스 (WordPress)**: `create_mysql_database`로 MariaDB 11.4 데이터베이스와 전용 로그인을 만들어요(보통 1분). `app`을 주면 그 앱만, 없으면 프로젝트 전체용이에요(전체용이 하나뿐이면 앱 모두가 받아요). `bind_mysql_database`로 받을 앱을 바꿔요. 받는 앱의 서버는 실행할 때 `MYSQL_URL`·`MYSQL_HOST`·`MYSQL_PORT`·`MYSQL_USER`·`MYSQL_PASSWORD`·`MYSQL_DATABASE`와 공식 WordPress 이미지가 읽는 `WORDPRESS_DB_HOST`(호스트:포트)·`WORDPRESS_DB_USER`·`WORDPRESS_DB_PASSWORD`·`WORDPRESS_DB_NAME`을 받아요. 빌드에는 들어가지 않고, 같은 이름으로 저장한 변수가 이겨요. 프리뷰도 같은 데이터베이스를 받아요. 주소는 이 서버 안의 앱에서만 닿아요. `get_mysql_connection`으로 연결 정보를, `list_mysql_databases`로 목록과 요금제 한도(무료 1개, Pro 10개, Enterprise 50개)를 봐요. 매일 밤 자동으로 백업되고, `create_mysql_backup`·`list_mysql_backups`·`restore_mysql_backup`으로 지금 백업하고 되돌려요(복원 직전 상태를 먼저 백업해요). `delete_mysql_database`는 데이터베이스와 로그인, 백업을 모두 지워요. 지우기와 복원은 `confirm`에 데이터베이스 이름을 그대로 넣어야 해요. REST: `/mysql`, `/mysql/<이름>`, `/mysql/<이름>/connection`, `/mysql/<이름>/backups`, `/mysql/<이름>/restore`.
- **전화와 websocket**: 서버 배포는 websocket을 받아요. Twilio 번호의 음성 웹훅을 `https://<주소>/voice`로 두면 통화 음성이 실시간으로 들어와요(시작 예제: 저장소의 `examples/phone-voice`). 연결이 열려 있는 동안 서버는 멈추거나 밀려나지 않아서 긴 통화도 끊기지 않아요. 대표 번호는 프로덕션 배포로 두세요. 멈춰 있던 서버를 깨우면 첫 전화가 몇 초 늦어요.
- **서버 메모리**는 기본 512MB(CPU 1개)이고, `set_server_memory`(콘솔의 배포 → 빌드 설정, 지금 값과 한 달 금액은 `get_server_memory`)로 1024·2048·4096MB 중에서 고를 수 있어요. 1024MB보다 크면 CPU가 2개예요. 쓴 메모리는 시간 단위로 `runtime_gb_hour`로 계량돼요. 요금제마다 고를 수 있는 최대 크기가 정해져 있어요. 프로덕션과 브랜치 주소의 서버는 요청이 없어도 늘 켜져 있어요(최소 인스턴스 1개, 따로 켤 설정은 없어요). 그래서 `get_server_memory`의 한 달 금액이 서버 하나의 한 달 요금이에요.
- **동시 빌드**: 이 프로젝트의 빌드가 한꺼번에 몇 개까지 돌지 `set_concurrent_builds`로 1~20을 정하거나 null로 한도를 없애요(콘솔의 배포 → 빌드 설정, 서버 메모리 바로 아래. 지금 값은 `get_concurrent_builds`). 넘는 빌드는 대기열에서 기다리고, 기다리는 동안에는 빌드 시간이 청구되지 않아요.
- **Dockerfile**: 프로젝트 폴더에 Dockerfile만 있고 package.json이 없으면(Python, Go 등) 그 Dockerfile로 이미지를 빌드해 실행해요. package.json도 있는 저장소는 프리셋을 `docker`로 고르거나 blackwall.json에 `"framework": "docker"`를 넣으면 이미지로 빌드해요. 이미지의 CMD로 시작하고, 이미지가 EXPOSE한 포트(가장 낮은 TCP 포트, 없으면 3000)에서 들어요. 다른 포트면 `update_app`의 `port`로 정해요. 보안을 위해 이미지의 USER와 상관없이 권한 없는 사용자(uid 10001)로, 읽기 전용 파일 시스템으로 실행해요. 쓸 수 있는 곳은 `/tmp`(64MB), `/run`·`/var/run/apache2`·`/var/lock/apache2`(각 16MB, 다시 시작하면 비어요), blackwall.json의 `"writablePaths"`에 적은 경로(최대 8개, 각 16MB), 그리고 볼륨이에요. 남겨야 하는 파일은 볼륨에 두세요(아래). WordPress 공식 이미지처럼 root로 시작해야 하는 이미지는 빌드 설정의 "이미지의 사용자로 실행"(update_app의 `run_as: "image"`)을 켜세요. 그러면 gVisor 격리 환경 안에서 이미지의 USER(root 포함)로 실행돼서 root여도 다른 프로젝트에는 닿지 못해요. 이 서버에 gVisor가 없으면 켤 수 없다고 알려 줘요. 포트 80처럼 1024 아래 포트에서도 들을 수 있어요. 빌드할 때 환경 변수는 `--build-arg`로 전달되고 이미지 기록에 남으니 비밀값은 실행 변수로 두세요. 빌드는 단계마다 메모리 2GB·CPU 2개, 최대 45분이고 이미지는 4GB까지예요.
- **`/data`**: Dockerfile이 아닌 서버(Node 등)는 `/data`에 바로 쓸 수 있어요. Railway·Fly용으로 만든 SDK가 캐시와 토큰을 거기에 두기 때문이에요. 서버 메모리의 1/8(16MB~256MB)까지이고 다시 시작하면 비어요. 남겨야 하면 `/data`에 볼륨을 붙이세요(아래). 볼륨이 그 자리를 대신해요.
- **볼륨**: 서버를 다시 시작하거나 새로 배포해도 남는 디스크예요. `create_volume`으로 앱의 프로덕션 서버에 붙이고(예: WordPress는 `/var/www/html`), `list_volumes`로 보고, `resize_volume`으로 늘리고(줄일 수는 없어요), `delete_volume`으로 지워요(파일과 백업이 모두 사라지고 되돌릴 수 없어서 confirm에 경로를 넣어야 해요). `create_volume_backup`은 파일을 묶어 30일 보관하고, `restore_volume_backup`은 그 백업으로 되돌려요(되돌리기 전 파일은 7일 동안 백업해 둬요). 크기는 1~500GB이고 상한이라 넘으면 쓰기가 실패해요. 프로젝트의 볼륨 합계는 요금제가 정해요(Free 1GB, Pro 50GB). 프리뷰와 지금 프로덕션이 아닌 예전 배포에는 붙지 않아요. 붙이거나 지우면 1분 안에 서버가 다시 시작해요. 콘솔의 배포 → 빌드 설정에도 있어요.
- 바꾼 설정은 **다음 push부터** 적용돼요. 소유자·관리자만 바꿀 수 있어요.

## Git 연결: 저장소 가져오기, 비공개 저장소와 GitLab

콘솔의 배포 → **Git 연결**과 같은 것들이에요.

```
list_git_access               연결한 저장소(connected), GitHub 계정, 저장한 토큰(끝 네 글자)
list_importable_repositories  GitHub App이 볼 수 있는 저장소 찾기 (query는 전체에서, refresh는 다시 묻기)
import_repository             저장소를 연결하고 첫 배포 시작 (빌드 설정·dotenv를 함께)
disconnect_repository         저장소 하나의 연결 해제: push해도 배포하지 않음, 배포 기록은 그대로
set_checks_on_server          검사를 One Server에서 실행: GitHub 저장소의 Actions 작업을 One Server 머신에서(켜기·끄기)
add_repository                첫 push 전에 저장소 붙이기 (github: owner/name, gitlab: group/name)
set_repo_token                비공개 저장소를 받을 토큰 (자체 GitLab이면 gitlab_base)
remove_repo_token             토큰 지우기
link_github_app               조직의 다른 프로젝트에 연결된 GitHub 계정을 이 프로젝트에 연결
unlink_github_app             GitHub 계정(App 설치) 하나 또는 전부의 연결 해제
```

GitHub 계정을 **연결하는** 것은 GitHub에서 사람이 직접 확인해야 해서 콘솔의 배포 → Git 연결에서만 돼요. 연결한 뒤의 가져오기는 MCP로 할 수 있어요.
비공개 저장소의 빌드가 받기 단계에서 실패하면 `list_git_access`부터 보세요. 모두 소유자·관리자만 바꿀 수 있어요.

## 다른 서버에서 가져오기

콘솔의 조직 → **다른 서버에서 가져오기**와 같아요. 원본 계정(Supabase, Vercel, PostgreSQL)은 콘솔에서 한 번 연결해요(토큰을 대화에 붙여 넣지 않게). Supabase는 [Supabase로 로그인]으로 허락만 누르면 연결돼요(토큰 복사 없음).

```
import_sources                연결한 원본 계정과 그 프로젝트, 지금까지의 가져오기
start_import                  가져오기 시작 (콘솔에서 나눈 import, 또는 connection+project로 묶음마다 새 프로젝트)
get_import                    옮길 곳마다 단계별 진행, 옮기지 않는 것과 이유
copy_auth_from_import         Supabase 프로젝트의 로그인 설정(소셜 제공자, 돌아갈 주소)을 이 서버의 프로젝트로 복사
set_vault_project_keys       이 프로젝트 자기 주소·비밀 키를 Vault에 넣기(서버 안에서만 옮겨요)
copy_vault_secrets_from_project  다른 프로젝트의 Vault 비밀을 값째 복사(값은 서버 안에서만 옮겨요)
copy_jobs_from_project        다른 프로젝트의 예약 작업을 꺼진 채로 복사
copy_auth_from_project        다른 프로젝트의 로그인 설정을 이 프로젝트로 복사(시크릿은 서버 안에서만 옮겨요)
copy_storage_from_project     다른 프로젝트의 버킷과 파일을 이 프로젝트로 복사 (같은 크기면 건너뛰어요)
copy_storage_from_import      Supabase 프로젝트의 버킷과 파일을 이 서버의 프로젝트로 복사 (같은 크기면 건너뛰어요)
```

Supabase 원본은 start_import의 address에 세션 풀러 연결 문자열을 넣어요. 이번 실행에만 쓰고 지워요. 예약 작업은 꺼진 채로 옮겨요. REST로는 `GET /v1/imports/sources`, `POST /v1/imports {org, import, connection, project, address}`, `GET /v1/imports/<id>`이에요.

이미 있는 프로젝트로 로그인 설정만 옮길 때는 `copy_auth_from_import`예요. 시크릿은 연결한 계정으로 Supabase에서 바로 읽어 저장하고 답에는 넣지 않아요. 먼저 `dry_run`으로 무엇이 옮겨질지 보고, 끝나면 답의 `callback_url`을 각 제공자 콘솔에 등록하세요. 등록 전에는 로그인이 실패해요. REST로는 `POST /v1/projects/<ref>/config/auth/copy-from-import {connection, project, providers, dry_run}`이에요.

파일만 옮길 때는 `copy_storage_from_import`예요. 여기 없는 버킷은 Supabase와 같은 공개 여부·한도로 만들고, 파일은 크기가 같은 것이 이미 있으면 건너뛰어요. 한 번에 파일 200개·45초까지 옮기고, 남으면 답의 `next`를 넘겨 `done`이 true가 될 때까지 다시 불러요. Supabase 키는 서버 안에서만 쓰고 답에 넣지 않아요. 버킷의 접근 규칙은 옮기지 않아요. REST로는 `POST /v1/projects/<ref>/storage/copy-from-import {connection, project, buckets, rename, dry_run, next, max_files}`이에요.

## push 없이 배포하기

```
redeploy             같은 커밋을 다시 빌드 (환경 변수를 나중에 넣었을 때)
deploy_commit        특정 커밋을 배포
promote_deployment   빌드가 끝난 프리뷰를 프로덕션으로
rollback_deployment  프로덕션을 예전 프로덕션 배포로 (다시 빌드하지 않음, 프로덕션 보류)
create_deploy_hook   부르면 브랜치의 최신 커밋을 빌드하는 주소 (CMS 발행 등)
list_deploy_hooks    그 주소들
remove_deploy_hook   주소 지우기
```

**환경 변수를 바꿨다면 redeploy가 필요해요.** 빌드 결과에 박힌 값은 다시 빌드해야 바뀌어요.
프리뷰를 프로덕션으로 올리면 프로덕션 환경 변수로 **다시 빌드**해요. 프리뷰 빌드를 그대로 쓰지 않아요.
프로덕션으로 가는 배포는 소유자·관리자만 할 수 있어요.

## 저장소 없이 배포하기

```
deploy_files    파일(경로 → 내용)을 보내 빌드하고 배포 (합쳐 3MB까지, 넘으면 create_source_upload_url → deploy_source)
deploy_source   이미 올린 압축 파일(source)을 배포
create_source_upload_url   큰 압축 파일을 토큰 없이 PUT할 주소 (1시간)
```

- push된 커밋과 **같은 빌드**를 거쳐요. package.json이 있으면 프레임워크를 찾아 빌드하고, `index.html`만 있으면 그대로 서비스해요.
- `prebuilt: true`면 빌드하지 않고 보낸 파일을 그대로 서비스해요(이미 빌드한 `dist/`).
- `target`은 `preview`(기본) 또는 `production`이에요. 프리뷰 주소는 `branch`(기본 `preview`)로 정해져요. 앱이 없는 프로젝트에는 `site` 앱이 생겨요.
- 큰 사이트는 `oneend deploy`(폴더를 압축해 올림)나 `POST /deployments/sources`(본문: .zip, .tar, .tar.gz, 50MB까지)를 쓰고, 받은 `source`로 배포해요.
- 도구만 쓸 때는 `create_source_upload_url`로 주소를 받아 압축 파일을 PUT하고(`curl -X PUT --data-binary @site.zip '<url>'`), 답의 `source`로 `deploy_source`를 불러요. 주소는 1시간 동안 그 프로젝트에만 쓰이고, 크기 한도와 검사는 `POST /deployments/sources`와 같아요. REST로는 `POST /v1/projects/<ref>/deployments/sources/upload-urls`이에요.
- `node_modules`와 `.git`은 빼고 올려요. 폴더 밖을 가리키는 경로나 링크, 압축을 풀면 너무 커지는 파일은 거절해요.

## 빌드 설정 더, 시스템 변수, 배포 보안

```
get_build_settings        앱마다 개발 명령, Node.js 버전, 빌드 건너뛰기, 프로덕션 먼저, 필요한 검사
set_build_dev_settings    개발 명령(oneend dev)과 Node.js 버전 (18 | 20 | 22 | 24)
set_ignored_build_step    빌드할 push (automatic | every | production | preview | folder | command)
set_production_first      대기열에서 프로덕션 빌드 먼저
set_git_checks            필요한 GitHub 검사, Git LFS, 서명된 커밋만
set_expose_system_env     BLACKWALL_ENV, BLACKWALL_URL 같은 시스템 변수 넣기·빼기
set_directory_listing     index.html 없는 폴더의 파일 목록 보이기
get_deployment_security   빌드 로그·출력을 볼 사람, 포크 보호, OIDC
set_deployment_security   위를 바꾸기 (빠진 항목은 그대로)
```

프레임워크, 빌드·설치·출력 명령, 빌드 머신은 `update_app`이에요. 모두 소유자·관리자만 바꿀 수 있어요.

## 배포 보관 기간과 되돌리기

```
get_deployment_retention   배포 종류별 보관 기간(지금 적용되는 날 수, 프로젝트 값, 요금제 기본값)
set_deployment_retention   보관 기간 바꾸기 (null이면 요금제 기본값, 프로젝트 관리자)
list_deleted_deployments   3일 안에 삭제된 배포와 되돌릴 수 있는 마감
delete_deployment          배포 삭제 (3일 동안 되돌릴 수 있어요)
restore_deployment         삭제한 배포 되돌리기
```

- 요금제 기본값: Free는 모두 30일. Pro·Enterprise는 취소 30일, 실패 90일, 프리뷰 180일, 프로덕션 1년(365일). 프로젝트가 정한 값이 있으면 그걸 따라요.
- 기간과 상관없이 남기는 배포: 가장 최근 배포 10개, 가장 최근 완료된 프로덕션 배포 20개, 가장 최근 완료된 프로덕션 외 배포 20개, 프로덕션·브랜치·도메인이 지금 서비스 중인 배포(브랜치마다 최근 프리뷰 포함).
- 삭제한 배포와 기간이 지나 삭제된 배포는 3일 동안 `restore_deployment`로 되돌릴 수 있어요. 그동안 그 배포의 `dpl-…` 주소는 410으로 답하고, 3일이 지나면 파일과 빌드 로그를 지워 되돌릴 수 없어요.
- REST로는 `GET·PUT /v1/projects/<ref>/deployment-retention`, `GET /v1/projects/<ref>/deployments/deleted`, `DELETE /v1/projects/<ref>/deployments/<id>`, `POST /v1/projects/<ref>/deployments/<id>/restore`이에요. CLI는 `oneend deployments retention`, `oneend deployments deleted`, `oneend deployments rm`, `oneend deployments restore`예요.

## 배포의 주소, 기다리기, 보안 점검

- 배포를 만드는 도구(`deploy_files`, `deploy_source`, `deploy_commit`, `redeploy`, `promote_deployment`)와 `get_deployment`, `list_deployments`, `rollback_deployment`는 주소를 함께 줘요. `url`은 그 배포만의 `dpl-…` 주소, `production_url`은 프로덕션 주소, `preview_url`은 그 브랜치의 프리뷰 주소예요. 프리뷰 보호가 켜져 있으면 `note`가 누가 열 수 있는지 말해요.
- `rollback_deployment` 뒤에는 그 앱의 프로덕션이 보류돼요(`production_held`, 콘솔의 "프로덕션 보류"). 새 프로덕션 빌드는 만들어지지만 프로덕션 주소로 나가지 않아요. `promote_deployment`로 빌드가 끝난 프로덕션 배포를 올리면 다시 빌드하지 않고 바로 나가고 보류가 풀려요. 지금 배포를 올리면 보류만 풀려요. `list_deployments`의 `production_held`가 보류된 앱을 알려 줘요.
- `wait`(초, 120까지)를 주면 빌드가 끝날(`ready`·`error`) 때까지 기다려 결과를 한 번에 줘요. 실패하면 `error`와 `build_log` 끝부분이 오고, 시간 안에 안 끝나면 `still_running: true`와 `retry_after_seconds`가 와요. 그 뒤에 `get_deployment`를 `wait`와 함께 부르면 이어서 기다려요. `create_project`의 `wait`는 데이터베이스가 준비될 때까지 기다리고, `list_projects`의 `status`가 `ready`·`provisioning`·`failed`·`paused`를 알려줘요.
- 답의 `advisors`는 `get_advisors`의 심각한 보안 문제(누구나 쓸 수 있는 규칙, 규칙을 무시하는 뷰 같은 것) 수와 상위 3개예요. 기본은 알리기만 하고 배포해요. `block_on_advisors: true`를 주면 그런 문제가 있는 동안 프로덕션 배포를 거절해요(프리뷰는 막지 않아요). `oneend deploy --block-on-advisors`도 같아요. git push로 시작한 배포는 막지 않아요.

---

https://end.blackall.ai/docs/deploy?lang=ko
