351 lines
14 KiB
Bash
351 lines
14 KiB
Bash
#!/bin/bash
|
||
# =============================================================================
|
||
# 🐳 StianNOR — PORTAINER INSTALLER v2.1
|
||
# =============================================================================
|
||
set -euo pipefail
|
||
|
||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
|
||
BLUE='\033[0;34m'; CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m'
|
||
LINE="────────────────────────────────────────────────────────────────────────────"
|
||
|
||
info() { echo -e "${CYAN}ℹ $*${RESET}"; }
|
||
success() { echo -e "${GREEN}✅ $*${RESET}"; }
|
||
warn() { echo -e "${YELLOW}⚠ $*${RESET}"; }
|
||
error() { echo -e "${RED}❌ $*${RESET}"; }
|
||
step() { echo -e "\n${BOLD}${BLUE}➤ $*${RESET}"; echo -e "${BLUE}${LINE}${RESET}"; }
|
||
die() { error "$*"; exit 1; }
|
||
|
||
FORCE_UPDATE=false
|
||
[[ "${1:-}" == "--update" ]] && FORCE_UPDATE=true
|
||
|
||
# =============================================================================
|
||
# SUDO — cache + keepalive
|
||
# =============================================================================
|
||
step "Checking sudo access"
|
||
sudo -v || die "sudo required"
|
||
( while true; do sudo -n true; sleep 55; done ) &
|
||
KEEPALIVE_PID=$!
|
||
trap 'kill "$KEEPALIVE_PID" 2>/dev/null || true' EXIT INT TERM
|
||
|
||
# =============================================================================
|
||
# DETECT DISTRO
|
||
# =============================================================================
|
||
step "Detecting Linux distribution"
|
||
[[ ! -f /etc/os-release ]] && die "Cannot detect distro — /etc/os-release missing"
|
||
source /etc/os-release
|
||
DISTRO_ID="${ID,,}"
|
||
DISTRO_CODENAME="${VERSION_CODENAME:-}"
|
||
[[ -z "$DISTRO_CODENAME" ]] && DISTRO_CODENAME=$(lsb_release -cs 2>/dev/null || echo "")
|
||
[[ -z "$DISTRO_CODENAME" && "$DISTRO_ID" == "ubuntu" ]] && DISTRO_CODENAME="jammy"
|
||
[[ -z "$DISTRO_CODENAME" && "$DISTRO_ID" == "debian" ]] && DISTRO_CODENAME="bookworm"
|
||
|
||
# Map to distro family
|
||
DISTRO_FAMILY="$DISTRO_ID"
|
||
case "$DISTRO_ID" in
|
||
ubuntu|debian|linuxmint|raspbian|pop) DISTRO_FAMILY="debian" ;;
|
||
fedora) DISTRO_FAMILY="fedora" ;;
|
||
centos|rhel|rocky|almalinux|ol) DISTRO_FAMILY="rhel" ;;
|
||
arch|manjaro|endeavouros|garuda|artix|arcolinux) DISTRO_FAMILY="arch" ;;
|
||
opensuse*|suse|sles) DISTRO_FAMILY="suse" ;;
|
||
alpine) DISTRO_FAMILY="alpine" ;;
|
||
*)
|
||
# Try ID_LIKE fallback
|
||
case "${ID_LIKE:-}" in
|
||
*debian*|*ubuntu*) DISTRO_FAMILY="debian" ;;
|
||
*rhel*|*fedora*) DISTRO_FAMILY="fedora" ;;
|
||
*arch*) DISTRO_FAMILY="arch" ;;
|
||
*suse*) DISTRO_FAMILY="suse" ;;
|
||
esac ;;
|
||
esac
|
||
success "Detected: ${NAME} (family: ${DISTRO_FAMILY})"
|
||
|
||
# =============================================================================
|
||
# INSTALL DOCKER
|
||
# =============================================================================
|
||
install_docker() {
|
||
step "Installing Docker CE"
|
||
case "$DISTRO_FAMILY" in
|
||
|
||
debian)
|
||
sudo apt-get update -qq
|
||
sudo apt-get install -y -qq ca-certificates curl gnupg lsb-release
|
||
sudo install -m0755 -d /etc/apt/keyrings
|
||
curl -fsSL "https://download.docker.com/linux/${DISTRO_ID}/gpg" \
|
||
| sudo gpg --dearmor --yes -o /etc/apt/keyrings/docker.gpg
|
||
sudo chmod a+r /etc/apt/keyrings/docker.gpg
|
||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
|
||
https://download.docker.com/linux/${DISTRO_ID} ${DISTRO_CODENAME} stable" \
|
||
| sudo tee /etc/apt/sources.list.d/docker.list >/dev/null
|
||
sudo apt-get update -qq
|
||
sudo apt-get install -y \
|
||
docker-ce docker-ce-cli containerd.io \
|
||
docker-buildx-plugin docker-compose-plugin
|
||
;;
|
||
|
||
fedora)
|
||
sudo dnf -y remove podman-docker 2>/dev/null || true
|
||
sudo dnf -y install dnf-plugins-core
|
||
sudo dnf config-manager \
|
||
--add-repo https://download.docker.com/linux/fedora/docker-ce.repo
|
||
sudo dnf -y install \
|
||
docker-ce docker-ce-cli containerd.io \
|
||
docker-buildx-plugin docker-compose-plugin
|
||
;;
|
||
|
||
rhel)
|
||
sudo yum install -y yum-utils
|
||
sudo yum-config-manager \
|
||
--add-repo https://download.docker.com/linux/centos/docker-ce.repo
|
||
sudo yum install -y \
|
||
docker-ce docker-ce-cli containerd.io \
|
||
docker-buildx-plugin docker-compose-plugin
|
||
;;
|
||
|
||
arch)
|
||
sudo pacman -Sy --noconfirm --needed docker docker-compose
|
||
;;
|
||
|
||
suse)
|
||
sudo zypper install -y docker docker-compose
|
||
;;
|
||
|
||
alpine)
|
||
sudo apk add --update docker docker-compose
|
||
sudo rc-update add docker boot
|
||
sudo service docker start
|
||
success "Docker installed (Alpine)"
|
||
return
|
||
;;
|
||
|
||
*)
|
||
die "Unsupported distro: $DISTRO_ID — see https://docs.docker.com/engine/install/"
|
||
;;
|
||
esac
|
||
|
||
sudo systemctl enable --now docker
|
||
success "Docker installed and started"
|
||
}
|
||
|
||
# =============================================================================
|
||
# FIX DOCKER SOCKET PERMISSIONS
|
||
# The real fix for "permission denied on /var/run/docker.sock":
|
||
# 1. Add user to docker group
|
||
# 2. Fix socket group ownership so the group can actually access it
|
||
# 3. Tell the user to run `newgrp docker` in their shell — we cannot
|
||
# push group changes to the parent shell process, that's a kernel limit.
|
||
# =============================================================================
|
||
fix_docker_permissions() {
|
||
step "Fixing Docker permissions for $USER"
|
||
|
||
# Ensure docker group exists
|
||
getent group docker >/dev/null 2>&1 || sudo groupadd docker
|
||
|
||
# Add user to docker group
|
||
if ! groups "$USER" | grep -qw docker; then
|
||
sudo usermod -aG docker "$USER"
|
||
success "Added $USER to docker group"
|
||
else
|
||
success "$USER already in docker group"
|
||
fi
|
||
|
||
# Fix socket group ownership and permissions
|
||
# This is what actually makes docker work without sudo RIGHT NOW
|
||
if [[ -S /var/run/docker.sock ]]; then
|
||
sudo chown root:docker /var/run/docker.sock
|
||
sudo chmod 660 /var/run/docker.sock
|
||
success "Fixed /var/run/docker.sock permissions (root:docker 660)"
|
||
fi
|
||
|
||
# Fix ~/.docker ownership
|
||
sudo mkdir -p "$HOME/.docker"
|
||
sudo chown -R "$USER:$USER" "$HOME/.docker"
|
||
success "Fixed ~/.docker ownership"
|
||
|
||
# Make socket permissions persist across reboots via systemd override
|
||
OVERRIDE_DIR="/etc/systemd/system/docker.socket.d"
|
||
if [[ -d /etc/systemd/system ]] && ! [[ -f "$OVERRIDE_DIR/permissions.conf" ]]; then
|
||
sudo mkdir -p "$OVERRIDE_DIR"
|
||
sudo tee "$OVERRIDE_DIR/permissions.conf" >/dev/null <<'OVERRIDE'
|
||
[Socket]
|
||
SocketMode=0660
|
||
SocketGroup=docker
|
||
OVERRIDE
|
||
sudo systemctl daemon-reload
|
||
sudo systemctl restart docker.socket docker 2>/dev/null || true
|
||
success "Persistent socket permission override installed"
|
||
fi
|
||
}
|
||
|
||
# =============================================================================
|
||
# ACTIVATE GROUP IN CURRENT SESSION
|
||
# sg/newgrp cannot push groups back to the parent shell.
|
||
# Best we can do: detect if THIS script process already has docker group,
|
||
# and if not, use `sg` to run the rest of the script with the group active.
|
||
# =============================================================================
|
||
activate_docker_group() {
|
||
# Check if docker group is already active in this process
|
||
if id -nG "$USER" 2>/dev/null | grep -qw docker && \
|
||
[[ "$(stat -c '%G' /var/run/docker.sock 2>/dev/null)" == "docker" ]]; then
|
||
# Try running docker without sudo
|
||
if docker info >/dev/null 2>&1; then
|
||
success "Docker accessible without sudo ✓"
|
||
DOCKER_CMD="docker"
|
||
return 0
|
||
fi
|
||
fi
|
||
|
||
# Socket fix applied above — try again with newgrp trick
|
||
# We use `sg docker` to get the group active for the REST of this script
|
||
if [[ "${_DOCKER_GROUP_EXEC:-}" != "1" ]]; then
|
||
export _DOCKER_GROUP_EXEC=1
|
||
info "Activating docker group for this session via sg..."
|
||
exec sg docker -c "export _DOCKER_GROUP_EXEC=1; bash '$0' ${1:-}"
|
||
fi
|
||
|
||
# If still failing, use sudo docker for the rest of the script
|
||
if sudo docker info >/dev/null 2>&1; then
|
||
warn "Using sudo docker for this session."
|
||
warn "To use docker without sudo in YOUR terminal, run:"
|
||
warn " newgrp docker"
|
||
warn "Or log out and back in."
|
||
DOCKER_CMD="sudo docker"
|
||
else
|
||
die "Docker daemon is not accessible even with sudo. Check: sudo systemctl status docker"
|
||
fi
|
||
}
|
||
|
||
# =============================================================================
|
||
# MAIN — INSTALL IF NEEDED
|
||
# =============================================================================
|
||
if ! command -v docker >/dev/null 2>&1; then
|
||
install_docker
|
||
else
|
||
success "Docker already installed: $(docker --version)"
|
||
fi
|
||
|
||
fix_docker_permissions
|
||
activate_docker_group
|
||
|
||
# =============================================================================
|
||
# DOCKER COMPOSE CHECK
|
||
# =============================================================================
|
||
step "Checking Docker Compose"
|
||
if $DOCKER_CMD compose version >/dev/null 2>&1; then
|
||
success "Docker Compose plugin: $($DOCKER_CMD compose version --short 2>/dev/null || echo installed)"
|
||
elif command -v docker-compose >/dev/null 2>&1; then
|
||
success "Docker Compose standalone: $(docker-compose --version)"
|
||
else
|
||
info "Installing Docker Compose standalone..."
|
||
sudo curl -fsSL \
|
||
"https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" \
|
||
-o /usr/local/bin/docker-compose
|
||
sudo chmod +x /usr/local/bin/docker-compose
|
||
success "Docker Compose installed: $(/usr/local/bin/docker-compose --version)"
|
||
fi
|
||
|
||
# =============================================================================
|
||
# ENSURE DOCKER IS RUNNING
|
||
# =============================================================================
|
||
step "Ensuring Docker service is running"
|
||
if [[ "$DISTRO_FAMILY" == "alpine" ]]; then
|
||
sudo service docker status 2>/dev/null | grep -q started \
|
||
|| { sudo service docker start; sleep 3; }
|
||
else
|
||
if ! sudo systemctl is-active --quiet docker; then
|
||
sudo systemctl start docker
|
||
sleep 3
|
||
fi
|
||
fi
|
||
success "Docker service running"
|
||
|
||
# =============================================================================
|
||
# PORTAINER
|
||
# =============================================================================
|
||
step "Installing Portainer CE"
|
||
|
||
PORTAINER_EXISTS=false
|
||
$DOCKER_CMD ps -a --format '{{.Names}}' 2>/dev/null | grep -q "^portainer$" \
|
||
&& PORTAINER_EXISTS=true
|
||
|
||
if [[ "$PORTAINER_EXISTS" == true ]]; then
|
||
if [[ "$FORCE_UPDATE" == true ]]; then
|
||
info "Removing existing Portainer for update..."
|
||
$DOCKER_CMD stop portainer 2>/dev/null || true
|
||
$DOCKER_CMD rm portainer 2>/dev/null || true
|
||
else
|
||
PORTAINER_STATUS=$($DOCKER_CMD inspect --format '{{.State.Status}}' portainer 2>/dev/null || echo "unknown")
|
||
success "Portainer already installed (status: $PORTAINER_STATUS)"
|
||
info "Run with --update to force reinstall"
|
||
IP=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||
[[ -z "$IP" ]] && IP=$(ip route get 1.1.1.1 2>/dev/null | awk '{print $7; exit}')
|
||
[[ -z "$IP" ]] && IP="localhost"
|
||
echo -e "\n${LINE}"
|
||
echo -e " ${BOLD}${CYAN}🌐 Portainer:${RESET} ${GREEN}https://${IP}:9443${RESET}"
|
||
echo -e "${LINE}"
|
||
exit 0
|
||
fi
|
||
fi
|
||
|
||
info "Pulling latest Portainer CE image..."
|
||
$DOCKER_CMD pull portainer/portainer-ce:latest
|
||
|
||
# SELinux detection
|
||
SELINUX_FLAG=""
|
||
if command -v getenforce >/dev/null 2>&1 && [[ "$(getenforce 2>/dev/null)" == "Enforcing" ]]; then
|
||
SELINUX_FLAG="--security-opt label=disable"
|
||
info "SELinux enforcing — adding label=disable"
|
||
fi
|
||
|
||
info "Starting Portainer container..."
|
||
$DOCKER_CMD run -d \
|
||
$SELINUX_FLAG \
|
||
-p 8000:8000 \
|
||
-p 9443:9443 \
|
||
--name portainer \
|
||
--restart=always \
|
||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||
-v portainer_data:/data \
|
||
portainer/portainer-ce:latest
|
||
|
||
success "Portainer container started"
|
||
|
||
# =============================================================================
|
||
# DONE
|
||
# =============================================================================
|
||
IP=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||
[[ -z "$IP" ]] && IP=$(ip route get 1.1.1.1 2>/dev/null | awk '{print $7; exit}')
|
||
[[ -z "$IP" ]] && IP="localhost"
|
||
|
||
# Check if docker still needs newgrp in the user's shell
|
||
NEEDS_NEWGRP=false
|
||
if ! docker info >/dev/null 2>&1; then
|
||
NEEDS_NEWGRP=true
|
||
fi
|
||
|
||
echo ""
|
||
echo -e "${LINE}"
|
||
echo -e "${BOLD}${GREEN} ✅ Portainer CE installed successfully!${RESET}"
|
||
echo -e "${LINE}"
|
||
echo -e " ${CYAN}Dashboard:${RESET} ${GREEN}https://${IP}:9443${RESET}"
|
||
echo -e " ${CYAN}Tunnel:${RESET} ${BLUE}http://${IP}:8000${RESET}"
|
||
echo -e " ${YELLOW}Note:${RESET} Accept the self-signed certificate on first visit."
|
||
echo -e " ${YELLOW}Note:${RESET} Create your admin account within 5 minutes of starting."
|
||
echo -e "${LINE}"
|
||
|
||
if [[ "$NEEDS_NEWGRP" == true ]]; then
|
||
echo ""
|
||
echo -e "${LINE}"
|
||
echo -e " ${BOLD}${YELLOW}⚠ ONE MORE STEP — activate docker group in your shell:${RESET}"
|
||
echo -e "${LINE}"
|
||
echo -e " Docker is installed and working, but your CURRENT terminal"
|
||
echo -e " session doesn't have the docker group active yet."
|
||
echo -e ""
|
||
echo -e " Run this in your terminal now:"
|
||
echo -e " ${BOLD}${CYAN} newgrp docker${RESET}"
|
||
echo -e ""
|
||
echo -e " Or simply log out and back in."
|
||
echo -e " After that, ${CYAN}docker ps${RESET} will work without sudo."
|
||
echo -e "${LINE}"
|
||
fi
|
||
echo ""
|