Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,26 @@ sudo INSTALL_REF=<release-tag> bash install.sh
# Pinned commit
sudo INSTALL_REF=<40-char-commit-sha> bash install.sh
```

Non-interactive automation (CI/provisioning-safe):
```bash
sudo INSTALL_NONINTERACTIVE=true \
INSTALL_MODE=fresh \
INSTALL_USE_DOMAIN=false \
INSTALL_REF=<release-tag> \
bash install.sh
```

Optional non-interactive domain mode:
```bash
sudo INSTALL_NONINTERACTIVE=true \
INSTALL_MODE=fresh \
INSTALL_USE_DOMAIN=true \
INSTALL_DOMAIN_NAME=chat.example.com \
INSTALL_SSL_EMAIL=admin@example.com \
INSTALL_REF=<release-tag> \
bash install.sh
```
Unsafe/dev-only (mutable branch head):
```bash
curl -fsSLo install.sh https://raw.githubusercontent.com/ehsanking/ElaheMessenger/main/install.sh
Expand All @@ -167,13 +187,24 @@ Installer safety behavior:
- Aborts upgrades when git sync fails or the worktree is dirty (no implicit `rm -rf` fallback).
- Uses Caddy on `:80/:443`; in IP-only mode the generated `APP_URL` uses `http://<server-ip>` (no internal `:3000` mismatch).
- Never prints bootstrap admin password in terminal output; auto-generated credentials are written once to a local secrets file with restrictive permissions.
- Non-interactive installs are first-class: no hidden interactive dependency; install choices are deterministic and env-driven.
- Verifies post-launch health in explicit phases: container health, local reverse-proxy routing, and external DNS/TLS readiness guidance.
- Fails install when local reverse-proxy routing does not work, and only warns for external DNS/TLS propagation uncertainty.
- Source trust defaults to a pinned tag when available; mutable branch-head installs are opt-in and explicitly warned during installer prompts.
- Fresh/reinstall writes bootstrap admin password to a one-time file (`./runtime/admin-bootstrap-password`) and passes it via `ADMIN_BOOTSTRAP_PASSWORD_FILE`.
- `ADMIN_USERNAME`/`ADMIN_PASSWORD` are create-only by default; if `ADMIN_BOOTSTRAP_RESET_EXISTING=true` is used, reset is consumed once per credential set (not repeated on every restart).
- Does **not** auto-enable UFW; firewall changes remain operator-driven.

### Installer troubleshooting

- **Installer hangs in piped mode**: run with `INSTALL_NONINTERACTIVE=true` (and optionally `INSTALL_MODE`, `INSTALL_USE_DOMAIN`, `INSTALL_DOMAIN_NAME`).
- **Ports 80/443 are already used**: stop conflicting services; non-interactive installs fail fast on conflicts by design.
- **Domain install fails local probe**: verify `INSTALL_DOMAIN_NAME`/domain prompt value is correct and resolves publicly; installer now validates host-routed proxy behavior locally with `--resolve`.
- **Docker Compose missing on Debian/Ubuntu**: installer attempts distro compose plugin packages (`docker-compose-plugin` / `docker-compose-v2`) and exits with actionable guidance if unavailable.
- **Need strict reproducibility**: pin `INSTALL_REF` to a release tag or commit, not `main`.

Detailed runbook: `docs/installer-verification-checklist.md`.

---

## Manual Installation
Expand Down Expand Up @@ -530,4 +561,3 @@ If this project helps you, you can support its maintenance:

- **USDT (TRC20 / Tether):** `TKPswLQqd2e73UTGJ5prxVXBVo7MTsWedU`
- **TRON (TRX):** `TKPswLQqd2e73UTGJ5prxVXBVo7MTsWedU`

50 changes: 50 additions & 0 deletions docs/installer-root-cause-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Installer Root-Cause Report (March 30, 2026)

## What failed

1. **Piped/non-interactive installs could hang indefinitely** during fresh install.
2. **Domain-mode post-install health probe could fail even when services were healthy**.
3. **Non-interactive runs with existing deployments could block on prompts or continue ambiguously**.

## Why it failed

### 1) Non-interactive admin bootstrap prompt loop
- Fresh install always prompts for admin password mode.
- Default mode selected `provide password`.
- In piped/non-interactive execution there is no usable TTY for secret input.
- Password prompt returned empty values repeatedly, causing a never-ending loop.

