Add Komodo installation script and docs

Add Komodo (Docker + MongoDB) installation script for Alpine VM
Update README with Komodo requirements and installation command
This commit is contained in:
Damien
2026-06-14 14:09:38 +02:00
parent 1caf3737fd
commit 7eee3b85b1
3 changed files with 560 additions and 2 deletions

View File

@@ -24,9 +24,10 @@ These scripts automate the deployment of personal infrastructure components. The
| [`gitea-runner/install.sh`](gitea-runner/) | Gitea Act Runner on Alpine LXC (Proxmox) | `bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/gitea-runner/install.sh)"` |
| [`openbao/install.sh`](openbao/) | OpenBao secrets manager on Alpine LXC (Proxmox) | `bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/openbao/install.sh)"` |
| [`ferretdb/install.sh`](ferretdb/) | FerretDB (MongoDB-compatible) on Debian LXC (Proxmox) | `bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/ferretdb/install.sh)"` |
| [`komodo/install.sh`](komodo/) | Komodo (Docker + MongoDB) on Alpine VM | `bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/komodo/install.sh)"` |
### Requirements
- Fresh Debian 12/13 installation (proxy, netlab) or Proxmox VE host (gitea-runner, openbao, ferretdb)
- User with sudo privileges (do not run as root) — except gitea-runner, openbao and ferretdb which run as root on Proxmox
- Fresh Debian 12/13 installation (proxy, netlab) or Proxmox VE host (gitea-runner, openbao, ferretdb) or Alpine VM (komodo)
- User with sudo privileges (do not run as root) — except gitea-runner, openbao, ferretdb, and komodo which run as root
- Internet access

94
komodo/README.md Normal file
View File

