commit cd56e73e367795b8c28543839cb0149b18b3a242 Author: bzo Date: Sat Sep 5 15:38:46 2026 +0300 init diff --git a/docker-opensuse-ct.sh b/docker-opensuse-ct.sh new file mode 100755 index 0000000..7210b8b --- /dev/null +++ b/docker-opensuse-ct.sh @@ -0,0 +1,258 @@ +#!/usr/bin/env bash +# +# docker-opensuse-ct.sh — interactive (whiptail) installer that provisions an +# openSUSE LXC container on Proxmox and installs Docker + Docker Compose inside it. +# +# Same UX pattern as grafana-opensuse-ct.sh: Default/Advanced menu, whiptail +# prompts, then a scripted pct create + pct exec install flow. +# +# Run this ON the Proxmox host as root: +# ./docker-opensuse-ct.sh + +set -euo pipefail + +APP="Docker" +COMPOSE_FALLBACK_VERSION="v2.29.7" + +# ---------- helpers ---------- +msg_info() { echo -e "\e[1;34m[*]\e[0m $1"; } +msg_ok() { echo -e "\e[1;32m[OK]\e[0m $1"; } +msg_err() { echo -e "\e[1;31m[ERR]\e[0m $1" >&2; } + +die() { msg_err "$1"; exit 1; } + +command -v pct >/dev/null 2>&1 || die "pct not found — run this on a Proxmox VE host." +command -v whiptail >/dev/null 2>&1 || { msg_info "whiptail not found, installing..."; apt-get update -qq && apt-get install -y whiptail; } + +# ---------- welcome + mode ---------- +whiptail --title "$APP LXC Installer" --msgbox \ + "This will create an openSUSE LXC container on this Proxmox host and install Docker + Docker Compose inside it." \ + 10 65 + +MODE=$(whiptail --title "$APP LXC Installer" --menu "Choose setup mode" 12 65 2 \ + "default" "Quick install — sane defaults, minimal prompts" \ + "advanced" "Advanced — customize every setting" \ + 3>&1 1>&2 2>&3) || exit 1 + +# ---------- gather settings ---------- +NEXTID=$(pvesh get /cluster/nextid) + +if [[ "$MODE" == "default" ]]; then + CTID="$NEXTID" + CT_HOSTNAME="docker" + CT_STORAGE=$(pvesm status -content rootdir | awk 'NR==2{print $1}') + [[ -n "$CT_STORAGE" ]] || die "No rootdir-capable storage found." + DISK_GB=16 + CORES=2 + MEMORY=2048 + SWAP=512 + BRIDGE="vmbr0" + VLAN="" + NET_MODE="dhcp" + UNPRIVILEGED=1 + ROOT_PASSWORD=$(tr -dc 'A-Za-z0-9' &1 1>&2 2>&3) || exit 1 + CT_HOSTNAME=$(whiptail --inputbox "Hostname" 8 58 "docker" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 + + mapfile -t STORAGES < <(pvesm status -content rootdir | awk 'NR>1{print $1}') + [[ ${#STORAGES[@]} -gt 0 ]] || die "No rootdir-capable storage found." + RADIO_ARGS=(); first=1 + for s in "${STORAGES[@]}"; do + if [[ $first -eq 1 ]]; then RADIO_ARGS+=("$s" "" ON); first=0; else RADIO_ARGS+=("$s" "" OFF); fi + done + CT_STORAGE=$(whiptail --radiolist "Storage for rootfs" 15 60 6 "${RADIO_ARGS[@]}" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 + + DISK_GB=$(whiptail --inputbox "Disk size (GB) — Docker images/volumes add up, size generously" 8 70 "16" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 + CORES=$(whiptail --inputbox "CPU cores" 8 58 "2" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 + MEMORY=$(whiptail --inputbox "Memory (MB)" 8 58 "2048" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 + SWAP=$(whiptail --inputbox "Swap (MB)" 8 58 "512" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 + + mapfile -t BRIDGES < <(ip -o link show type bridge | awk -F': ' '{print $2}') + [[ ${#BRIDGES[@]} -gt 0 ]] || die "No bridges found on this host." + RADIO_ARGS=(); first=1 + for b in "${BRIDGES[@]}"; do + if [[ $first -eq 1 ]]; then RADIO_ARGS+=("$b" "" ON); first=0; else RADIO_ARGS+=("$b" "" OFF); fi + done + BRIDGE=$(whiptail --radiolist "Network bridge" 15 60 6 "${RADIO_ARGS[@]}" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 + + VLAN=$(whiptail --inputbox "VLAN tag (blank = untagged)" 8 58 "" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 + + NET_MODE=$(whiptail --menu "Network mode" 12 58 2 \ + "dhcp" "DHCP" \ + "static" "Static IP" \ + --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 + + if [[ "$NET_MODE" == "static" ]]; then + STATIC_IP_CIDR=$(whiptail --inputbox "IP/CIDR, e.g. 192.168.10.60/24" 8 58 "" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 + STATIC_GW=$(whiptail --inputbox "Gateway" 8 58 "" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 + fi + + if whiptail --title "$APP LXC" --yesno \ + "Create as unprivileged container?\n\nRecommended, but Docker needs the nesting+keyctl features either way (already enabled by this script). Choose 'No' only if you hit cgroup/overlay issues under unprivileged mode." 12 70; then + UNPRIVILEGED=1 + else + UNPRIVILEGED=0 + fi + + ROOT_PASSWORD=$(whiptail --passwordbox "Root password for the CT" 8 58 --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 + SSH_PUBKEY_FILE=$(whiptail --inputbox "SSH public key file to inject (blank = skip)" 8 58 "$HOME/.ssh/id_rsa.pub" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 +fi + +pct status "$CTID" >/dev/null 2>&1 && die "CT $CTID already exists." + +# ---------- template ---------- +mapfile -t TEMPLATES < <(pveam available -section system | awk '{print $2}' | grep -i opensuse || true) +if [[ ${#TEMPLATES[@]} -eq 0 ]]; then + msg_info "Refreshing template index..." + pveam update + mapfile -t TEMPLATES < <(pveam available -section system | awk '{print $2}' | grep -i opensuse || true) +fi +[[ ${#TEMPLATES[@]} -gt 0 ]] || die "No openSUSE templates found in the Proxmox appliance list." + +if [[ "$MODE" == "default" ]]; then + TEMPLATE="${TEMPLATES[-1]}" # newest listed +else + RADIO_ARGS=(); first=1 + for t in "${TEMPLATES[@]}"; do + if [[ $first -eq 1 ]]; then RADIO_ARGS+=("$t" "" ON); first=0; else RADIO_ARGS+=("$t" "" OFF); fi + done + TEMPLATE=$(whiptail --radiolist "Choose openSUSE template" 20 72 10 "${RADIO_ARGS[@]}" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 +fi + +if ! pveam list local | grep -q "$TEMPLATE"; then + msg_info "Downloading template $TEMPLATE..." + pveam download local "$TEMPLATE" + msg_ok "Template downloaded." +fi + +# ---------- confirm ---------- +SUMMARY="CTID: $CTID +Hostname: $CT_HOSTNAME +Template: $TEMPLATE +Storage: $CT_STORAGE (${DISK_GB}G) +CPU/RAM: ${CORES} cores / ${MEMORY}MB (+${SWAP}MB swap) +Bridge: $BRIDGE${VLAN:+ (vlan $VLAN)} +Network: ${NET_MODE}${STATIC_IP_CIDR:+ $STATIC_IP_CIDR via $STATIC_GW} +Priv: $([[ $UNPRIVILEGED -eq 1 ]] && echo unprivileged || echo privileged) +Features: nesting=1,keyctl=1 (required for Docker)" + +whiptail --title "Confirm" --yesno "$SUMMARY + +Proceed?" 20 65 || exit 1 + +# ---------- build net string ---------- +if [[ "$NET_MODE" == "static" ]]; then + NET_STR="name=eth0,bridge=${BRIDGE},ip=${STATIC_IP_CIDR},gw=${STATIC_GW}" +else + NET_STR="name=eth0,bridge=${BRIDGE},ip=dhcp" +fi +[[ -n "$VLAN" ]] && NET_STR="${NET_STR},tag=${VLAN}" + +# ---------- create + start ---------- +CREATE_ARGS=( + "$CTID" "local:vztmpl/${TEMPLATE}" + --hostname "$CT_HOSTNAME" + --cores "$CORES" + --memory "$MEMORY" + --swap "$SWAP" + --rootfs "${CT_STORAGE}:${DISK_GB}" + --net0 "$NET_STR" + --unprivileged "$UNPRIVILEGED" + --onboot 1 + --features "nesting=1,keyctl=1" + --password "$ROOT_PASSWORD" +) +[[ -n "${SSH_PUBKEY_FILE:-}" && -f "${SSH_PUBKEY_FILE:-}" ]] && CREATE_ARGS+=(--ssh-public-keys "$SSH_PUBKEY_FILE") + +msg_info "Creating CT $CTID ($CT_HOSTNAME)..." +pct create "${CREATE_ARGS[@]}" >/dev/null +msg_ok "Container created." + +msg_info "Starting CT $CTID..." +pct start "$CTID" + +msg_info "Waiting for network..." +for i in {1..30}; do + pct exec "$CTID" -- sh -c "true" >/dev/null 2>&1 && pct exec "$CTID" -- ping -c1 -W2 1.1.1.1 >/dev/null 2>&1 && break + sleep 2 + [[ $i -eq 30 ]] && die "CT never came up with network access." +done +msg_ok "Network is up." + +# ---------- install docker inside the CT ---------- +run() { pct exec "$CTID" -- bash -c "$1"; } + +msg_info "Refreshing zypper repos..." +run "zypper --non-interactive refresh" >/dev/null +msg_ok "Repos refreshed." + +msg_info "Installing prerequisites (curl)..." +run "zypper --non-interactive install curl" >/dev/null +msg_ok "Prerequisites installed." + +msg_info "Installing Docker from openSUSE repos..." +run "zypper --non-interactive install docker" >/dev/null +msg_ok "Docker installed." + +msg_info "Enabling and starting the Docker service..." +run "systemctl enable --now docker" >/dev/null +msg_ok "Docker service running." + +msg_info "Resolving latest Docker Compose release..." +COMPOSE_VERSION=$(pct exec "$CTID" -- curl -s https://api.github.com/repos/docker/compose/releases/latest \ + | grep -Po '"tag_name":\s*"\K[^"]+' || true) +[[ -z "$COMPOSE_VERSION" ]] && COMPOSE_VERSION="$COMPOSE_FALLBACK_VERSION" +msg_ok "Using Compose $COMPOSE_VERSION" + +msg_info "Installing Docker Compose plugin (${COMPOSE_VERSION})..." +run "mkdir -p /usr/local/lib/docker/cli-plugins" +run "curl -sSL https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-linux-x86_64 -o /usr/local/lib/docker/cli-plugins/docker-compose" +run "chmod +x /usr/local/lib/docker/cli-plugins/docker-compose" +msg_ok "Docker Compose plugin installed." + +msg_info "Verifying install (docker run hello-world)..." +if run "docker run --rm hello-world" >/dev/null 2>&1; then + msg_ok "Docker can run containers." +else + msg_err "hello-world test failed — likely a cgroup/overlay issue under unprivileged LXC. See summary notes below." +fi + +DOCKER_VERSION=$(run "docker --version" || echo "unknown") +COMPOSE_VERSION_INSTALLED=$(run "docker compose version" || echo "unknown") + +# ---------- summary ---------- +CT_IP=$(pct exec "$CTID" -- hostname -I 2>/dev/null | awk '{print $1}') + +whiptail --title "$APP LXC Installer — Done" --msgbox \ +"Docker is running. + +CT ID: $CTID +IP: ${CT_IP} +$DOCKER_VERSION +$COMPOSE_VERSION_INSTALLED +Root pass: ${ROOT_PASSWORD} + +Enter with: pct enter $CTID +(also printed below in the terminal)" 18 65 + +cat <&2; } + +die() { msg_err "$1"; exit 1; } + +command -v pct >/dev/null 2>&1 || die "pct not found — run this on a Proxmox VE host." +command -v whiptail >/dev/null 2>&1 || { msg_info "whiptail not found, installing..."; apt-get update -qq && apt-get install -y whiptail; } + +# ---------- welcome + mode ---------- +whiptail --title "$APP LXC Installer" --msgbox \ + "This will create an openSUSE LXC container on this Proxmox host and install Grafana inside it." \ + 10 65 + +MODE=$(whiptail --title "$APP LXC Installer" --menu "Choose setup mode" 12 65 2 \ + "default" "Quick install — sane defaults, minimal prompts" \ + "advanced" "Advanced — customize every setting" \ + 3>&1 1>&2 2>&3) || exit 1 + +# ---------- gather settings ---------- +NEXTID=$(pvesh get /cluster/nextid) + +if [[ "$MODE" == "default" ]]; then + CTID="$NEXTID" + CT_HOSTNAME="grafana" + CT_STORAGE=$(pvesm status -content rootdir | awk 'NR==2{print $1}') + [[ -n "$CT_STORAGE" ]] || die "No rootdir-capable storage found." + DISK_GB=8 + CORES=2 + MEMORY=1024 + SWAP=512 + BRIDGE="vmbr0" + VLAN="" + NET_MODE="dhcp" + UNPRIVILEGED=1 + GRAFANA_PORT=3000 + ROOT_PASSWORD=$(tr -dc 'A-Za-z0-9' &1 1>&2 2>&3) || exit 1 + CT_HOSTNAME=$(whiptail --inputbox "Hostname" 8 58 "grafana" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 + + mapfile -t STORAGES < <(pvesm status -content rootdir | awk 'NR>1{print $1}') + [[ ${#STORAGES[@]} -gt 0 ]] || die "No rootdir-capable storage found." + RADIO_ARGS=(); first=1 + for s in "${STORAGES[@]}"; do + if [[ $first -eq 1 ]]; then RADIO_ARGS+=("$s" "" ON); first=0; else RADIO_ARGS+=("$s" "" OFF); fi + done + CT_STORAGE=$(whiptail --radiolist "Storage for rootfs" 15 60 6 "${RADIO_ARGS[@]}" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 + + DISK_GB=$(whiptail --inputbox "Disk size (GB)" 8 58 "8" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 + CORES=$(whiptail --inputbox "CPU cores" 8 58 "2" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 + MEMORY=$(whiptail --inputbox "Memory (MB)" 8 58 "1024" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 + SWAP=$(whiptail --inputbox "Swap (MB)" 8 58 "512" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 + + mapfile -t BRIDGES < <(ip -o link show type bridge | awk -F': ' '{print $2}') + [[ ${#BRIDGES[@]} -gt 0 ]] || die "No bridges found on this host." + RADIO_ARGS=(); first=1 + for b in "${BRIDGES[@]}"; do + if [[ $first -eq 1 ]]; then RADIO_ARGS+=("$b" "" ON); first=0; else RADIO_ARGS+=("$b" "" OFF); fi + done + BRIDGE=$(whiptail --radiolist "Network bridge" 15 60 6 "${RADIO_ARGS[@]}" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 + + VLAN=$(whiptail --inputbox "VLAN tag (blank = untagged)" 8 58 "" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 + + NET_MODE=$(whiptail --menu "Network mode" 12 58 2 \ + "dhcp" "DHCP" \ + "static" "Static IP" \ + --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 + + if [[ "$NET_MODE" == "static" ]]; then + STATIC_IP_CIDR=$(whiptail --inputbox "IP/CIDR, e.g. 192.168.10.50/24" 8 58 "" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 + STATIC_GW=$(whiptail --inputbox "Gateway" 8 58 "" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 + fi + + if whiptail --title "$APP LXC" --yesno "Create as unprivileged container? (recommended)" 8 58; then + UNPRIVILEGED=1 + else + UNPRIVILEGED=0 + fi + + GRAFANA_PORT=$(whiptail --inputbox "Grafana HTTP port" 8 58 "3000" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 + ROOT_PASSWORD=$(whiptail --passwordbox "Root password for the CT" 8 58 --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 + GRAFANA_ADMIN_PASSWORD=$(whiptail --passwordbox "Grafana admin password" 8 58 --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 + SSH_PUBKEY_FILE=$(whiptail --inputbox "SSH public key file to inject (blank = skip)" 8 58 "$HOME/.ssh/id_rsa.pub" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 +fi + +pct status "$CTID" >/dev/null 2>&1 && die "CT $CTID already exists." + +# ---------- template ---------- +mapfile -t TEMPLATES < <(pveam available -section system | awk '{print $2}' | grep -i opensuse || true) +if [[ ${#TEMPLATES[@]} -eq 0 ]]; then + msg_info "Refreshing template index..." + pveam update + mapfile -t TEMPLATES < <(pveam available -section system | awk '{print $2}' | grep -i opensuse || true) +fi +[[ ${#TEMPLATES[@]} -gt 0 ]] || die "No openSUSE templates found in the Proxmox appliance list." + +if [[ "$MODE" == "default" ]]; then + TEMPLATE="${TEMPLATES[-1]}" # newest listed +else + RADIO_ARGS=(); first=1 + for t in "${TEMPLATES[@]}"; do + if [[ $first -eq 1 ]]; then RADIO_ARGS+=("$t" "" ON); first=0; else RADIO_ARGS+=("$t" "" OFF); fi + done + TEMPLATE=$(whiptail --radiolist "Choose openSUSE template" 20 72 10 "${RADIO_ARGS[@]}" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 +fi + +if ! pveam list local | grep -q "$TEMPLATE"; then + msg_info "Downloading template $TEMPLATE..." + pveam download local "$TEMPLATE" + msg_ok "Template downloaded." +fi + +# ---------- confirm ---------- +SUMMARY="CTID: $CTID +Hostname: $CT_HOSTNAME +Template: $TEMPLATE +Storage: $CT_STORAGE (${DISK_GB}G) +CPU/RAM: ${CORES} cores / ${MEMORY}MB (+${SWAP}MB swap) +Bridge: $BRIDGE${VLAN:+ (vlan $VLAN)} +Network: ${NET_MODE}${STATIC_IP_CIDR:+ $STATIC_IP_CIDR via $STATIC_GW} +Priv: $([[ $UNPRIVILEGED -eq 1 ]] && echo unprivileged || echo privileged) +Grafana: port $GRAFANA_PORT" + +whiptail --title "Confirm" --yesno "$SUMMARY + +Proceed?" 20 65 || exit 1 + +# ---------- build net string ---------- +if [[ "$NET_MODE" == "static" ]]; then + NET_STR="name=eth0,bridge=${BRIDGE},ip=${STATIC_IP_CIDR},gw=${STATIC_GW}" +else + NET_STR="name=eth0,bridge=${BRIDGE},ip=dhcp" +fi +[[ -n "$VLAN" ]] && NET_STR="${NET_STR},tag=${VLAN}" + +# ---------- create + start ---------- +CREATE_ARGS=( + "$CTID" "local:vztmpl/${TEMPLATE}" + --hostname "$CT_HOSTNAME" + --cores "$CORES" + --memory "$MEMORY" + --swap "$SWAP" + --rootfs "${CT_STORAGE}:${DISK_GB}" + --net0 "$NET_STR" + --unprivileged "$UNPRIVILEGED" + --onboot 1 + --features "nesting=1" + --password "$ROOT_PASSWORD" +) +[[ -n "${SSH_PUBKEY_FILE:-}" && -f "${SSH_PUBKEY_FILE:-}" ]] && CREATE_ARGS+=(--ssh-public-keys "$SSH_PUBKEY_FILE") + +msg_info "Creating CT $CTID ($CT_HOSTNAME)..." +pct create "${CREATE_ARGS[@]}" >/dev/null +msg_ok "Container created." + +msg_info "Starting CT $CTID..." +pct start "$CTID" + +msg_info "Waiting for network..." +for i in {1..30}; do + pct exec "$CTID" -- sh -c "true" >/dev/null 2>&1 && pct exec "$CTID" -- ping -c1 -W2 1.1.1.1 >/dev/null 2>&1 && break + sleep 2 + [[ $i -eq 30 ]] && die "CT never came up with network access." +done +msg_ok "Network is up." + +# ---------- install grafana inside the CT ---------- +run() { pct exec "$CTID" -- bash -c "$1"; } + +msg_info "Refreshing zypper repos..." +run "zypper --non-interactive refresh" >/dev/null +msg_ok "Repos refreshed." + +msg_info "Installing prerequisites (curl, gpg2, firewalld)..." +run "zypper --non-interactive install curl gpg2 firewalld" >/dev/null +msg_ok "Prerequisites installed." + +msg_info "Adding Grafana repo..." +run "zypper -n ar --refresh https://rpm.grafana.com grafana || true" +run "zypper --gpg-auto-import-keys --non-interactive refresh" >/dev/null +msg_ok "Grafana repo added." + +msg_info "Installing Grafana..." +run "zypper --non-interactive install grafana" >/dev/null +msg_ok "Grafana installed." + +msg_info "Configuring Grafana (port ${GRAFANA_PORT}, admin password)..." +run "sed -i '/^\[server\]/,/^\[/{s/^;\{0,1\}[[:space:]]*http_port[[:space:]]*=.*/http_port = ${GRAFANA_PORT}/}' /etc/grafana/grafana.ini" +run "sed -i '/^\[security\]/,/^\[/{s/^;\{0,1\}[[:space:]]*admin_password[[:space:]]*=.*/admin_password = ${GRAFANA_ADMIN_PASSWORD}/}' /etc/grafana/grafana.ini" +msg_ok "Grafana configured." + +msg_info "Enabling firewalld and opening ports 22/${GRAFANA_PORT}..." +run "systemctl enable --now firewalld" >/dev/null +run "firewall-cmd --permanent --add-port=22/tcp" >/dev/null +run "firewall-cmd --permanent --add-port=${GRAFANA_PORT}/tcp" >/dev/null +run "firewall-cmd --reload" >/dev/null +msg_ok "Firewall configured." + +msg_info "Starting Grafana..." +run "systemctl enable --now grafana-server" >/dev/null + +msg_info "Waiting for Grafana to answer..." +for i in {1..15}; do + run "curl -sf http://localhost:${GRAFANA_PORT}/api/health >/dev/null" && break + sleep 3 + [[ $i -eq 15 ]] && die "Grafana did not come up in time — check: pct exec $CTID -- systemctl status grafana-server" +done +msg_ok "Grafana is up." + +# ---------- summary ---------- +CT_IP=$(pct exec "$CTID" -- hostname -I 2>/dev/null | awk '{print $1}') + +whiptail --title "$APP LXC Installer — Done" --msgbox \ +"Grafana is running. + +CT ID: $CTID +URL: http://${CT_IP}:${GRAFANA_PORT} +User: admin +Password: ${GRAFANA_ADMIN_PASSWORD} +Root pass: ${ROOT_PASSWORD} + +(also printed below in the terminal)" 16 65 + +cat <