### 2) Domain-mode health check used an IP URL that bypassed domain routing assumptions
- Installer validated reverse proxy health using `http://127.0.0.1/api/health/live`.
- In domain-mode Caddy config, host-based routing/redirect behavior may not treat `127.0.0.1` as the configured site host.
- Result: false negative install failures despite app/db being healthy.

### 3) Prompt-driven decisions were not deterministic without TTY
- Existing-install mode selection and proxy behavior relied on interactive prompts.
- In non-interactive contexts this could lead to unintended defaults or blocked flows without explicit operator intent.

## What was changed

1. Added explicit **non-interactive mode detection** (`INSTALL_NONINTERACTIVE=true` or no TTY) and deterministic behavior.
2. Added **non-interactive install controls**:
- `INSTALL_MODE=fresh|upgrade|reinstall`
- `INSTALL_USE_DOMAIN=true|false`
- `INSTALL_DOMAIN_NAME`
- `INSTALL_SSL_EMAIL`
3. Implemented **safe non-interactive admin bootstrap**:
- Uses `ADMIN_USERNAME` / `ADMIN_PASSWORD` if provided and valid.
- Otherwise auto-generates strong credentials and enforces first-login password change.
- Fails fast with actionable errors when invalid env values are supplied.
4. Made **port conflict handling deterministic**:
- non-interactive runs now fail explicitly instead of continuing with uncertain behavior.
5. Fixed **domain-mode reverse proxy verification**:
- probes Caddy using host-resolved request (`--resolve <domain>:80:127.0.0.1`) and accepts expected redirect/success status codes.
6. Added regression assertions in installer tests to ensure these protections remain in place.

## Why the fix is correct

- Eliminates TTY-dependent logic from required bootstrap path in non-interactive mode.
- Makes installer decisions explicit, reproducible, and scriptable for automation.
- Preserves current security posture (strong secret requirements, no secret printing, strict validation).
- Keeps idempotent behavior for upgrades/reinstalls by preserving existing env/config unless explicit regeneration is selected.
- Avoids false negatives in domain deployments by validating routing with the configured host semantics.
64 changes: 64 additions & 0 deletions docs/installer-verification-checklist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Installer Verification Checklist

## 1) Install command

### One-line pipe mode
```bash
curl -fsSL https://raw.githubusercontent.com/ehsanking/ElaheMessenger/main/install.sh | ( [ "$(id -u)" -eq 0 ] && bash || sudo bash )
```

### Local-file mode
```bash
curl -fsSLo install.sh https://raw.githubusercontent.com/ehsanking/ElaheMessenger/main/install.sh
sudo bash install.sh
```

### Non-interactive reproducible mode
```bash
sudo INSTALL_NONINTERACTIVE=true INSTALL_MODE=fresh INSTALL_USE_DOMAIN=false INSTALL_REF=<tag-or-commit> bash install.sh
```

## 2) Expected container state

```bash
cd ElaheMessenger
docker compose ps
```

Expected:
- `db` => `healthy`
- `app` => `healthy`
- `caddy` => `running`

## 3) Expected health endpoints

```bash
curl -fsS http://127.0.0.1/api/health/live
curl -fsS http://127.0.0.1/api/health/ready
```

Expected:
- Both return JSON with HTTP 200 in IP-only mode.

Domain mode host-routed probe:
```bash
curl -sS --resolve <domain>:80:127.0.0.1 -o /dev/null -w '%{http_code}\n' http://<domain>/api/health/live
```

Expected:
- `200` or redirect status (`301/302/307/308`) depending on HTTP->HTTPS policy.

## 4) Expected URL behavior

- IP-only install: `APP_URL=http://<server-ip>` and local health should pass via `http://127.0.0.1/api/health/live`.
- Domain install: Caddy routes requests for configured host; installer verifies host-routed local probe and warns if external DNS/TLS propagation is still pending.

## 5) Admin bootstrap validation

```bash
cd ElaheMessenger
docker compose exec -T db sh -lc 'PGPASSWORD="$POSTGRES_PASSWORD" psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -tAc "SELECT COUNT(*) FROM \"User\" WHERE role = '\''ADMIN'\'';"'
```

Expected:
- Exactly one bootstrap admin on first install.
Loading
Loading