@@ -0,0 +1,94 @@
# Komodo
Automated installation script for [Komodo](https://komo.do) — Docker + a MongoDB-backed Core + Periphery stack — running inside an **Alpine Linux VM**.
Follows the upstream [MongoDB quick start](https://komo.do/docs/setup/mongo): MongoDB stores all
resource configuration, audit logs, users, and system state. The Core exposes the API/UI on
loopback only; [Tailscale](https://tailscale.com) (running on the VM) acts as the reverse
proxy and terminates TLS via tailnet certificates.
### Why a VM, not an LXC?
Komodo Periphery binds `/var/run/docker.sock` and `/proc` from the host into a container so it
can manage other containers and report system stats. In an unprivileged LXC this clashes with
the cgroup / docker-in-docker constraints Proxmox imposes. A regular VM keeps it boring — the
script can just run `apk add docker` and let dockerd own the kernel namespace.
### Features
| Context | Action |
| ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| Fresh Alpine VM (no `/opt/komodo/compose.env`) | Enables the `community` repo, installs Docker + Tailscale, generates secrets, writes compose files, pulls + ups |
| Re-run on an existing install (`/opt/komodo/compose.env` already there) | Preserves the generated secrets, re-pulls the latest images, and recreates the stack |
### Requirements
- Alpine Linux **VM** (`/etc/alpine-release` must exist), not an LXC
- Internet access from the VM
- Run as **root** (enforced)
### Usage
#### Install / update
```bash
bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/komodo/install.sh)"
```
The script prints the generated `KOMODO_DATABASE_PASSWORD`, `KOMODO_WEBHOOK_SECRET`, and
`KOMODO_JWT_SECRET` at the end. They are also written to `/opt/komodo/compose.env`
(`chmod 600`, root-only).
The initial admin user is `admin` / `changeme` — change it from the UI immediately after first
login.
#### Customisation
Every parameter is exposed as an environment variable:
| Variable | Default | Description |
| -------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `KOMODO_DIR` | `/opt/komodo` | Where the compose files and env file live |
| `KOMODO_HOSTNAME` | `komodo` | Hostname used when running `tailscale up` |
| `KOMODO_LISTEN_ADDR` | `127.0.0.1:9120` | Host-side bind for Core's HTTP listener. Loopback by default — Tailscale fronts it. |
| `KOMODO_HOST` | `https://komodo.taila5ad8.ts.net` | Written verbatim into `compose.env`; used by Komodo for OAuth / webhook URL suggestions. Set this to whatever your tailnet (or external reverse proxy) is. |
| `TS_AUTHKEY` | _(unset)_ | Pre-auth key (generate at <https://login.tailscale.com/admin/settings/keys>). If unset, finish `tailscale up` manually inside the VM. |
All other Komodo / Periphery variables are written with the upstream defaults — edit
`/opt/komodo/compose.env` if you need to tune them, then:
```bash
cd /opt/komodo
docker compose --env-file compose.env -f mongo.compose.yaml up -d
```
#### Tailscale reverse proxy
Komodo Core binds to `127.0.0.1:9120` only — Tailscale (running on the VM, **not** in a
container) is the reverse proxy and terminates TLS via tailnet certificates.
If `TS_AUTHKEY` was supplied at install time, the script runs `tailscale up` and
`tailscale serve --bg --https=443 http://127.0.0.1:9120` automatically. Komodo then becomes
reachable at `https://<hostname>.<tailnet>.ts.net` (matching `KOMODO_HOST`).
Otherwise, finish setup manually inside the VM:
```bash
tailscale up --ssh --hostname komodo
tailscale serve --bg --https=443 http://127.0.0.1:9120
tailscale status # confirms the tailnet FQDN
```
> HTTPS in `tailscale serve` requires HTTPS to be enabled on your tailnet
> (Admin console → DNS → HTTPS Certificates).
### Architecture
- **OS**: Alpine VM with the `community` apk repo enabled (required for `docker`)
- **Engine**: `docker` + `docker-cli-compose` (the `docker compose` v2 plugin), OpenRC service `docker` enabled at boot
- **Stack**: `mongo`, `ghcr.io/moghtech/komodo-core:2`, `ghcr.io/moghtech/komodo-periphery:2` — see [upstream `mongo.compose.yaml`](https://komo.do/docs/setup/mongo)
- **Compose files**: `/opt/komodo/mongo.compose.yaml` + `/opt/komodo/compose.env` (`chmod 600`)
- **Network**: Core's `ports:` mapping is rewritten to `${KOMODO_LISTEN_ADDR}:9120` so only the loopback (and Tailscale serve) can reach it; Periphery has no host port
- **Reverse proxy**: Tailscale runs natively on the VM (not in a container) and publishes the loopback listener via `tailscale serve --https=443`
- **Secrets**: `KOMODO_DATABASE_PASSWORD`, `KOMODO_WEBHOOK_SECRET`, `KOMODO_JWT_SECRET` are auto-generated (`openssl rand -hex …`) on first install and preserved across re-runs
- **Logs**: Docker `json-file` driver capped at `max-size=10m`, `max-file=3` per container (`/etc/docker/daemon.json`)

463
komodo/install.sh Executable file
View File

@@ -0,0 +1,463 @@
#!/bin/bash
# install.sh - Komodo (Docker + MongoDB) on an Alpine VM
# Usage (from inside the Alpine VM, as root):
# bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/komodo/install.sh)"
#
# Unlike the openbao/ferretdb scripts, this is targeted at an Alpine *VM*, not
# an LXC: there is no Proxmox-host orchestration mode. The script just installs
# Docker, pulls Komodo (Core + Periphery + MongoDB) via docker compose, and
# fronts it on the tailnet via `tailscale serve --https=443`.
#
# Re-running the script is safe: existing /opt/komodo/compose.env secrets are
# preserved, and `docker compose up -d --pull always` upgrades the images.
set -euo pipefail
# --- Config (override via environment) ---
KOMODO_DIR="${KOMODO_DIR:-/opt/komodo}"
KOMODO_HOSTNAME="${KOMODO_HOSTNAME:-komodo}"
# Komodo Core publishes its HTTP listener on this address — kept on loopback
# only; the tailnet reaches it through `tailscale serve`.
KOMODO_LISTEN_ADDR="${KOMODO_LISTEN_ADDR:-127.0.0.1:9120}"
# `KOMODO_HOST` ends up in the compose.env verbatim. Default is the tailnet
# FQDN you asked for; override to point Komodo at a different reverse proxy.
KOMODO_HOST="${KOMODO_HOST:-https://komodo.taila5ad8.ts.net}"
# Optional: pre-authorise the VM's Tailscale non-interactively.
# Generate at https://login.tailscale.com/admin/settings/keys
TS_AUTHKEY="${TS_AUTHKEY:-}"
# --- Colors ---
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Logs go to stderr so callers can safely use $(fn) without capturing log noise.
log_info() { echo -e "${GREEN}[INFO]${NC} $1" >&2; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1" >&2; }
log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
require_root() {
if [[ "$(id -u)" -ne 0 ]]; then
log_error "This script must be run as root (current uid: $(id -u))."
exit 1
fi
}
require_alpine() {
if [[ ! -f /etc/alpine-release ]]; then
log_error "This script targets Alpine Linux (no /etc/alpine-release found)."
exit 1
fi
}
# Hex secret of the requested byte length.
gen_secret() {
local bytes="${1:-32}"
if command -v openssl >/dev/null 2>&1; then
openssl rand -hex "$bytes"
else
head -c "$bytes" /dev/urandom | od -An -tx1 | tr -d ' \n'
fi
}
# ============================================================
# Docker — Alpine install per https://wiki.alpinelinux.org/wiki/Docker
# ============================================================
install_docker() {
log_info "Enabling the Alpine 'community' repository..."
# The community repo holds the docker package. The default repositories file
# carries a commented `community` line; uncomment it (idempotent).
sed -i -E 's|^#(.*/community)$|\1|' /etc/apk/repositories
log_info "Updating package index..."
apk update >/dev/null
log_info "Installing Docker + Compose plugin..."
# docker-cli-compose provides `docker compose` (v2 plugin), which is what the
# Komodo upstream docs assume. openrc is normally already on the VM but
# listing it explicitly keeps this script self-contained.
apk add --no-cache docker docker-cli-compose openrc >/dev/null
log_info "Enabling dockerd at boot..."
rc-update add docker default >/dev/null
if ! rc-service docker status >/dev/null 2>&1; then
log_info "Starting dockerd..."
rc-service docker start >/dev/null
else
log_info "dockerd already running."
fi
# Sanity check — fail fast if the daemon never came up.
local tries=0
until docker info >/dev/null 2>&1; do
tries=$((tries + 1))
if (( tries > 15 )); then
log_error "dockerd did not become ready after 15s — check /var/log/docker.log"
exit 1
fi
sleep 1
done
}
# ============================================================
# Tailscale — install and front Komodo with `tailscale serve`
# ============================================================
install_tailscale() {
if command -v tailscale >/dev/null 2>&1; then
log_info "Tailscale already installed."
return 0
fi
log_info "Installing Tailscale from the Alpine community repo..."
apk add --no-cache tailscale >/dev/null
rc-update add tailscale default >/dev/null 2>&1 || true
rc-service tailscale start >/dev/null 2>&1 \
|| log_warn "tailscaled failed to start — check /var/log/tailscaled.log"
}
configure_tailscale_proxy() {
if ! command -v tailscale >/dev/null 2>&1; then
log_warn "tailscale CLI not found, skipping reverse-proxy setup."
return 0
fi
local backend_state
backend_state=$(tailscale status --json 2>/dev/null | jq -r '.BackendState // "unknown"')
if [[ "$backend_state" != "Running" ]]; then
if [[ -n "$TS_AUTHKEY" ]]; then
log_info "Bringing Tailscale up with provided auth key..."
tailscale up --authkey "$TS_AUTHKEY" --ssh --hostname "$KOMODO_HOSTNAME" \
|| log_warn "tailscale up failed — run it manually inside the VM."
else
log_warn "Tailscale not authenticated and TS_AUTHKEY was not supplied."
log_warn "Finish setup with: tailscale up --ssh --hostname ${KOMODO_HOSTNAME}"
log_warn "Then publish Komodo with: tailscale serve --bg --https=443 http://${KOMODO_LISTEN_ADDR}"
return 0
fi
fi
if tailscale serve status 2>/dev/null | grep -q "${KOMODO_LISTEN_ADDR}"; then
log_info "Tailscale serve already publishes http://${KOMODO_LISTEN_ADDR}."
else
log_info "Publishing Komodo on the tailnet via 'tailscale serve' (HTTPS:443)..."
tailscale serve --bg --https=443 "http://${KOMODO_LISTEN_ADDR}" \
|| log_warn "tailscale serve failed — enable HTTPS on your tailnet and retry."
fi
}
# ============================================================
# Komodo — compose files in $KOMODO_DIR
# ============================================================
# Pulls the existing value of $1 out of compose.env (if any). Used so re-runs
# preserve the password and secrets generated the first time around.
read_env_var() {
local key="$1"
local env_file="${KOMODO_DIR}/compose.env"
[[ -f "$env_file" ]] || return 0
awk -F= -v k="$key" '$1 == k { sub(/^[^=]*=/, ""); gsub(/^"|"$/, ""); print; exit }' "$env_file"
}
write_compose_files() {
log_info "Provisioning ${KOMODO_DIR}..."
mkdir -p "$KOMODO_DIR"
chmod 750 "$KOMODO_DIR"
# Reuse existing secrets if compose.env was written by a previous run.
local db_password webhook_secret jwt_secret
db_password="$(read_env_var KOMODO_DATABASE_PASSWORD || true)"
webhook_secret="$(read_env_var KOMODO_WEBHOOK_SECRET || true)"
jwt_secret="$(read_env_var KOMODO_JWT_SECRET || true)"
if [[ -z "$db_password" ]]; then
db_password="$(gen_secret 24)"
log_info "Generated KOMODO_DATABASE_PASSWORD."
else
log_info "Preserving existing KOMODO_DATABASE_PASSWORD from compose.env."
fi
if [[ -z "$webhook_secret" ]]; then
webhook_secret="$(gen_secret 32)"
log_info "Generated KOMODO_WEBHOOK_SECRET."
else
log_info "Preserving existing KOMODO_WEBHOOK_SECRET from compose.env."
fi
if [[ -z "$jwt_secret" ]]; then
jwt_secret="$(gen_secret 32)"
log_info "Generated KOMODO_JWT_SECRET."
else
log_info "Preserving existing KOMODO_JWT_SECRET from compose.env."
fi
# Stash the resolved values so the install summary can print them.
KOMODO_DB_PASSWORD_RESOLVED="$db_password"
KOMODO_WEBHOOK_SECRET_RESOLVED="$webhook_secret"
KOMODO_JWT_SECRET_RESOLVED="$jwt_secret"
# mongo.compose.yaml is upstream's reference (https://komo.do/docs/setup/mongo)
# with one change: Core's host port is bound to ${KOMODO_LISTEN_ADDR} instead
# of every interface, so only the loopback (and Tailscale serve) can reach it.
cat > "${KOMODO_DIR}/mongo.compose.yaml" <<EOF
services:
mongo:
image: mongo
labels:
komodo.skip:
command: --quiet --wiredTigerCacheSizeGB 0.25
restart: unless-stopped
volumes:
- mongo-data:/data/db
- mongo-config:/data/configdb
environment:
MONGO_INITDB_ROOT_USERNAME: \${KOMODO_DATABASE_USERNAME}
MONGO_INITDB_ROOT_PASSWORD: \${KOMODO_DATABASE_PASSWORD}
core:
image: ghcr.io/moghtech/komodo-core:\${COMPOSE_KOMODO_IMAGE_TAG:-2}
init: true
restart: unless-stopped
depends_on:
- mongo
ports:
- ${KOMODO_LISTEN_ADDR}:9120
env_file: ./compose.env
environment:
KOMODO_DATABASE_ADDRESS: mongo:27017
volumes:
- keys:/config/keys
- \${COMPOSE_KOMODO_BACKUPS_PATH}:/backups
periphery:
image: ghcr.io/moghtech/komodo-periphery:\${COMPOSE_KOMODO_IMAGE_TAG:-2}
init: true
restart: unless-stopped
depends_on:
- core
env_file: ./compose.env
volumes:
- keys:/config/keys
- /var/run/docker.sock:/var/run/docker.sock
- /proc:/proc
- \${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}:\${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}
volumes:
mongo-data:
mongo-config:
keys:
EOF
# compose.env is upstream's reference file with the three secrets injected
# and KOMODO_HOST set to the tailnet FQDN. Every other variable is left at
# the upstream default — adjust by hand later if needed.
cat > "${KOMODO_DIR}/compose.env" <<EOF
####################################
# Generated by infra-scripts/komodo #
####################################
COMPOSE_KOMODO_IMAGE_TAG="2"
COMPOSE_KOMODO_BACKUPS_PATH=/etc/komodo/backups
## DB credentials — password generated at install time, do not commit anywhere
KOMODO_DATABASE_USERNAME=admin
KOMODO_DATABASE_PASSWORD=${db_password}
TZ=Etc/UTC
#=-------------------------=#
#= Komodo Core Environment =#
#=-------------------------=#
## Used for Oauth / Webhook url suggestion — served via 'tailscale serve'.
KOMODO_HOST=${KOMODO_HOST}
KOMODO_TITLE=Komodo
KOMODO_PERIPHERY_PUBLIC_KEY=file:/config/keys/periphery.pub
KOMODO_LOCAL_AUTH=true
KOMODO_INIT_ADMIN_USERNAME=admin
KOMODO_INIT_ADMIN_PASSWORD=changeme
KOMODO_FIRST_SERVER_NAME=Local
KOMODO_DISABLE_CONFIRM_DIALOG=false
KOMODO_DISABLE_INIT_RESOURCES=false
## Secrets — generated at install time
KOMODO_WEBHOOK_SECRET=${webhook_secret}
KOMODO_JWT_SECRET=${jwt_secret}
KOMODO_JWT_TTL="1-day"
KOMODO_MONITORING_INTERVAL="15-sec"
KOMODO_RESOURCE_POLL_INTERVAL="1-hr"
KOMODO_DISABLE_USER_REGISTRATION=false
KOMODO_ENABLE_NEW_USERS=false
KOMODO_DISABLE_NON_ADMIN_CREATE=false
KOMODO_TRANSPARENT_MODE=false
KOMODO_OIDC_ENABLED=false
KOMODO_GITHUB_OAUTH_ENABLED=false
KOMODO_GOOGLE_OAUTH_ENABLED=false
KOMODO_AWS_ACCESS_KEY_ID=
KOMODO_AWS_SECRET_ACCESS_KEY=
KOMODO_LOGGING_PRETTY=false
KOMODO_PRETTY_STARTUP_CONFIG=false
#=------------------------------=#
#= Komodo Periphery Environment =#
#=------------------------------=#
PERIPHERY_CORE_ADDRESS=ws://core:9120
PERIPHERY_CONNECT_AS=\${KOMODO_FIRST_SERVER_NAME}
PERIPHERY_CORE_PUBLIC_KEYS=file:/config/keys/core.pub
PERIPHERY_ROOT_DIRECTORY=/etc/komodo
PERIPHERY_DISABLE_TERMINALS=false
PERIPHERY_DISABLE_CONTAINER_TERMINALS=false
PERIPHERY_INCLUDE_DISK_MOUNTS=/etc/hostname
PERIPHERY_LOGGING_PRETTY=false
PERIPHERY_PRETTY_STARTUP_CONFIG=false
EOF
# Compose env contains the DB password + JWT/webhook secrets — keep readable
# only by root.
chmod 600 "${KOMODO_DIR}/compose.env"
}
bring_up_stack() {
log_info "Pulling Komodo + MongoDB images..."
(cd "$KOMODO_DIR" && docker compose --env-file ./compose.env -f mongo.compose.yaml pull) >/dev/null
log_info "Starting the Komodo stack..."
(cd "$KOMODO_DIR" && docker compose --env-file ./compose.env -f mongo.compose.yaml up -d) >/dev/null
}
# ============================================================
# Log hygiene + console auto-login (repo conventions)
# ============================================================
configure_log_hygiene() {
# Bound the journal-equivalent — Alpine uses OpenRC, so we cap Docker's
# per-container logs via daemon.json (json-file driver, default).
log_info "Capping per-container Docker log size..."
mkdir -p /etc/docker
cat > /etc/docker/daemon.json <<'EOF'
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
EOF
# Reload so the cap applies to subsequent container starts. Existing
# containers keep their previous log driver until recreated.
rc-service docker reload >/dev/null 2>&1 \
|| rc-service docker restart >/dev/null 2>&1 || true
}
configure_console_autologin() {
log_info "Enabling console auto-login on tty1..."
# Alpine's busybox getty does not support --autologin; use util-linux's agetty.
apk add --no-cache agetty >/dev/null 2>&1 || apk add --no-cache util-linux >/dev/null
sed -i '/^tty1::/d' /etc/inittab
echo 'tty1::respawn:/sbin/agetty --autologin root --noclear 38400 tty1' >> /etc/inittab
kill -HUP 1 2>/dev/null || true
pkill -KILL -f '(getty|agetty).*tty1' 2>/dev/null || true
}
write_motd() {
log_info "Configuring MOTD..."
# Quoted heredoc: every variable in the body is resolved at login time, not
# install time.
cat > /etc/profile.d/00-komodo.sh <<'MOTD'
KOMODO_DIR="${KOMODO_DIR:-/opt/komodo}"
KOMODO_HOST=$(awk -F= '$1 == "KOMODO_HOST" { print $2; exit }' "${KOMODO_DIR}/compose.env" 2>/dev/null)
[ -z "$KOMODO_HOST" ] && KOMODO_HOST="https://<not-set>"
TS_FQDN=$(tailscale status --json 2>/dev/null | awk -F'"' '
/"Self"/ { in_self=1 }
in_self && /"DNSName"/ { gsub(/\.$/, "", $4); print $4; exit }
')
[ -z "$TS_FQDN" ] && TS_FQDN="$(hostname).ts.net"
CORE_STATE=$(docker inspect -f '{{.State.Status}}' komodo-core-1 2>/dev/null || echo "missing")
PERI_STATE=$(docker inspect -f '{{.State.Status}}' komodo-periphery-1 2>/dev/null || echo "missing")
MONGO_STATE=$(docker inspect -f '{{.State.Status}}' komodo-mongo-1 2>/dev/null || echo "missing")
echo ""
echo " _ __ _ "
echo "| |/ /___ _ __ ___ __| | ___ "
echo "| ' // _ \\| '_ \` _ \\ / _\` |/ _ \\ "
echo "| . \\ (_) | | | | | | (_| | (_) |"
echo "|_|\\_\\___/|_| |_| |_|\\__,_|\\___/ "
echo ""
echo "Komodo (Docker + MongoDB)"
echo "─────────────────────────────────────────"
echo "Access:"
echo " • Local : http://127.0.0.1:9120"
echo " • Tailnet : ${KOMODO_HOST}"
echo ""
echo "Containers:"
echo " • core : ${CORE_STATE}"
echo " • periphery : ${PERI_STATE}"
echo " • mongo : ${MONGO_STATE}"
echo ""
echo "Useful commands:"
echo " cd ${KOMODO_DIR}"
echo " docker compose --env-file compose.env -f mongo.compose.yaml ps"
echo " docker compose --env-file compose.env -f mongo.compose.yaml logs -f core"
echo " docker compose --env-file compose.env -f mongo.compose.yaml pull && \\"
echo " docker compose --env-file compose.env -f mongo.compose.yaml up -d"
echo "─────────────────────────────────────────"
echo ""
MOTD
chmod +x /etc/profile.d/00-komodo.sh
}
# ============================================================
# Main
# ============================================================
main() {
require_root
require_alpine
log_info "=== Komodo — installation ==="
# Base tooling for the rest of the script (jq is used to read tailscale status,
# curl/ca-certificates for image pulls / future tailscale install).
apk add --no-cache bash curl jq ca-certificates openssl >/dev/null
install_docker
install_tailscale
write_compose_files
bring_up_stack
configure_log_hygiene
configure_console_autologin
configure_tailscale_proxy
write_motd
echo ""
log_info "========================================="
log_info "Komodo installation complete!"
log_info "========================================="
echo ""
echo " Local URL : http://${KOMODO_LISTEN_ADDR}"
echo " Tailnet URL : ${KOMODO_HOST}"
echo " Compose dir : ${KOMODO_DIR}"
echo ""
echo "Initial admin (change immediately via the UI):"
echo " username : admin"
echo " password : changeme"
echo ""
echo "Generated secrets (also in ${KOMODO_DIR}/compose.env, root-only):"
echo " KOMODO_DATABASE_PASSWORD : ${KOMODO_DB_PASSWORD_RESOLVED}"
echo " KOMODO_WEBHOOK_SECRET : ${KOMODO_WEBHOOK_SECRET_RESOLVED}"
echo " KOMODO_JWT_SECRET : ${KOMODO_JWT_SECRET_RESOLVED}"
echo ""
}
main "$@"