diff --git a/README.ko.md b/README.ko.md
index 05befbb..9eb31f7 100644
--- a/README.ko.md
+++ b/README.ko.md
@@ -28,21 +28,90 @@ Railway, Coolify, Dokploy는 각 권리자의 상표입니다. Aven은 그 주
## 빠른 시작
-아래 Linux 설치 명령은 [`install-server.sh`](install-server.sh)에 있습니다. Windows 10/11에서 네이티브 Windows 서비스로 실행하려면 [Windows 가이드](docs/windows-native.ko.md)를 참고하세요. Windows Server 에디션은 Docker Desktop 지원 대상이 아닙니다.
+Linux 설치 스크립트는 [`install-server.sh`](install-server.sh)입니다. 명령은 하나지만, 직접 운영하는 머신과 Docker가 필요하고, 자동 TLS를 쓰려면 내가 관리하는 도메인의 DNS도 필요합니다. 서버와 도메인 비용은 따로 냅니다. 이 저장소는 사용량 요금을 받지 않습니다.
-필요한 것:
+Windows 10/11에서 네이티브 Windows 서비스로 실행하는 길은 다릅니다. [Windows 가이드](docs/windows-native.ko.md)를 보세요. Windows Server 에디션은 Docker Desktop 지원 대상이 아닙니다. 아래는 Linux 경로입니다.
-- systemd와 Docker가 있고 80·443 포트가 비어 있는 Linux 서버(amd64 또는 arm64)
-- 대시보드용 DNS 이름(예: `aven.example.com`)과, 생성 도메인을 쓰려면 와일드카드(`*.apps.example.com`)
-- 여러 서버를 쓸 경우 Tailscale이나 WireGuard 같은 사설망으로 서로 닿는 추가 서버(선택)
+### 필요한 것
+
+설치 스크립트가 확인하는 것은 첫 목록입니다. 메모리, 디스크, Docker 버전 번호는 재지 않습니다.
+
+- **머신.** Linux, amd64(`x86_64`) 또는 arm64(`aarch64`), systemd(`systemctl`과 `/run/systemd/system`). `curl`, `tar`, 그리고 `sha256sum` 또는 `openssl`.
+- **Docker.** `docker` 명령이 있고, root로 `docker info`가 응답해야 합니다. unit 파일에 `User=`가 없으므로 `aven-server`는 root로 돕니다. Docker가 없으면 확인을 받거나 `--install-docker`를 주면 get.docker.com으로 설치할 수 있습니다. 스크립트가 띄우는 프록시 이미지는 `caddy:2-alpine`입니다.
+- **메모리와 디스크.** 검사하지 않습니다. `aven-server`는 프로세스 하나이고, 프록시는 컨테이너 하나입니다. 앱과 데이터베이스는 컨테이너가 더 생기고, 데이터베이스는 볼륨을 씁니다. 제어 평면 파일(`aven.db`, `master.key`)은 `/var/lib/aven`에 있습니다. 올릴 이미지와 볼륨을 담을 디스크를 두세요.
+- **이 호스트의 포트.** 80/tcp, 443/tcp, 443/udp가 비어 있어야 합니다. 프록시가 셋을 모두 publish합니다(443/udp는 HTTP/3). 대시보드 API는 `127.0.0.1:7070`만 듣고 바깥으로 열지 않습니다. 서버가 둘 이상이면 mesh는 그 노드의 사설 주소가 사설 IP일 때 그 주소의 7443 포트만 듣습니다.
+- **자동 TLS를 위한 이름과 DNS.** 대시보드용 `https://` 이름(예: `aven.example.com`). 서비스 호스트 이름을 자동으로 만들려면 `apps.example.com` 같은 기본 도메인. 인증서가 필요한 첫 요청 전에 이 서버를 가리키게 합니다.
+
+ | 이름 | 종류 | 값 |
+ | --- | --- | --- |
+ | `aven.example.com` | A 그리고/또는 AAAA | 이 서버의 공인 주소 |
+ | `*.apps.example.com` | A 그리고/또는 AAAA | 같은 주소 |
+
+ 인증서는 호스트 이름마다 첫 TLS 핸드셰이크 때 받습니다(HTTP-01 또는 TLS-ALPN). 와일드카드 이름 자체에 인증서 하나를 요청하지는 않습니다. 인터넷에서 80과 443에 닿아야 합니다. 입력한 기본 도메인이 `AVEN_BASE_DOMAIN`이 됩니다.
+
+ 도메인이 없으면 대시보드는 그 머신의 `http://localhost:7070`에만 열립니다.
+
+서버를 더 두는 것은 선택입니다. 추가 머신에는 Docker가 있어야 하고, 메인 서버에 HTTPS로 닿아야 합니다. 서로 다른 머신의 서비스가 통신하려면 Tailscale이나 WireGuard 같은 사설망을 씁니다.
+
+### 스크립트를 읽고 실행
+
+```sh
+curl -fsSL https://raw.githubusercontent.com/turin-dev/aven/main/install-server.sh -o install-server.sh
+less install-server.sh # 또는: sh install-server.sh --help
+sh install-server.sh
+```
+
+일반 사용자로 실행합니다. 스크립트가 `sudo`로 스스로를 다시 실행합니다. `sudo sh`로 파이프하면 sudo-rs(Ubuntu 25.10부터 기본 sudo)가 스크립트를 터미널의 백그라운드에 두어 질문을 할 수 없으므로, `--yes`가 없으면 멈추고 그 이유를 출력합니다.
+
+터미널에서는 먼저 이 머신의 역할을 묻습니다. 메인 서버(대시보드와 API)인지, 이미 있는 메인 서버에 붙는 리전 서버인지입니다. 메인 서버는 이어서 대시보드 주소, 기본 도메인, Let's Encrypt 이메일(선택), `admin@local` 비밀번호를 묻습니다. 요약을 보여 주고, 확인하기 전에는 아무것도 바꾸지 않습니다. 릴리스 체크섬을 확인한 뒤 아래 파일을 설치하고 `/etc/aven/aven.env`를 쓴 다음 서비스를 시작하고, 로그인 정보와 DNS 이름을 출력합니다.
+
+스크립트를 읽은 뒤 무인으로 실행하려면:
```sh
-curl -fsSL https://raw.githubusercontent.com/turin-dev/aven/main/install-server.sh | sh
+sudo sh install-server.sh --yes \
+ --public-url https://aven.example.com \
+ --base-domain apps.example.com \
+ --acme-email you@example.com
```
-터미널에서 실행하면 대시보드 주소, 서비스용 기본 도메인, Let's Encrypt 이메일, 관리자 비밀번호를 차례로 묻고 요약을 보여 준 뒤 확인을 받고 설치합니다. 스크립트는 릴리스 체크섬을 확인하고 `aven-server`, CLI, Railpack을 설치합니다. 설정을 `/etc/aven/aven.env`에 쓰고(관리자 비밀번호를 정하지 않으면 새로 만듭니다), 서비스를 시작한 뒤 로그인 정보와 만들어야 할 DNS 레코드를 알려 줍니다. `aven.example.com`(생성 도메인을 쓰려면 `AVEN_BASE_DOMAIN`과 함께 `*.apps.example.com`도)을 서버로 향하게 하고 로그인한 다음, *어드민 → Owner · 초기 설정*의 체크리스트를 따라가세요.
+### 메인 서버 설치가 바꾸는 것
+
+- `/usr/local/bin/aven-server`와 `/usr/local/bin/aven`
+- `/usr/local/bin/railpack`. `--no-railpack`을 주거나 `railpack`이 이미 `PATH`에 있으면 건너뜁니다(스크립트가 받는 Railpack 릴리스 기본값은 v0.42.0)
+- `/usr/local/lib/aven/`의 agent와 CLI 다운로드
+- `/etc/systemd/system/aven-server.service`
+- `/etc/aven/aven.env`(모드 0600)와 `/etc/aven/aven.env.example`
+- 첫 기동 때 `/var/lib/aven`(`aven.db`, `master.key`, `session.key`, `logs/`), 이름이 `aven`인 Docker 네트워크, 볼륨 `aven-caddy-data`, `aven-caddy-config`, `aven-proxy-wake`를 가진 `aven-proxy` 컨테이너
+
+리전 서버는 `aven-server`를 설치하지 않습니다. `/usr/local/bin/aven-agent`를 설치하고 `/etc/keel-agent/agent.env`를 쓰며, 노드 자격 증명은 `/var/lib/keel-agent/credentials.json`에 두고 `keel-agent.service`를 켭니다. Keel을 쓴 적이 없는 머신도 지금 설치 스크립트가 만드는 경로와 unit 이름입니다. 이미 있는 agent를 그 자리에서 바꾸기 위해서입니다. 그 env 파일 안의 설정 이름은 `AVEN_*`입니다. `KEEL_*`도 계속 읽습니다.
+
+이미 `/etc/keel/aven.env`나 `/var/lib/keel`이 있는 설치는 그 경로를 유지합니다. [Keel에서 업그레이드](#keel에서-업그레이드)를 보세요.
+
+### 켜졌는지 확인
+
+```sh
+systemctl is-active aven-server
+curl -fsS -o /dev/null http://127.0.0.1:7070/api/cli/downloads
+docker ps --filter name=aven-proxy --format '{{.Names}} {{.Status}}'
+```
+
+설치 스크립트는 그 URL이 응답할 때까지 기다린 뒤 대시보드 주소를 출력하고, 비밀번호를 새로 만들었으면 그것도 보여 줍니다. `admin@local`로 로그인합니다. 한국어 UI 이름은 **어드민 → Owner · 초기 설정**(`/admin/?section=setup`)입니다. 영어 UI 이름은 **Admin → Owner · Setup**입니다.
+
+프로젝트를 열고 캔버스에서 **추가**를 눌러 서비스를 배포합니다. 설치 옵션 전체, 직접 설치, 백업과 복원(PITR 포함), 업그레이드, 제거, 아래 실패 상황의 긴 설명은 [셀프 호스팅 가이드](docs/self-hosting.ko.md)에 있습니다.
+
+### 안 올라오면
+
+- **80 또는 443 포트를 누군가 쓰는 중.** 설치 스크립트는 경고만 하고 계속합니다. 그다음 `aven-server`가 종료됩니다. `journalctl -u aven-server`에 `start the proxy:`와 `ports AVEN_HTTP_PORT/AVEN_HTTPS_PORT, default 80/443, must be free`가 있습니다. `ss -Htlpn 'sport = :80'`으로 누가 듣는지 봅니다.
+- **DNS가 아직 퍼지지 않았거나 TLS가 발급되지 않음.** 설치는 인증서를 기다리지 않습니다. 대시보드 이름이 해석되지 않으면 설치 스크립트가 경고합니다. 인증서는 첫 HTTPS 요청 때 요청됩니다. ACME 오류는 `docker logs aven-proxy`에 있습니다. 아직 `keel-proxy`가 도는 설치는 그 컨테이너 로그를 봅니다.
+- **Docker.** root로 `docker info`가 되어야 합니다. "not running"과 "cannot use its socket"은 다른 오류입니다. 뒤의 것은 권한 문제입니다.
+- **방화벽 또는 IPv6.** Docker가 publish한 포트는 ufw의 INPUT 체인을 타지 않습니다. 호스트 앞의 클라우드 방화벽은 그대로 적용됩니다. 이 호스트가 응답하지 않는 주소로 AAAA를 만들면 IPv6를 선호하는 클라이언트만 실패하고 IPv4는 됩니다.
+- **agent가 붙지 않음.** `journalctl -u keel-agent`. 참여 토큰은 한 번만 쓸 수 있습니다. 기본 수명은 1시간입니다(API는 60초에서 24시간까지 받습니다). 메인 서버의 `AVEN_PUBLIC_URL`은 `https://` 주소여야 합니다.
+
+고치는 명령은 [문제 해결](docs/self-hosting.ko.md#troubleshooting)에 있습니다.
+
+### 직접 설치하거나 제거
-그다음 프로젝트를 열고 캔버스에서 **추가**를 눌러 첫 서비스를 배포합니다. DNS, 설정, 직접 설치, 백업, 업그레이드, 서버 추가는 [셀프 호스팅 가이드](docs/self-hosting.ko.md)에 있습니다.
+직접 설치는 [셀프 호스팅 가이드 3–6단계](docs/self-hosting.ko.md#3-build)입니다. 제거 스크립트는 없습니다. [제거와 되돌리기](docs/self-hosting.ko.md#uninstall-and-roll-back)에 멈출 unit, 컨테이너, 지울 파일과, `/var/lib/aven/backups/`의 업그레이드 복사본과 `/usr/local/bin/aven-server.prev`를 되돌리는 방법이 있습니다.
## 화면
@@ -171,7 +240,7 @@ Aven은 Keel이라는 이전 프로젝트를 잇습니다. 이 공개 저장소
- GitHub 저장소(Dockerfile 또는 Dockerfile 없이 [Railpack](https://github.com/railwayapp/railpack)), Docker 이미지, Docker Compose 파일, 템플릿, 단일 파일 function, 로컬 디렉터리(`aven up`)로 서비스를 만듭니다.
- push 자동 배포, watch paths, CI 대기, 커밋 상태 표시, PR 미리보기 환경.
- 변수 참조(`${{ db.DATABASE_URL }}`), 공유·봉인 변수, raw 편집기, config as code(`railway.json`, `aven.json`)를 갖춘 환경.
-- pre-deploy 명령, 헬스체크, 무중단 교체, 롤백, cron 작업, 앱 sleep, 이미지 자동 업데이트.
+- pre-deploy 명령, 헬스체크, 무중단 교체, 롤백, 이미지 자동 업데이트. cron 일정과 앱 sleep은 구현되어 있고, 위 표에서 실험적으로 표시합니다.
**데이터**
- 클릭 한 번으로 PostgreSQL, MySQL, Redis, MongoDB, S3 호환 버킷. 볼륨, 데이터 브라우저, S3로 보내는 논리 백업.
@@ -260,7 +329,7 @@ Railway는 그 권리자의 상표이며, 여기서는 호환성을 설명하는
## 문서
-- [셀프 호스팅 가이드](docs/self-hosting.ko.md): 설치, DNS, 설정, 백업, 업그레이드, 노드
+- [셀프 호스팅 가이드](docs/self-hosting.ko.md): 설치, DNS, 설치 스크립트가 바꾸는 파일, 백업과 복원, 업그레이드, 제거, 문제 해결
- [Aven on Aven](docs/selfhost.ko.md): Aven이 자기 서버를 관리하고 업그레이드하기
- [CLI 안내](docs/cli.md)
- [HTTP API](docs/api-v1.md)와 [GraphQL API](docs/graphql.md)
@@ -276,7 +345,7 @@ Railway는 그 권리자의 상표이며, 여기서는 호환성을 설명하는
**GitHub가 꼭 필요한가요?** 아니요. Docker 이미지, Compose 파일, 템플릿, 로컬 디렉터리(`aven up`)로도 배포할 수 있습니다. push 자동 배포와 미리보기 환경에만 GitHub가 필요합니다.
-**어떻게 업그레이드하나요?** 설치 스크립트를 다시 실행하거나, Aven이 자기 캔버스에서 스스로 업그레이드하게 하세요([Aven on Aven](docs/selfhost.ko.md)). DB 마이그레이션은 시작할 때 자동으로 실행됩니다.
+**어떻게 업그레이드하나요?** 설치 스크립트를 다시 실행하거나, Aven이 자기 캔버스에서 스스로 업그레이드하게 하세요([Aven on Aven](docs/selfhost.ko.md)). 스크립트는 `/etc/aven/aven.env`를 유지하고, 서버를 멈춘 뒤 `aven.db`(아직 그 파일이 DB라면 `keel.db`도)를 `/var/lib/aven/backups/`에 복사하며, 이전 바이너리를 `/usr/local/bin/aven-server.prev`로 남깁니다. 마이그레이션은 시작할 때 실행됩니다. `master.key`는 스크립트가 복사하지 않으니 따로 보관하세요. 마이그레이션이 이미 돌았다면 DB 복사본 없이 이전 바이너리만 되돌리면 열리지 않을 수 있습니다. 절차는 [셀프 호스팅 가이드](docs/self-hosting.ko.md#8-backups-and-upgrades)에 있습니다.
**Railway나 Dokploy에서 옮겨 올 수 있나요?** Aven은 `railway.json` / `railway.toml`을 읽고 Railway 형태의 GraphQL 질의에 답합니다. `aven import dokploy`는 Dokploy 설치의 서비스, 변수, 도메인, 볼륨을 다시 만듭니다. 데이터는 옮기지 않으니 DB 내용은 평소 쓰는 덤프·복원 도구로 옮기세요.
diff --git a/README.md b/README.md
index 720a5d5..23f6ef3 100644
--- a/README.md
+++ b/README.md
@@ -28,21 +28,90 @@ Railway, Coolify, and Dokploy are trademarks of their owners. Aven is not affili
## Quick start
-The Linux install command below is the one in [`install-server.sh`](install-server.sh). For a native Windows service on Windows 10/11, see the [Windows host guide](docs/windows-native.md). Windows Server editions are not supported by Docker Desktop.
+The Linux installer is [`install-server.sh`](install-server.sh). One command starts the install. It still needs a machine you operate, Docker, and, for automatic TLS, a domain whose DNS you control. You pay for the server and for the domain. This repository does not charge a usage fee.
-You need:
+A native Windows service on Windows 10/11 is a different path: [Windows host guide](docs/windows-native.md). Windows Server editions are not supported by Docker Desktop. The steps below are the Linux path.
-- a Linux server (amd64 or arm64) with systemd and Docker, ports 80 and 443 free;
-- a DNS name for the dashboard (for example `aven.example.com`) and, for generated service domains, a wildcard (`*.apps.example.com`);
-- optionally more servers for multi-node setups, reachable from each other over a private network such as Tailscale or WireGuard.
+### What you need
+
+The installer checks the first list. It does not measure memory, disk, or a Docker version.
+
+- **Machine.** Linux, amd64 (`x86_64`) or arm64 (`aarch64`), with systemd (`systemctl`, and `/run/systemd/system`). `curl`, `tar`, and `sha256sum` or `openssl`.
+- **Docker.** The `docker` command, and a daemon that answers `docker info` for root. The unit file does not set `User=`, so `aven-server` runs as root. If Docker is missing, the script can install it from get.docker.com when you confirm or pass `--install-docker`. The proxy image it starts is `caddy:2-alpine`.
+- **Memory and disk.** Not checked. `aven-server` is one process. The proxy is one container. Each app and database is another container and, for databases, a volume. The control-plane files (`aven.db`, `master.key`) live under `/var/lib/aven`. Leave disk for the images and volumes you are going to run.
+- **Ports on this host.** 80/tcp, 443/tcp, and 443/udp free. The proxy publishes all three (443/udp is HTTP/3). The dashboard API listens on `127.0.0.1:7070` and is not published. With more than one server, the mesh listens only on that node's private address, port 7443, when the address is a private IP.
+- **A name and DNS, for automatic TLS.** An `https://` name for the dashboard, for example `aven.example.com`. For generated service hostnames, a base domain such as `apps.example.com`. Point them at this server before the first request that needs a certificate:
+
+ | Name | Type | Value |
+ | --- | --- | --- |
+ | `aven.example.com` | A and/or AAAA | this server's public address |
+ | `*.apps.example.com` | A and/or AAAA | the same address |
+
+ Each concrete hostname gets its own certificate on the first TLS handshake (HTTP-01 or TLS-ALPN). Aven does not request one certificate for the wildcard name itself. Ports 80 and 443 have to be reachable from the public internet. The base domain you type becomes `AVEN_BASE_DOMAIN`.
+
+ With no domain, the installer can serve the dashboard only at `http://localhost:7070` on that machine.
+
+More than one server is optional. Extra machines need Docker and must reach the main server over HTTPS. A private network such as Tailscale or WireGuard is how services on different machines reach each other.
+
+### Read the script, then run it
+
+```sh
+curl -fsSL https://raw.githubusercontent.com/turin-dev/aven/main/install-server.sh -o install-server.sh
+less install-server.sh # or: sh install-server.sh --help
+sh install-server.sh
+```
+
+Run that as a normal user. The script starts itself again with `sudo`. Piped into `sudo sh`, sudo-rs (the default sudo since Ubuntu 25.10) keeps the script in the background of the terminal, where it cannot ask questions, and it stops unless `--yes` is set.
+
+On a terminal the first question is the role: main server (dashboard and API) or region server (joins an existing main server). A main server then asks for the dashboard URL, the base domain, an optional Let's Encrypt email, and the `admin@local` password. It shows a summary and changes nothing until you confirm. It checks the release checksum, installs the files below, writes `/etc/aven/aven.env`, starts the service, and prints the sign-in details and the DNS names.
+
+Unattended, after you have read the script:
```sh
-curl -fsSL https://raw.githubusercontent.com/turin-dev/aven/main/install-server.sh | sh
+sudo sh install-server.sh --yes \
+ --public-url https://aven.example.com \
+ --base-domain apps.example.com \
+ --acme-email you@example.com
```
-On a terminal the script asks for the dashboard URL, the base domain for services, a Let's Encrypt email and the admin password, shows a summary and installs after you confirm. It verifies the release checksum, installs `aven-server`, the CLI and Railpack, writes `/etc/aven/aven.env` (generating the admin password unless you chose one), starts the service and prints the sign-in details and the DNS records to create. Point `aven.example.com` (and, for generated service domains, `*.apps.example.com` with `AVEN_BASE_DOMAIN`) at the server, sign in, and follow the first-run checklist under *Admin → Owner · Setup*.
+### What a main-server install changes
+
+- `/usr/local/bin/aven-server` and `/usr/local/bin/aven`
+- Railpack at `/usr/local/bin/railpack`, unless you pass `--no-railpack` or `railpack` is already on `PATH` (the script's default Railpack release is v0.42.0)
+- agent and CLI downloads in `/usr/local/lib/aven/`
+- `/etc/systemd/system/aven-server.service`
+- `/etc/aven/aven.env` (mode 0600) and `/etc/aven/aven.env.example`
+- on first start, `/var/lib/aven` (`aven.db`, `master.key`, `session.key`, `logs/`), a Docker network named `aven`, and the `aven-proxy` container with volumes `aven-caddy-data`, `aven-caddy-config`, and `aven-proxy-wake`
+
+A region server does not install `aven-server`. It installs `/usr/local/bin/aven-agent`, writes `/etc/keel-agent/agent.env`, stores the node credential in `/var/lib/keel-agent/credentials.json`, and enables `keel-agent.service`. Those are the paths and the unit name the agent installer still creates, including on a machine that never ran Keel, so an existing agent is replaced in place. Settings inside that env file use the `AVEN_*` names. `KEEL_*` is still read.
+
+An install that already has `/etc/keel/aven.env` or `/var/lib/keel` keeps those paths. See [Upgrading from Keel](#upgrading-from-keel).
+
+### Check that it started
+
+```sh
+systemctl is-active aven-server
+curl -fsS -o /dev/null http://127.0.0.1:7070/api/cli/downloads
+docker ps --filter name=aven-proxy --format '{{.Names}} {{.Status}}'
+```
+
+The installer waits until that URL answers, then prints the dashboard URL and the password if it generated one. Sign in as `admin@local`. The English UI label is **Admin → Owner · Setup** (`/admin/?section=setup`). The Korean UI label is **어드민 → Owner · 초기 설정**.
+
+Open a project, press **Add** on the canvas, and deploy a service. The [self-hosting guide](docs/self-hosting.md) is the long version: every installer option, a manual install, backups and restore (including PITR), upgrades, uninstall, and the failure modes below.
+
+### If it does not come up
+
+- **Port 80 or 443 is taken.** The installer warns and continues. `aven-server` then exits. `journalctl -u aven-server` contains `start the proxy:` and `ports AVEN_HTTP_PORT/AVEN_HTTPS_PORT, default 80/443, must be free`. `ss -Htlpn 'sport = :80'` shows the listener.
+- **DNS has not propagated, or TLS is not issued.** The install does not wait for a certificate. If the dashboard name does not resolve, the installer warns. Certificates are requested on the first HTTPS request. `docker logs aven-proxy` shows ACME errors. On an install that still runs `keel-proxy`, read that container instead.
+- **Docker.** `docker info` has to succeed as root. "not running" and "cannot use its socket" are different errors. The second one is a permissions failure.
+- **Firewall or IPv6.** Docker-published ports are not filtered by ufw's INPUT chain. A cloud firewall in front of the host still is. An AAAA record for an address this host does not answer on makes clients that prefer IPv6 fail while IPv4 works.
+- **The agent does not join.** `journalctl -u keel-agent`. The join token is single-use. The default lifetime is 1 hour (the API accepts 60 seconds to 24 hours). `AVEN_PUBLIC_URL` on the main server has to be an `https://` URL.
+
+Commands and what to change are in [Troubleshooting](docs/self-hosting.md#troubleshooting).
+
+### Install by hand, or remove it
-Then open a project, press **Add** on the canvas and deploy your first service. The [self-hosting guide](docs/self-hosting.md) covers DNS, configuration, manual installation, backups, upgrades and adding servers.
+Hand install is [steps 3–6 of the self-hosting guide](docs/self-hosting.md#3-build). There is no uninstall script. [Uninstall and roll back](docs/self-hosting.md#uninstall-and-roll-back) lists the units, containers, and files to stop and delete, and how an upgrade copy under `/var/lib/aven/backups/` plus `/usr/local/bin/aven-server.prev` is put back.
## Screenshots
@@ -171,7 +240,7 @@ AI coding agents were used substantially in development. Changes went through hu
- Services from GitHub repositories (Dockerfile or [Railpack](https://github.com/railwayapp/railpack), no Dockerfile needed), Docker images, Docker Compose files, templates, single-file functions or a local directory (`aven up`).
- Push-to-deploy with watch paths, wait-for-CI, commit statuses and pull request preview environments.
- Environments with variable references (`${{ db.DATABASE_URL }}`), shared and sealed variables, a raw editor and config as code (`railway.json`, `aven.json`).
-- Pre-deploy commands, health checks, zero-downtime overlap, rollbacks, cron jobs, app sleeping and image auto-updates.
+- Pre-deploy commands, health checks, zero-downtime overlap, rollbacks, and image auto-updates. Cron schedules and app sleeping are implemented and marked experimental in the table above.
**Data**
- One-click PostgreSQL, MySQL, Redis, MongoDB and S3-compatible buckets, with volumes, a data browser and logical backups to S3.
@@ -260,7 +329,7 @@ Railway is a trademark of its owner and is used here only to describe compatibil
## Documentation
-- [Self-hosting guide](docs/self-hosting.md): install, DNS, configuration, backups, upgrades, nodes
+- [Self-hosting guide](docs/self-hosting.md): install, DNS, what the installer changes, backups and restore, upgrades, uninstall, troubleshooting
- [Aven on Aven](docs/selfhost.md): let Aven manage and upgrade its own server
- [CLI reference](docs/cli.md)
- [HTTP API](docs/api-v1.md) and [GraphQL API](docs/graphql.md)
@@ -276,7 +345,7 @@ Railway is a trademark of its owner and is used here only to describe compatibil
**Do I need GitHub?** No. You can deploy Docker images, Compose files, templates and local directories (`aven up`). GitHub is needed for push-to-deploy and preview environments.
-**How do I upgrade?** Run the install script again, or let Aven upgrade itself from its own canvas ([Aven on Aven](docs/selfhost.md)). Database migrations run on start.
+**How do I upgrade?** Run the install script again, or let Aven upgrade itself from its own canvas ([Aven on Aven](docs/selfhost.md)). The script keeps `/etc/aven/aven.env`, copies `aven.db` (and `keel.db` if that file is still the database) to `/var/lib/aven/backups/` after stopping the server, and keeps the old binary as `/usr/local/bin/aven-server.prev`. Migrations run on start. Copy `master.key` somewhere else yourself: the script does not. Putting the previous binary back without the database copy can fail if a migration already ran. The steps are in the [self-hosting guide](docs/self-hosting.md#8-backups-and-upgrades).
**Can I move from Railway or Dokploy?** Aven reads `railway.json` / `railway.toml` and answers Railway-shaped GraphQL queries, and `aven import dokploy` recreates a Dokploy installation's services, variables, domains and volumes. Data is not copied: move database contents with your usual dump and restore tools.
diff --git a/docs/self-hosting.ko.md b/docs/self-hosting.ko.md
index cfc1f0c..1671159 100644
--- a/docs/self-hosting.ko.md
+++ b/docs/self-hosting.ko.md
@@ -10,34 +10,44 @@ Aven은 Docker 옆에서 서버 프로세스 하나(`aven-server`)로 동작합
## 1. 준비물
-- 공인 IPv4 또는 IPv6 주소가 있는 Linux 서버(amd64 또는 arm64). vCPU 2개, RAM 4 GB면 넉넉하게 시작할 수 있습니다.
-- systemd 서비스로 도는 최신 Docker Engine
-- 비어 있는 80, 443 포트(Aven proxy가 사용)
-- 관리할 수 있는 도메인. 예를 들어 `example.com`이라면:
- - `aven.example.com`: 대시보드와 API
- - `*.example.com`(또는 `*.apps.example.com`): 서비스에 자동으로 붙는 주소
-- 소스에서 빌드하려면 Go 1.26 이상, Node.js 20.9 이상과 npm, `make`
-- 서버에 [Railpack](https://github.com/railwayapp/railpack/releases): Dockerfile 없이 소스 코드로 빌드할 때 필요합니다.
+설치 스크립트가 볼 수 있는 것만 확인합니다. 메모리, 디스크, Docker 버전 번호는 재지 않습니다.
+
+- Linux, amd64(`x86_64`) 또는 arm64(`aarch64`), systemd(`systemctl`과 `/run/systemd/system`). `curl`, `tar`, 그리고 `sha256sum` 또는 `openssl`.
+- Docker: `docker` 명령이 있고, root로 `docker info`가 성공해야 합니다. `deploy/aven-server.service`에 `User=`가 없어서 `aven-server`는 root로 돕니다. 프록시 이미지는 `caddy:2-alpine`입니다. Docker가 없으면 설치 스크립트가 get.docker.com을 실행할 수 있습니다(`--install-docker`, 또는 물어볼 때 확인).
+- 메모리와 디스크는 검사하지 않습니다. `aven-server`는 프로세스 하나, 프록시는 컨테이너 하나이고, 앱과 데이터베이스는 컨테이너가 더 생깁니다. 데이터베이스 서비스는 Docker 볼륨을 씁니다. 제어 평면(`aven.db`, `master.key`, `session.key`, `logs/`)은 `AVEN_DATA`에 있으며 새 설치의 기본값은 `/var/lib/aven`입니다. 올릴 이미지와 볼륨을 담을 디스크를 두세요.
+- 호스트의 80/tcp, 443/tcp, 443/udp가 비어 있어야 합니다. 프록시가 셋을 모두 publish합니다. 443/udp는 HTTP/3입니다. API는 `AVEN_LISTEN`을 바꾸지 않으면 `127.0.0.1:7070`을 듣고, 그 포트는 publish하지 않습니다.
+- 자동 TLS를 쓰려면 공인 IPv4 또는 IPv6와, 직접 관리하는 도메인이 필요합니다. [DNS](#2-dns)를 보세요. 도메인이 없으면 대시보드는 그 머신의 `http://localhost:7070`에 둘 수 있습니다.
+- 소스에서 빌드하려면 Go 1.26 이상, Node.js 20.9 이상과 npm, `make`.
+- Dockerfile 없이 서비스를 빌드하려면 서버에 [Railpack](https://github.com/railwayapp/railpack/releases)이 있어야 합니다. 설치 스크립트는 v0.42.0을 `/usr/local/bin/railpack`에 둘 수 있습니다. `aven-server`는 `AVEN_RAILPACK_BIN`이 있으면 그것을 쓰고, 없으면 `PATH`의 `railpack` 또는 `/usr/local/bin/railpack`을 찾습니다. 서버가 Railpack을 받아 오지는 않습니다. `aven-agent`는 설정이 `auto`일 때(agent 기본값) 지정된 릴리스를 처음 쓸 때 받습니다.
## 2. DNS
-proxy가 첫 요청 때 인증서를 받을 수 있도록 미리 만들어 둡니다.
+인증서가 필요한 첫 요청 전에 레코드를 만드세요. 설치 스크립트는 인증서를 기다리지 않습니다.
| 이름 | 종류 | 값 |
| --- | --- | --- |
-| `aven.example.com` | A / AAAA | 서버의 공인 IPv4 / IPv6 |
-| `*.example.com` | A / AAAA | 서버의 공인 IPv4 / IPv6 |
+| `aven.example.com` | A 그리고/또는 AAAA | 서버의 공인 IPv4 그리고/또는 IPv6 |
+| `*.example.com` | A 그리고/또는 AAAA | 같은 주소 |
대시보드 주소는 아무 이름이나 써도 되며 `AVEN_PUBLIC_URL`과 같기만 하면 됩니다. 와일드카드는 `AVEN_BASE_DOMAIN`과 맞아야 합니다.
+와일드카드 기본 도메인의 TLS는 요청이 올 때 발급합니다. Aven은 `*.example.com` 자체를 덮는 인증서를 Let's Encrypt에 요청하지 않습니다. 그건 DNS-01 챌린지인데, 이 트리는 그 챌린지를 하지 않습니다. `web-production.example.com` 같은 구체적 호스트 이름은 첫 TLS 핸드셰이크 때 HTTP-01 또는 TLS-ALPN으로 인증서를 받습니다. 그때 인터넷에서 80과 443에 닿아야 합니다. AAAA는 이 호스트가 그 IPv6 주소로 응답할 때만 의미가 있습니다. 호스트가 받지 않는 주소를 가리키면 IPv6를 선호하는 클라이언트만 실패하고 IPv4는 됩니다.
+
+머신과 도메인 비용은 직접 냅니다. 이 저장소는 사용량 요금을 받지 않습니다.
+
## 스크립트로 설치
-DNS를 준비한 새 서버라면 명령 하나로 최신 릴리스를 설치할 수 있습니다.
+머신을 바꾸는 파일입니다. 실행하기 전에 읽으세요.
```sh
-curl -fsSL https://raw.githubusercontent.com/turin-dev/aven/main/install-server.sh | sh
+curl -fsSL https://raw.githubusercontent.com/turin-dev/aven/main/install-server.sh -o install-server.sh
+less install-server.sh
+sh install-server.sh --help
+sh install-server.sh
```
+스크립트가 디스크에 있으면 `sh install-server.sh --help`가 옵션 목록을 출력합니다. 파일 없이 `sh`로 파이프한 상태에서 `--help`를 주면, 셸이 본문을 보여 줄 파일이 없으므로 저장소 사본을 가리키는 한 줄만 출력합니다.
+
일반 사용자로 실행하면 스크립트가 `sudo`로 스스로를 다시 실행합니다. `sudo sh`로 파이프하면 sudo-rs(Ubuntu 25.10부터 기본 sudo)가 스크립트를 터미널의 백그라운드에 두어 질문을 할 수 없으므로, `--yes`가 없으면 안내를 출력하고 멈춥니다.
터미널에서 실행하면 첫 설치를 대화형으로 진행합니다. 대시보드 주소(비워 두면 `http://localhost:7070`으로 설치), 생성 서비스 이름에 쓸 기본 도메인, Let's Encrypt 이메일(선택), `admin@local` 비밀번호(Enter를 누르면 생성)를 묻고, Docker나 Railpack이 없으면 설치할지 묻습니다. 마지막에 요약을 보여 주고, 확인하기 전에는 아무것도 바꾸지 않습니다. 잘못된 답은 다시 묻고, 업그레이드는 한 번만 확인합니다. 무인 설치는 설정을 옵션으로 넘기고 `--yes`를 붙입니다.
@@ -47,7 +57,7 @@ curl -fsSL https://raw.githubusercontent.com/turin-dev/aven/main/install-server.
| sudo sh -s -- --yes --public-url https://aven.example.com --acme-email you@example.com
```
-스크립트는 시스템(root, systemd, amd64/arm64, Docker, 80/443 포트)을 확인하고, GitHub Releases에서 아키텍처에 맞는 릴리스를 내려받아 SHA-256 체크섬을 검증합니다. `aven-server`, `aven` CLI, agent와 CLI 다운로드 파일, Railpack을 설치합니다. 관리자 비밀번호와 웹훅 비밀값을 생성해 `/etc/aven/aven.env`를 만들고, systemd unit을 설치해 시작한 뒤, 서버가 응답할 때까지 기다렸다가 로그인 정보와 만들어야 할 DNS 이름을 출력합니다. 그다음 [첫 로그인](#7-첫-로그인과-온보딩)으로 넘어가세요.
+스크립트는 root, systemd, amd64/arm64, `curl`, `tar`, 체크섬 도구, Docker를 확인합니다. 80과 443 포트는 경고만 합니다. `ss`로 TCP 리스너를 보고, 그래도 설치는 계속합니다. GitHub Releases에서 아키텍처에 맞는 릴리스를 받아 `checksums.txt`의 SHA-256을 확인합니다. `aven-server`, `aven` CLI, agent와 CLI 다운로드 파일을 설치하고, 건너뛰지 않았다면 Railpack도 설치합니다. 관리자 비밀번호와 웹훅 비밀값을 만들어 `/etc/aven/aven.env`에 쓰고, systemd unit을 설치해 시작한 뒤 `http://127.0.0.1:7070/api/cli/downloads`가 응답할 때까지 기다립니다(약 60초). 그다음 로그인 정보와 DNS 이름을 출력합니다. [켜졌는지 확인](#check-that-it-started)과 [첫 로그인](#7-첫-로그인과-온보딩)으로 넘어가세요.
| 옵션 | 뜻 |
| --- | --- |
@@ -60,10 +70,59 @@ curl -fsSL https://raw.githubusercontent.com/turin-dev/aven/main/install-server.
| `--install-docker` | Docker가 없으면 get.docker.com으로 설치 |
| `--no-railpack`, `--no-start`, `--yes` | Railpack 건너뛰기, 서비스 시작 안 함, 묻지 않기 |
-같은 명령을 다시 실행하면 **업그레이드**합니다. `/etc/aven/aven.env`는 그대로 두고, DB를 `/var/lib/aven/backups/`에 복사하고, 이전 바이너리를 `/usr/local/bin/aven-server.prev`로 남긴 뒤 서버를 재시작합니다. 실행 중인 서비스는 계속 돌아갑니다.
+같은 명령을 다시 실행하면 **업그레이드**합니다. `/etc/aven/aven.env`는 그대로 두고, DB를 `/var/lib/aven/backups/`에 복사하고, 이전 바이너리를 `/usr/local/bin/aven-server.prev`로 남긴 뒤 서버를 재시작합니다. 실행 중인 서비스는 계속 돌아갑니다. 복사 범위는 아래 표와 같습니다. `master.key`는 복사하지 않습니다.
직접 설치하려면 3~6단계를 따르세요.
+
+
+### 설치 스크립트가 바꾸는 것
+
+메인 서버, 첫 설치:
+
+| 경로 | 내용 |
+| --- | --- |
+| `/usr/local/bin/aven-server` | 서버 바이너리 |
+| `/usr/local/bin/aven` | CLI |
+| `/usr/local/bin/railpack` | Railpack. 없었고 `--no-railpack`도 없을 때 |
+| `/usr/local/lib/aven/` | 서버가 나눠 주는 `aven-agent`와 CLI 다운로드 |
+| `/etc/systemd/system/aven-server.service` | systemd unit. `EnvironmentFile=/etc/aven/aven.env`. `KillMode=process`라서 서버를 멈춰도 컨테이너는 남습니다 |
+| `/etc/aven/aven.env` | 설정, 모드 0600. 첫 설치에만 만들고, 업그레이드는 유지합니다 |
+| `/etc/aven/aven.env.example` | 릴리스에 들어 있는 주석 달린 예시 |
+| `/var/lib/aven/` | 서비스가 시작할 때 생깁니다(`StateDirectory=aven`과 `AVEN_DATA`). `aven.db`, `master.key`, `session.key`, `logs/` |
+
+셸 스크립트가 아니라 첫 기동이 Docker 네트워크 `aven`과 컨테이너 `aven-proxy`(`caddy:2-alpine`, restart `unless-stopped`)를 만들고, 볼륨 `aven-caddy-data`, `aven-caddy-config`, `aven-proxy-wake`를 붙입니다.
+
+업그레이드(`/etc/aven/aven.env`가 이미 있거나, 예전 `/etc/keel/aven.env` 또는 `/etc/keel/keel.env`가 있는 경우)는 그 env 파일을 유지하고 `aven-server`를 멈춘 뒤, `aven.db`와 `keel.db`가 있으면 `/var/lib/aven/backups/<이름>-pre-upgrade-<시각>.db`로 복사합니다. 옆의 `-wal` / `-shm`도 복사합니다. 이전 바이너리는 `/usr/local/bin/aven-server.prev`로 둡니다. `master.key`는 복사하지 않습니다. unit의 `KillMode`가 `process`라서 앱 컨테이너는 계속 돕니다.
+
+리전 서버(`--role region`, 또는 `--server` / `--token`)는 `aven-server`를 설치하지 않습니다. 메인 서버의 `install-agent.sh`를 받아 실행하고, 그 스크립트가 다음을 씁니다.
+
+| 경로 | 내용 |
+| --- | --- |
+| `/usr/local/bin/aven-agent` | agent 바이너리 |
+| `/etc/keel-agent/agent.env` | 모드 0600. `AVEN_SERVER_URL`. `AVEN_JOIN_TOKEN`은 등록이 끝날 때까지만. 추가 설정도 `AVEN_*`로 저장합니다 |
+| `/var/lib/keel-agent/credentials.json` | 노드 키와 인증서, 모드 0600 |
+| `/etc/systemd/system/keel-agent.service` | unit. 이름은 아직 `keel-agent`입니다. 다시 실행하면 이미 쓰던 agent unit을 그 자리에서 바꿉니다 |
+
+agent 경로는 오늘 새 설치도 이렇게 만듭니다. 설정 이름은 `AVEN_*`입니다. 예전 env 파일의 `KEEL_*`도 읽습니다.
+
+`keel-server.service`가 켜져 있으면 스크립트는 멈추고, 같은 데이터 디렉터리로 둘을 같이 돌리지 말라고 합니다. `aven.self=server` 또는 `keel.self=server` 라벨의 컨테이너가 돌고 있으면, 대시보드에서 그 컨테이너를 올리라고 하고 멈춥니다.
+
+
+
+### 켜졌는지 확인
+
+```sh
+systemctl is-active aven-server
+curl -fsS -o /dev/null http://127.0.0.1:7070/api/cli/downloads
+docker ps --filter name=aven-proxy --format '{{.Names}} {{.Status}}'
+journalctl -u aven-server -n 50 --no-pager
+```
+
+`systemctl is-active`는 `active`를 출력합니다. `curl`은 설치 스크립트가 기다리는 요청과 같습니다. `docker ps`에는 `aven-proxy`가 `Up`으로 보여야 합니다. 그다음 로그인합니다.
+
+
+
## 3. 빌드
```sh
@@ -132,7 +191,7 @@ sudo systemctl enable --now aven-server
journalctl -u aven-server -f
```
-서버는 시작할 때 Docker를 확인하고 네트워크를 만든 뒤 `keel-proxy`를 띄웁니다. `AVEN_ADMIN_PASSWORD`가 없거나 Docker에 연결할 수 없으면 journal에 오류를 남기고 종료합니다.
+서버는 시작할 때 Docker를 확인하고 네트워크를 만든 뒤 `aven-proxy`를 띄웁니다. 이미 `keel-proxy`가 돌고 있으면 그 컨테이너를 유지합니다. 비밀번호가 없거나, Docker에 닿지 않거나, 80/443을 누군가 쓰고 있으면 journal에 오류를 남기고 종료합니다. 비밀번호 오류는 `AVEN_ADMIN_PASSWORD (or KEEL_ADMIN_PASSWORD) must be set to at least 8 characters`입니다. 프록시 오류에는 `start the proxy:`와 `ports AVEN_HTTP_PORT/AVEN_HTTPS_PORT, default 80/443, must be free`가 들어 있습니다. Docker 오류에는 `docker is not reachable`이 있고, 이 사용자가 소켓을 써도 되는지 묻습니다.
## 7. 첫 로그인과 온보딩
@@ -155,57 +214,125 @@ journalctl -u aven-server -f
환경 변수로 정하는 설정은 `sudo systemctl restart aven-server` 뒤에 반영됩니다. 서버를 재시작해도 실행 중인 서비스는 계속 돌아갑니다.
+
+
## 8. 백업과 업그레이드
-Aven이 아는 모든 것(사용자, 프로젝트, 변수, 암호화된 비밀값)은 `AVEN_DATA`에 있습니다.
+사용자, 프로젝트, 변수, 암호화된 비밀값처럼 Aven이 아는 것은 `AVEN_DATA`에 있습니다. 새 설치는 `/var/lib/aven`이고, 그 디렉터리가 아직 쓰이는 설치는 `/var/lib/keel`입니다.
-- `keel.db`: SQLite 데이터베이스
-- `master.key`: DB 안의 비밀값을 암호화하는 키. **이 파일이 없으면 DB 백업을 복호화할 수 없습니다.**
-- `session.key`, `logs/`, 로컬 백업
+- `aven.db`: 새 설치의 SQLite 데이터베이스입니다. `aven.db`가 옆에 없으면 `keel.db`라는 예전 파일을 엽니다. 살아있는 `keel.db` 옆에 빈 `aven.db`를 만들어도 예전 데이터가 옮겨지지는 않습니다.
+- `master.key`: 그 DB의 비밀값을 암호화하는 키입니다. **같은 키가 없으면 DB 백업을 복호화할 수 없습니다.**
+- `session.key`, `logs/`, `backups/` 아래 파일
-디렉터리 전체를 백업하세요. *어드민 → Owner · DB 백업*에서 일관된 DB 사본을 만들어 내려받을 수 있습니다. 직접 복사하려면 다음과 같이 합니다.
+디렉터리 전체를 복사하고, `master.key`를 DB와 함께 보관하세요. *어드민 → Owner · DB 백업*은 서버가 도는 동안 SQLite `VACUUM INTO`로 일관된 사본을 만들고, 그것을 받을 수 있습니다. **돌고 있는** DB를 직접 복사할 때는 `cp`가 아니라 SQLite 백업 API를 쓰세요.
```sh
-sudo sqlite3 /var/lib/aven/keel.db ".backup '/var/lib/aven/backups/keel-$(date +%F).db'"
-sudo cp /var/lib/aven/master.key /안전한/위치/
+sudo mkdir -p /var/lib/aven/backups
+sudo sqlite3 /var/lib/aven/aven.db ".backup '/var/lib/aven/backups/aven-$(date +%F).db'"
+sudo cp -p /var/lib/aven/master.key /안전한/위치/master.key
```
-업그레이드는 새 리비전을 빌드하고, 백업하고, 바이너리를 바꾼 뒤 재시작합니다.
+`ls /var/lib/aven/aven.db`가 실패하고 `keel.db`가 있으면 그 파일을 그 이름으로 백업하세요. `sqlite3`가 없으면 서버를 먼저 멈추세요. `sudo systemctl stop aven-server` 뒤에 `aven.db`(있으면 `aven.db-wal` / `aven.db-shm`도)와 `master.key`를 복사하고 다시 시작합니다.
+
+**제어 평면 복원.** `aven-server`를 멈춥니다. `aven.db`를 백업으로 바꿉니다. `aven.db-wal`과 `aven.db-shm`은 지우세요. 다른 파일의 로그를 SQLite가 다시 적용하지 않게 하려는 것입니다. 그 백업에 속한 `master.key`를 되돌려 놓고 `aven-server`를 시작합니다. 이 정지 동안 앱 컨테이너는 계속 돕니다(`KillMode=process`). 대시보드는 서버가 올라올 때까지 끊깁니다. 이렇게 하면 사용자, 프로젝트, 설정이 돌아옵니다. Postgres나 MySQL 볼륨 안의 바이트는 돌아오지 않습니다.
+
+**데이터베이스 서비스 백업.** 데이터베이스 서비스는 논리 덤프를 로컬 파일이나, *설정 → DB 백업*에 추가한 S3 호환 저장소로 보낼 수 있습니다. 복원은 같은 프로젝트의 같은 엔진 서비스에 쓰며, 그 서비스의 현재 데이터를 바꿉니다. 서버가 이것을 저절로 켜지는 않습니다.
+
+**시점 복구.** `aven pitr`(그리고 Postgres 또는 MySQL 서비스의 *설정*)은 Postgres WAL 또는 MySQL binlog를 같은 종류의 백업 저장소로 보냅니다. 저장소를 정하지 않으면 로컬입니다. Postgres를 켜면 그 데이터베이스가 한 번 재시작될 수 있습니다. `aven pitr restore --at <시각>`은 `--in-place`가 없으면 `<이름>-restored-...`라는 새 서비스로 복원합니다. `--in-place`는 그 서비스를 덮어씁니다. 시각은 RFC3339, 로컬 `YYYY-MM-DD HH:MM[:SS]`, 또는 지난 시간(`30m`, `2h`, `1d`, `1w`)입니다. 명령 설명은 [docs/cli.md](cli.md)에 있습니다. Redis와 MongoDB는 이 PITR 경로에 없습니다.
+
+**설치 스크립트로 업그레이드.** `install-server.sh`를 다시 실행합니다. env 파일을 유지하고 `aven-server`를 멈춘 뒤, [설치 스크립트가 바꾸는 것](#installer-changes)에 적힌 대로 DB 파일을 복사하고 바이너리를 바꾼 다음 서버를 시작합니다. 마이그레이션은 시작할 때 실행됩니다. 먼저 릴리스 노트를 읽으세요. 시작하기 전에 `master.key`는 직접 복사하세요. 앱 컨테이너는 계속 돕니다. 다른 머신의 agent는 서버 설치 스크립트가 올리지 않습니다. 서버가 올라온 뒤 메인 서버의 `install-agent.sh`로 agent 설치를 다시 실행하세요. 노드가 이미 등록됐으면 토큰은 필요 없습니다.
+
+**체크아웃에서 업그레이드.**
```sh
git pull && make all
-sudo cp /usr/local/bin/aven-server /usr/local/bin/aven-server.prev
+sudo cp -p /usr/local/bin/aven-server /usr/local/bin/aven-server.prev
+sudo systemctl stop aven-server
+sudo mkdir -p /var/lib/aven/backups
+sudo cp -p /var/lib/aven/aven.db "/var/lib/aven/backups/aven-pre-upgrade-$(date +%Y%m%d-%H%M%S).db"
sudo install -m 0755 bin/aven-server /usr/local/bin/aven-server
sudo install -m 0755 bin/aven-agent-linux-* bin/aven-linux-* bin/aven-darwin-* bin/aven-windows-*.exe /usr/local/lib/aven/
-sudo systemctl restart aven-server
+sudo systemctl start aven-server
```
-DB 마이그레이션은 시작할 때 자동으로 실행됩니다. 되돌리려면 서버를 멈추고 이전 바이너리와 DB 백업을 복원한 뒤 다시 시작하세요.
+쓰이는 파일이 `keel.db`이면 같은 방식으로 복사하세요. `-wal`과 `-shm`이 있으면 함께 복사하세요.
+
+**업그레이드 되돌리기.** `aven-server`를 멈춥니다. `/usr/local/bin/aven-server.prev`를 `/usr/local/bin/aven-server`로 되돌립니다. `pre-upgrade` 복사본에서 DB 파일을 복원하고, 그 복사본에 없던 `-wal` / `-shm`은 지웁니다. 서버를 시작합니다. 새 마이그레이션이 이미 DB를 고쳤다면 예전 바이너리가 그 DB를 거부할 수 있으므로, 바이너리와 DB를 함께 되돌리세요.
-직접 업그레이드하는 대신 Aven이 자기 자신을 배포하게 할 수도 있습니다. [Aven on Aven](selfhost.ko.md)을 보세요.
+`aven-server`가 [Aven on Aven](selfhost.ko.md)의 Docker 컨테이너이면(라벨 `aven.self=server`) 설치 스크립트는 진행을 거부합니다. `aven-server` 서비스를 배포해서 그 컨테이너를 올리세요. systemd unit과 그 컨테이너를 같은 데이터 디렉터리로 같이 돌리지 마세요.
## 9. 서버 추가
-다른 서버에서는 `aven-agent`가 HTTPS로 `aven-server`에 접속합니다. *어드민 → 서버 노드 → 노드 추가*에서 참여 토큰을 만든 뒤, Docker가 설치된 새 서버에서 실행합니다.
+다른 서버의 `aven-agent`는 HTTPS로 `aven-server`에 밖으로 접속합니다. agent는 대시보드 포트를 열지 않습니다. *어드민 → 서버 노드 → 노드 추가*에서 참여 토큰을 만든 뒤, Docker가 있는 새 서버에서 실행합니다.
```sh
curl -fsSL https://aven.example.com/install-agent.sh | sudo sh -s -- --server https://aven.example.com --token
```
-서버 설치 스크립트로도 됩니다. 새 서버에서 `install-server.sh`를 실행하고 첫 질문("What is this server?")에서 **Region server**를 고르면 메인 서버 URL, 참여 토큰, 사설 주소(선택)를 묻고, 원하면 Docker를 설치한 뒤 메인 서버의 `install-agent.sh`를 실행합니다. 무인 설치:
+셸에 파이프하기 전에 브라우저나 `curl`로 `https://aven.example.com/install-agent.sh`를 읽으세요.
+
+서버 설치 스크립트로도 됩니다. 새 서버에서 `install-server.sh`를 실행하고 "What is this server?"에서 **Region server**를 고르면 메인 서버 URL, 참여 토큰, 사설 주소(선택)를 묻고, 원하면 Docker를 설치한 뒤 메인 서버의 `install-agent.sh`를 실행합니다. 무인 설치:
```sh
curl -fsSL https://raw.githubusercontent.com/turin-dev/aven/main/install-server.sh | sudo sh -s -- --yes --server https://aven.example.com --token
```
-토큰은 한 번만 쓸 수 있고 만료됩니다. agent는 자기 키를 만들고 Aven 내부 CA에서 인증서를 받습니다. 나중에 같은 명령을 다시 실행하면 저장된 자격 증명을 유지한 채 agent를 업그레이드합니다.
+토큰은 한 번만 씁니다. 기본 수명은 1시간입니다. API는 `ttl_s`로 60에서 86400까지 받습니다. agent는 자기 키를 만들고 Aven 내부 CA에서 인증서를 받습니다. `credentials.json`이 생기면 `/etc/keel-agent/agent.env`에서 토큰 줄을 지웁니다. 설치 스크립트는 그 파일을 최대 60초 기다리고, `keel-agent`가 아직 활성이고 재시작하지 않았는지 확인합니다. 나중에 같은 명령을 다시 실행하면 바이너리를 올리고 자격 증명은 유지합니다. 다른 메인 서버에 붙이려면 `--reenroll`과 새 토큰으로 `install-agent.sh`를 실행하세요.
+
+토큰을 만들기 전에 메인 서버의 `AVEN_PUBLIC_URL`은 `https://`여야 합니다. 아니면 API가 503을 주고 `AVEN_PUBLIC_URL`을 이름에 넣습니다. agent는 등록 중 HTTP 리다이렉트를 따라가지 않으므로, URL은 `/api/agent/enroll`을 직접 제공하는 주소여야 합니다.
+
+서버들이 사설망(WireGuard, Tailscale)으로 이어져 있으면 각 서버의 그 주소를 `AVEN_PRIVATE_ADDRESS`로 두세요. 메인 서버는 `/etc/aven/aven.env`입니다. 리전 설치 스크립트는 주소를 받으면 `/etc/keel-agent/agent.env`에 `AVEN_PRIVATE_ADDRESS`를 씁니다. mesh 게이트웨이는 그 주소만 듣고, 포트는 7443입니다(`AVEN_MESH_PORT`).
+
+
+
+## 제거와 되돌리기
+
+제거 명령은 없습니다. 아래는 설치 스크립트가 만든 메인 서버를 걷어 내는 절차입니다. 앱 볼륨은 지우지 않습니다. 데이터를 지울 생각일 때만 따로 지우세요.
+
+```sh
+sudo systemctl disable --now aven-server
+sudo docker rm -f aven-proxy
+sudo rm -f /usr/local/bin/aven-server /usr/local/bin/aven-server.prev /usr/local/bin/aven /etc/systemd/system/aven-server.service
+sudo systemctl daemon-reload
+```
+
+`/etc/aven/aven.env`에는 관리자 비밀번호와 GitHub 웹훅 비밀값이 있습니다. `/var/lib/aven`에는 `aven.db`와 `master.key`가 있습니다. 이 두 디렉터리는, 버려도 되는 복사본을 갖고 있거나 제어 평면을 되돌릴 생각이 없을 때만 지우세요. 프록시 볼륨 `aven-caddy-data`, `aven-caddy-config`, `aven-proxy-wake`에는 인증서와 프록시 소켓이 있습니다. 그 셋을 `docker volume rm`하면 인증서가 사라집니다. 그 위의 컨테이너가 없으면 Docker 네트워크 `aven`은 `docker network rm aven`으로 지울 수 있습니다. Aven 라벨이 붙은 앱 컨테이너와 볼륨은 위 명령으로 지워지지 않습니다.
+
+리전 서버:
+
+```sh
+sudo systemctl disable --now keel-agent
+sudo rm -f /usr/local/bin/aven-agent /etc/systemd/system/keel-agent.service
+sudo rm -rf /etc/keel-agent /var/lib/keel-agent
+sudo systemctl daemon-reload
+```
+
+agent가 띄운 그 노드의 `aven-proxy`는 별도 컨테이너입니다. `docker rm -f aven-proxy`로 지웁니다. agent가 서비스용으로 띄운 컨테이너는 직접 지우기 전까지 남습니다.
+
+제거하지 않고 업그레이드만 되돌리는 방법은 [업그레이드 되돌리기](#8-backups-and-upgrades)입니다.
-서버들이 사설망(WireGuard, Tailscale)으로 연결되어 있다면 각 서버에 `AVEN_PRIVATE_ADDRESS`를 설정해 서로 다른 노드의 서비스끼리 그 망으로 통신하게 하세요.
+
## 문제 해결
-- **`AVEN_ADMIN_PASSWORD(8자 이상)가 필요합니다`**: `/etc/aven/aven.env`에 8자 이상의 비밀번호를 넣으세요.
-- **시작할 때 Docker 오류**: `systemctl status docker`를 확인하고 `aven-server`가 root로 실행되는지 확인하세요(unit 파일은 root로 실행합니다).
-- **인증서가 발급되지 않음**: DNS가 이 서버를 가리키고 인터넷에서 80/443에 접속할 수 있어야 합니다. ACME 오류는 `docker logs keel-proxy`에서 볼 수 있습니다.
-- **빌드가 `railpack을 찾을 수 없습니다`로 실패**: Railpack을 `/usr/local/bin/railpack`에 설치하거나 `AVEN_RAILPACK_BIN`을 설정하세요.
-- **LAN에서 일반 HTTP로 로그인**(평가용만): 세션 쿠키는 기본적으로 `Secure`이므로 `AVEN_INSECURE_COOKIE=1`을 설정하세요.
+- **80 또는 443 포트를 쓰는 중.** 설치 스크립트는 `Port 80 is in use` 또는 `Port 443 is in use`(또는 둘 다)를 출력하고 계속합니다. 그다음 `aven-server`가 종료됩니다. `journalctl -u aven-server`에 `start the proxy:`와 `ports AVEN_HTTP_PORT/AVEN_HTTPS_PORT, default 80/443, must be free`가 있습니다. `ss -Htlpn 'sport = :80'`과 443에 대한 같은 명령으로 리스너를 봅니다. 포트를 비우거나, 호스트의 다른 프록시가 80/443을 쓰면 `AVEN_HTTP_PORT`와 `AVEN_HTTPS_PORT`를 빈 포트로 두고 `AVEN_PROXY_BIND=127.0.0.1`로 하세요. 그 프록시가 TLS를 끝내면 `AVEN_TLS=off`로 두고 고른 포트로 전달하세요. `aven-server`를 재시작합니다.
+- **DNS가 퍼지지 않았거나 인증서가 안 나옴.** 설치는 인증서를 요청하지 않습니다. `getent`가 대시보드 호스트를 해석하지 못하면 설치 스크립트가 `<호스트> does not resolve yet`라고 경고합니다. DNS가 이 서버를 가리킨 뒤, 구체적 이름에 대한 첫 HTTPS 요청이 발급을 시작합니다. ACME 오류는 `docker logs aven-proxy`에 있습니다. `keel-proxy`가 실제로 돌 때만 `docker logs keel-proxy`를 보세요. 80과 443은 호스트 안에서만이 아니라 인터넷에서 닿아야 합니다. 이름이 다른 주소로 해석되거나, 이 호스트가 응답하지 않는 AAAA이면 그 주소를 쓰는 클라이언트의 챌린지가 실패합니다. `*.example.com` 자체에 대한 와일드카드 인증서는 없습니다.
+- **Docker 권한, 또는 데몬이 멈춤.** 설치 스크립트는 root로 `docker info`를 실행합니다. 권한 실패는 `cannot use its socket`이라고 하고 `docker info` 출력을 붙입니다. 데몬이 멈춘 경우는 `not running`이라고 하고 같은 출력을 붙입니다. `systemctl status docker`로 "inactive"와 소켓 오류를 구분합니다. unit은 `aven-server`를 root로 실행합니다. 다른 사용자로 바이너리를 직접 시작하면, 그 사용자가 Docker 소켓을 열 수 없을 때 `docker is not reachable`과 `may this user use its socket?`으로 실패합니다. unit 파일이 구성하는 방식은 rootless Docker가 아닙니다.
+- **방화벽 또는 IPv6.** Docker로 포트를 publish해도 ufw의 INPUT 체인을 타지 않습니다. env 예시도 TCP 프록시 포트에 대해 같은 말을 합니다. 호스트 방화벽이 보게 하려면 `DOCKER-USER` 체인에 규칙을 넣어야 합니다. 머신 앞의 클라우드 보안 그룹은 별개이고 80/443을 막을 수 있습니다. 기본 publish는 `80:80`, `443:443`, `443/udp`이고 Docker가 바인드하는 모든 인터페이스입니다. 호스트에 동작하는 공인 IPv6가 없으면 그 주소의 AAAA를 알리지 마세요. 도메인별 IPv6 주소(`AVEN_IPV6_POOL`)는 선택이고, 첫 인증서에 필요하지 않습니다.
+- **agent가 연결되지 않음.** 새 머신에서 `systemctl status keel-agent`와 `journalctl -u keel-agent`를 보세요. `/var/lib/keel-agent/credentials.json`이 생기지 않으면 설치 스크립트는 `the agent did not join within 60s`로 끝나고, 토큰이 잘못됐거나 만료됐을 수 있다고 합니다. agent 로그는 `enrollment HTTP 401`입니다. 그 상태의 API 본문은 `invalid join token`(토큰이 64자가 아님) 또는 `invalid or expired join token`입니다. agent는 그 본문이 아니라 상태 코드만 기록합니다. 토큰을 새로 만드세요. agent가 있는 머신에서 넘긴 `https://` URL의 메인 서버에 닿아야 합니다(`curl -fsSI https://aven.example.com/install-agent.sh`). 리다이렉트는 따라가지 않습니다. agent 머신의 Docker는 `docker info`에 응답해야 합니다. 등록 이후 연결이 끊기면 `disconnected from server; containers keep running`이라고 기록하고 agent가 다시 시도합니다. 그 WebSocket이 돌아올 때까지 노드는 오프라인으로 보입니다. 이 연결을 위해 agent 머신에 들어오는 포트는 필요 없습니다.
+- **`AVEN_ADMIN_PASSWORD (or KEEL_ADMIN_PASSWORD) must be set to at least 8 characters`:** `/etc/aven/aven.env`에 넣고 재시작하세요. 설치 스크립트는 8자보다 짧은 비밀번호를 거부하고, env 파일에서 값이 한 줄이기 때문에 탭, 줄바꿈, 따옴표, 백슬래시, `$`, 백틱, 공백도 거부합니다.
+- **빌드가 `railpack을 찾을 수 없습니다 (AVEN_RAILPACK_BIN (or KEEL_RAILPACK_BIN))`로 실패:** Railpack을 `/usr/local/bin/railpack`에 설치하거나 `AVEN_RAILPACK_BIN`을 설정하세요.
+- **LAN에서 일반 HTTP로 로그인**(평가용만): `AVEN_INSECURE_COOKIE=1`을 설정하세요. 세션 쿠키는 기본적으로 `Secure`입니다. 대시보드 URL이 `http://localhost`이면 설치 스크립트가 이 값을 직접 넣습니다.
+
+## 알려진 한계
+
+README의 [기능 안정성](../README.ko.md#기능-안정성) 표와 코드에 맞춘 내용입니다.
+
+- 제어 평면은 메인 서버의 SQLite 데이터베이스 하나입니다. 이 트리에는 두 번째 제어 평면 복제본이 없습니다.
+- 이 트리에는 git receive-pack 서버가 없습니다. push 배포는 GitHub 웹훅입니다.
+- 앱 sleep, cron 일정, 웹 대시보드, Cloudflare Tunnel 진입, AI 어시스턴트는 실험적입니다. 이유는 그 표에 있습니다. 대시보드는 설치하면 쓰는 UI입니다. `web/`에는 컴포넌트 테스트나 종단간 테스트가 없습니다.
+- 논리 백업과 PITR은 켜기 전에는 돌지 않습니다. 여기서 PITR은 Postgres WAL과 MySQL binlog이며, 모든 데이터베이스가 아닙니다.
+- 새 설치에서도 agent unit과 디렉터리 이름은 `keel-agent`입니다(`/etc/keel-agent`, `/var/lib/keel-agent`). env 파일 안의 설정 이름은 `AVEN_*`입니다.
+- 설치 스크립트는 메모리, 디스크, Docker 버전, DNS 이름이 이 머신을 가리키는지를 확인하지 않습니다. 이름이 아예 해석되지 않을 때만 경고합니다.
+- 새 설치는 설치 중에 인증서를 발급하지 않습니다. TLS는 호스트 이름마다 첫 핸드셰이크 때 일어나고, 80과 443이 닿을 때만 됩니다.
+- Aven 위의 `aven-server`(자기 업그레이드 컨테이너)는 특권 컨테이너이고 Docker 소켓을 마운트합니다. 그 경로는 [Aven on Aven](selfhost.ko.md)에 있고, 선택입니다.
diff --git a/docs/self-hosting.md b/docs/self-hosting.md
index 24d2bea..3d217fe 100644
--- a/docs/self-hosting.md
+++ b/docs/self-hosting.md
@@ -10,34 +10,44 @@ Aven runs as one server process (`aven-server`) next to Docker. It starts its ow
## 1. Requirements
-- A Linux server (amd64 or arm64) with a public IPv4 and/or IPv6 address. 2 vCPU and 4 GB RAM is a comfortable start.
-- A recent Docker Engine, running as a systemd service.
-- Ports 80 and 443 free on the host (Aven's proxy binds them).
-- A domain you control, for example `example.com`:
- - `aven.example.com` points at the server (dashboard and API).
- - `*.example.com` (or `*.apps.example.com`) points at the server, so services get automatic hostnames.
+The installer checks the items it can see. It does not measure memory, disk, or a Docker version number.
+
+- Linux, amd64 (`x86_64`) or arm64 (`aarch64`), with systemd (`systemctl` and `/run/systemd/system`). `curl`, `tar`, and `sha256sum` or `openssl`.
+- Docker: the `docker` command, and `docker info` succeeding for root. `aven-server` runs as root because `deploy/aven-server.service` sets no `User=`. The proxy image is `caddy:2-alpine`. If Docker is missing, the installer can run get.docker.com (`--install-docker`, or confirm when it asks).
+- Memory and disk are not checked. `aven-server` is one process, the proxy is one container, and each app or database is another container. Database services use a Docker volume. The control plane (`aven.db`, `master.key`, `session.key`, `logs/`) is under `AVEN_DATA`, `/var/lib/aven` on a new install. Size the disk for the images and volumes you will run.
+- Ports 80/tcp, 443/tcp, and 443/udp free on the host. The proxy publishes all three. 443/udp is HTTP/3. The API listens on `127.0.0.1:7070` unless `AVEN_LISTEN` says otherwise, and that port is not published.
+- A public IPv4 and/or IPv6 address, and a domain you control, when you want automatic TLS. See [DNS](#2-dns). Without a domain the dashboard can stay on `http://localhost:7070` of that machine.
- To build from source: Go 1.26+, Node.js 20.9+ with npm, `make`.
-- [Railpack](https://github.com/railwayapp/railpack/releases) on the server, to build services from source code without a Dockerfile.
+- [Railpack](https://github.com/railwayapp/railpack/releases) on the server, to build services that have no Dockerfile. The installer can place v0.42.0 at `/usr/local/bin/railpack`. `aven-server` uses `AVEN_RAILPACK_BIN` when it is set, otherwise `railpack` on `PATH` or `/usr/local/bin/railpack`. It does not download Railpack. `aven-agent` downloads the pinned release on first use when its setting is `auto`, which is the agent default.
## 2. DNS
-Create these records before you start, so the proxy can get certificates on first request:
+Create these records before the first request that needs a certificate. The installer does not wait for one.
| Name | Type | Value |
| --- | --- | --- |
-| `aven.example.com` | A / AAAA | the server's public IPv4 / IPv6 |
-| `*.example.com` | A / AAAA | the server's public IPv4 / IPv6 |
+| `aven.example.com` | A and/or AAAA | the server's public IPv4 and/or IPv6 |
+| `*.example.com` | A and/or AAAA | the same address |
+
+Use whatever name you like for the dashboard. It has to match `AVEN_PUBLIC_URL`. The wildcard has to match `AVEN_BASE_DOMAIN`.
+
+TLS for a wildcard base domain is on-demand. Aven does not ask Let's Encrypt for a certificate covering `*.example.com` itself (that would be a DNS-01 challenge, which this tree does not run). Each concrete hostname, such as `web-production.example.com`, gets its own certificate on the first TLS handshake, over HTTP-01 or TLS-ALPN. Ports 80 and 443 must be reachable from the public internet for that. An AAAA record is useful only when this host answers on that IPv6 address. A record that points at an address the host does not serve makes clients that prefer IPv6 fail while IPv4 still works.
-Use whatever name you like for the dashboard; it only has to match `AVEN_PUBLIC_URL`. The wildcard has to match `AVEN_BASE_DOMAIN`.
+You pay for the machine and for the domain. This repository does not charge a usage fee.
## Install with the script
-On a fresh server with DNS in place, one command installs the latest release:
+Read the script before you run it. It is the file that changes the machine.
```sh
-curl -fsSL https://raw.githubusercontent.com/turin-dev/aven/main/install-server.sh | sh
+curl -fsSL https://raw.githubusercontent.com/turin-dev/aven/main/install-server.sh -o install-server.sh
+less install-server.sh
+sh install-server.sh --help
+sh install-server.sh
```
+`sh install-server.sh --help` prints the option list when the script is on disk. Piped into `sh` with no file, `--help` prints a one-line pointer at the copy in the repository, because the script body is not a file the shell can show.
+
Run it as a normal user: the script runs itself again with `sudo`. Piped into `sudo sh` instead, sudo-rs (the default sudo since Ubuntu 25.10) keeps it in the background of the terminal, where it cannot ask questions, so it stops unless `--yes` is given.
On a terminal the script walks you through the first install: it asks for the dashboard URL (Enter without one installs on `http://localhost:7070`), the base domain for generated service names, an optional Let's Encrypt email and the `admin@local` password (Enter generates one), offers to install Docker and Railpack when they are missing, then shows a summary and changes nothing until you confirm. Invalid answers are asked again. Upgrades ask once. For unattended installs pass the settings as options and add `--yes`:
@@ -47,7 +57,52 @@ curl -fsSL https://raw.githubusercontent.com/turin-dev/aven/main/install-server.
| sudo sh -s -- --yes --public-url https://aven.example.com --acme-email you@example.com
```
-The script checks the system (root, systemd, amd64/arm64, Docker, ports 80/443), downloads the release for your architecture from GitHub Releases and verifies its SHA-256 checksum. It installs `aven-server`, the `aven` CLI, the agent and CLI downloads, and Railpack. It writes `/etc/aven/aven.env` with a generated admin password and webhook secret, installs and starts the systemd unit, waits until the server answers, and prints the sign-in details and the DNS names to create. Then continue with [First sign-in](#7-first-sign-in-and-onboarding).
+The script checks root, systemd, amd64/arm64, `curl`, `tar`, a checksum tool, and Docker. Ports 80 and 443 are only a warning: `ss` looks for a TCP listener, and the script continues. It downloads the release for your architecture from GitHub Releases and checks the SHA-256 in `checksums.txt`. It installs `aven-server`, the `aven` CLI, the agent and CLI downloads, and Railpack unless you skip it. It writes `/etc/aven/aven.env` with a generated admin password and webhook secret, installs and starts the systemd unit, and waits until `http://127.0.0.1:7070/api/cli/downloads` answers (up to about 60 seconds). Then it prints the sign-in details and the DNS names. Continue with [Check that it started](#check-that-it-started) and [First sign-in](#7-first-sign-in-and-onboarding).
+
+### What the installer changes
+
+Main server, first install:
+
+| Path | What it is |
+| --- | --- |
+| `/usr/local/bin/aven-server` | the server binary |
+| `/usr/local/bin/aven` | the CLI |
+| `/usr/local/bin/railpack` | Railpack, when it was missing and you did not pass `--no-railpack` |
+| `/usr/local/lib/aven/` | `aven-agent` and CLI downloads the server hands out |
+| `/etc/systemd/system/aven-server.service` | systemd unit. `EnvironmentFile=/etc/aven/aven.env`, `KillMode=process` so stopping the server leaves containers running |
+| `/etc/aven/aven.env` | settings, mode 0600. Created only on a first install; an upgrade keeps it |
+| `/etc/aven/aven.env.example` | the commented example from the release |
+| `/var/lib/aven/` | created when the service starts (`StateDirectory=aven` and `AVEN_DATA`). Holds `aven.db`, `master.key`, `session.key`, `logs/` |
+
+The first start, not the shell script itself, also creates the Docker network `aven` and the container `aven-proxy` (`caddy:2-alpine`, restart `unless-stopped`) with volumes `aven-caddy-data`, `aven-caddy-config`, and `aven-proxy-wake`.
+
+An upgrade (`/etc/aven/aven.env` already exists, or an older `/etc/keel/aven.env` / `/etc/keel/keel.env`) keeps that env file, stops `aven-server`, copies `aven.db` and `keel.db` when those files exist to `/var/lib/aven/backups/-pre-upgrade-.db` (and the `-wal` / `-shm` files beside them), and copies the old binary to `/usr/local/bin/aven-server.prev`. It does not copy `master.key`. Running app containers stay up because the unit's `KillMode` is `process`.
+
+A region server (`--role region`, or `--server` / `--token`) does not install `aven-server`. It downloads `install-agent.sh` from the main server and that script writes:
+
+| Path | What it is |
+| --- | --- |
+| `/usr/local/bin/aven-agent` | the agent binary |
+| `/etc/keel-agent/agent.env` | mode 0600. `AVEN_SERVER_URL`, and `AVEN_JOIN_TOKEN` only until enrollment finishes. Extra settings are stored as `AVEN_*` |
+| `/var/lib/keel-agent/credentials.json` | the node key and certificate, mode 0600 |
+| `/etc/systemd/system/keel-agent.service` | the unit. The name is still `keel-agent` so a re-run replaces the unit an existing agent is using |
+
+Those agent paths are what a new install creates today. `AVEN_*` is the name to set. `KEEL_*` in an older env file is still read.
+
+If `keel-server.service` is active, the script stops and tells you not to run both against one data directory. If a container labeled `aven.self=server` or `keel.self=server` is running, the script stops and tells you to upgrade that container from the dashboard instead.
+
+
+
+### Check that it started
+
+```sh
+systemctl is-active aven-server
+curl -fsS -o /dev/null http://127.0.0.1:7070/api/cli/downloads
+docker ps --filter name=aven-proxy --format '{{.Names}} {{.Status}}'
+journalctl -u aven-server -n 50 --no-pager
+```
+
+`systemctl is-active` prints `active`. The `curl` is the same request the installer waits for. `docker ps` should show `aven-proxy` as `Up`. Then sign in.
| Option | Meaning |
| --- | --- |
@@ -132,24 +187,24 @@ sudo systemctl enable --now aven-server
journalctl -u aven-server -f
```
-On start the server checks Docker, creates its network and starts `keel-proxy`. Errors such as a missing `AVEN_ADMIN_PASSWORD` or an unreachable Docker daemon are printed to the journal and the service exits.
+On start the server checks Docker, creates its network, and starts `aven-proxy`. An install that already has a running `keel-proxy` keeps that container. A missing password, an unreachable Docker daemon, or ports 80/443 already taken are printed to the journal and the service exits. The password error is `AVEN_ADMIN_PASSWORD (or KEEL_ADMIN_PASSWORD) must be set to at least 8 characters`. The proxy error includes `start the proxy:` and `ports AVEN_HTTP_PORT/AVEN_HTTPS_PORT, default 80/443, must be free`. The Docker error includes `docker is not reachable` and asks whether this user may use the socket.
## 7. First sign-in and onboarding
1. Open `https://aven.example.com` and sign in with **email** `admin@local` (or leave it empty) and the `AVEN_ADMIN_PASSWORD`.
-2. Aven shows the **first-run setup** prompt to server owners until the required items are done. You can open it any time at **Admin → Owner · 초기 설정** (`/admin/?section=setup`). Each item is checked automatically from the server's settings:
+2. Aven shows the **first-run setup** prompt to server owners until the required items are done. You can open it any time at **Admin → Owner · Setup** (`/admin/?section=setup`). The Korean UI calls that item **어드민 → Owner · 초기 설정**. Each item is checked automatically from the server's settings:
| Item | Required | How to finish it |
| --- | --- | --- |
| Public URL | yes | `AVEN_PUBLIC_URL` is an `https://` URL |
| Base domain | yes | `AVEN_BASE_DOMAIN` is set and its wildcard DNS points here |
- | Personal owner account | yes | Invite yourself under *Admin → Users*, accept, then make the account an owner under *Admin → Owner · 권한* |
+ | Personal owner account | yes | Invite yourself under *Admin → Users* (Korean: *어드민 → 사용자·초대*), accept, then make the account an owner under *Admin → Owner · Permissions* (Korean: *Owner · 권한*) |
| Two-factor authentication | yes | Add a TOTP app or a passkey under *Account → Security* |
| First project | yes | Create a project and deploy a service |
| GitHub | no | OAuth App and webhook secret (see above) |
| Email (SMTP) | no | `AVEN_SMTP_*` |
- | Backup storage | no | Add an S3-compatible destination under a database service's *Settings → DB backups* |
- | Additional nodes | no | See [Adding servers](#9-adding-servers) |
+ | Backup storage | no | Add an S3-compatible destination under a database service's *Settings → DB backups* (Korean: *설정 → DB 백업*) |
+ | Additional nodes | no | See [Adding servers](#9-adding-servers). The nav item is *Admin → Server nodes* (Korean: *서버 노드*) |
3. After you have a personal owner account, use it day to day and keep `admin@local` as a break-glass account with a long password.
@@ -157,55 +212,121 @@ Settings that come from environment variables take effect after `sudo systemctl
## 8. Backups and upgrades
-Everything Aven knows (users, projects, variables, encrypted secrets) lives in `AVEN_DATA`:
+Everything Aven knows about users, projects, variables, and encrypted secrets lives in `AVEN_DATA` (`/var/lib/aven` on a new install, `/var/lib/keel` when that directory is the one still in use):
-- `keel.db`: SQLite database
-- `master.key`: key that encrypts secrets in the database. **Without it, a database backup cannot be decrypted.**
-- `session.key`, `logs/`, local backups
+- `aven.db`: the SQLite database on a new install. An older file named `keel.db` is opened instead when `aven.db` is not beside it. Do not create a second empty `aven.db` next to a live `keel.db` and expect the old data to move.
+- `master.key`: the key that encrypts secrets in that database. **Without the same key, a database backup cannot be decrypted.**
+- `session.key`, `logs/`, and files under `backups/`
-Back up the whole directory. *Admin → Owner · DB 백업* creates a consistent copy of the database you can download. For a manual copy:
+Copy the whole directory, and keep `master.key` with the database. *Admin → Owner · DB backups* (Korean: *Owner · DB 백업*) writes a consistent copy with SQLite `VACUUM INTO` while the server is running, and you can download it. A manual copy of a **running** database should use the SQLite backup API, not `cp`:
```sh
-sudo sqlite3 /var/lib/aven/keel.db ".backup '/var/lib/aven/backups/keel-$(date +%F).db'"
-sudo cp /var/lib/aven/master.key /secure/place/
+sudo mkdir -p /var/lib/aven/backups
+sudo sqlite3 /var/lib/aven/aven.db ".backup '/var/lib/aven/backups/aven-$(date +%F).db'"
+sudo cp -p /var/lib/aven/master.key /secure/place/master.key
```
-To upgrade, build the new revision, back up, replace the binaries and restart:
+If `ls /var/lib/aven/aven.db` fails and `keel.db` is there, back that file up under its own name. Stop the server first if `sqlite3` is not installed: `sudo systemctl stop aven-server`, copy `aven.db` (and `aven.db-wal` / `aven.db-shm` if they exist), copy `master.key`, then start it again.
+
+**Restore the control plane.** Stop `aven-server`. Replace `aven.db` with the backup. Remove `aven.db-wal` and `aven.db-shm` so SQLite does not replay a log from a different file. Put back the `master.key` that belongs to that backup. Start `aven-server`. App containers keep running across this stop (`KillMode=process`), but the dashboard is down until the server is up. This restores users, projects, and settings. It does not restore the bytes inside a Postgres or MySQL volume.
+
+**Database service backups.** A database service can take logical dumps to a local file or to an S3-compatible destination you add under *Settings → DB backups*. Restore writes into a database service of the same engine in the same project and replaces that service's current data. Nothing in the server turns this on by itself.
+
+**Point-in-time recovery.** `aven pitr` (and *Settings* on a Postgres or MySQL service) archives Postgres WAL or MySQL binlogs to the same kind of backup storage, or locally when no destination is set. Enabling Postgres may restart that database once. `aven pitr restore --at