#!/bin/bash # Service-specific rootless Docker installation # Implements DEPLOYMENT_STRATEGY.md conventions set -euo pipefail # Required parameter SERVICE_USER="${1:-}" log() { echo "[Docker Install] $*" } error() { log "ERROR: $*" >&2 exit 1 } usage() { echo "Usage: $0 " echo " service_user: User to install rootless Docker for (e.g., tinsnip-test)" echo "" echo "Example: $0 tinsnip-test" exit 1 } # Helper function to run systemctl with fallback if systemd not available systemctl_user_safe() { local username="$1" shift local systemctl_command="$*" sudo -u "$username" -i bash << EOF if systemctl --user status >/dev/null 2>&1; then systemctl --user $systemctl_command exit 0 else exit 1 fi EOF local result=$? if [[ $result -eq 1 ]]; then log " ⚠️ Systemd not available, skipping: systemctl --user $systemctl_command" fi return $result } # Initialize systemd user session for a service user init_systemd_user_session() { local username="$1" local user_uid=$(id -u "$username") log "Initializing systemd user session for $username..." # Create XDG_RUNTIME_DIR if it doesn't exist if [[ ! -d "/run/user/$user_uid" ]]; then log " Creating XDG_RUNTIME_DIR for $username" sudo mkdir -p "/run/user/$user_uid" sudo chown "$username:$username" "/run/user/$user_uid" sudo chmod 700 "/run/user/$user_uid" fi # Enable lingering (creates persistent systemd user instance) log " Enabling systemd lingering..." sudo loginctl enable-linger "$username" # Start user@.service if not running if ! systemctl is-active --quiet "user@$user_uid.service"; then log " Starting user@$user_uid.service..." sudo systemctl start "user@$user_uid.service" fi # Wait for user session to be ready log " Waiting for systemd user session to initialize..." local max_attempts=10 local attempt=0 while [[ $attempt -lt $max_attempts ]]; do if sudo -u "$username" -i bash -c "XDG_RUNTIME_DIR=/run/user/$user_uid systemctl --user status >/dev/null 2>&1"; then log " ✓ Systemd user session ready" return 0 fi sleep 1 ((attempt++)) done log " WARNING: Systemd user session may not be fully initialized" return 1 } install_dependencies() { log "Installing rootless Docker dependencies..." if sudo apt-get update -qq >/dev/null 2>&1; then log " Package lists updated" else error "Failed to update package lists" fi # Install systemd and dbus components first for minimal systems log " Installing systemd components for user sessions..." if ! sudo apt-get install -y systemd systemd-sysv dbus dbus-user-session >/dev/null 2>&1; then log " WARNING: Some systemd components may already be installed" fi # Install Docker dependencies if sudo apt-get install -y uidmap systemd-container fuse-overlayfs slirp4netns >/dev/null 2>&1; then log " Docker dependencies installed" else error "Failed to install Docker dependencies" fi # Ensure dbus is running (needed for systemd user sessions) if ! systemctl is-active --quiet dbus; then log " Starting dbus service..." sudo systemctl start dbus sudo systemctl enable dbus fi } enable_privileged_ports() { log "Enabling privileged port binding for rootless Docker..." # Find rootlesskit binary path local rootlesskit_path if rootlesskit_path=$(which rootlesskit 2>/dev/null); then log "Setting CAP_NET_BIND_SERVICE on rootlesskit..." sudo setcap cap_net_bind_service=ep "$rootlesskit_path" log "Privileged ports enabled" else log "WARNING: rootlesskit not found, will enable after Docker installation" fi } setup_docker_environment() { local username="$1" log "Setting up Docker environment for $username" # Find the service .env file (should exist from mount_nas.sh) local service_env_file="/mnt/$username/.env" if [[ ! -f "$service_env_file" ]]; then log "WARNING: Service .env file not found at $service_env_file" log "Docker environment will not be persistent across hosts" return 1 fi # Always use systemd runtime directory since lingering is enabled local user_uid user_uid=$(id -u "$username") local xdg_runtime_dir="/run/user/$user_uid" local docker_host="unix:///run/user/$user_uid/docker.sock" # Start Docker daemon if not running sudo -u "$username" -i bash << EOF if ! pgrep -x dockerd >/dev/null; then export XDG_RUNTIME_DIR="$xdg_runtime_dir" export DOCKER_HOST="$docker_host" dockerd-rootless.sh > ~/.docker/docker.log 2>&1 & sleep 5 fi EOF # Update Docker environment variables in service .env file log "Adding Docker environment to service .env file" local service_env_file="/mnt/$username/.env" if [[ -f "$service_env_file" ]]; then # Remove existing Docker variables and add correct ones sudo -u "$username" bash -c "grep -v '^XDG_RUNTIME_DIR=\|^DOCKER_HOST=\|^PATH=' '$service_env_file' > '${service_env_file}.tmp' 2>/dev/null && mv '${service_env_file}.tmp' '$service_env_file'" # Add Docker environment variables using systemd runtime directory sudo -u "$username" bash -c "cat >> '$service_env_file' << 'DOCKER_EOF' # Docker rootless environment XDG_RUNTIME_DIR=$xdg_runtime_dir DOCKER_HOST=$docker_host PATH=/home/$username/bin:\$PATH DOCKER_EOF" log "Docker environment added to $service_env_file" else log "WARNING: Service .env file not found at $service_env_file" fi } install_rootless_docker() { local username="$1" local user_uid=$(id -u "$username") log "Installing rootless Docker for user: $username" # Check if already installed AND working if sudo -u "$username" -i bash -c "command -v docker &>/dev/null && docker version &>/dev/null"; then log "Docker already installed and working for $username" return 0 elif sudo -u "$username" -i bash -c "command -v docker &>/dev/null"; then log "Docker client installed but daemon not running for $username" setup_docker_environment "$username" # Continue to start the daemon below else log "Installing Docker client for $username" fi # Install rootless Docker with proper environment handling sudo -u "$username" -i bash << EOF # Set up environment explicitly export XDG_RUNTIME_DIR=/run/user/$user_uid export PATH=\$HOME/bin:/usr/bin:\$PATH export DOCKER_HOST=unix:///run/user/$user_uid/docker.sock echo "Installing rootless Docker with environment:" echo " XDG_RUNTIME_DIR: \$XDG_RUNTIME_DIR" echo " DOCKER_HOST: \$DOCKER_HOST" # Download and install rootless Docker curl -fsSL https://get.docker.com/rootless | sh # Try systemd setup first if systemctl --user status >/dev/null 2>&1; then echo " Setting up systemd service..." \$HOME/bin/dockerd-rootless-setuptool.sh install >/dev/null 2>&1 || echo " Systemd setup failed, using manual start" systemctl --user daemon-reload >/dev/null 2>&1 systemctl --user enable docker.service >/dev/null 2>&1 systemctl --user start docker.service >/dev/null 2>&1 # Wait for systemd Docker to start for i in {1..5}; do if docker version >/dev/null 2>&1; then echo " ✓ Docker started via systemd" exit 0 fi sleep 2 done echo " Systemd start failed, falling back to manual..." fi # Manual startup with explicit environment echo " Starting Docker daemon manually..." # Create .docker directory first mkdir -p ~/.docker # Start with explicit environment and proper logging nohup dockerd-rootless.sh > ~/.docker/docker.log 2>&1 & DOCKER_PID=\$! echo " Docker daemon started with PID: \$DOCKER_PID" # Wait for manual Docker to start for i in {1..15}; do if docker version >/dev/null 2>&1; then echo " ✓ Docker rootless installation successful (manual startup)" echo " Docker daemon PID: \$DOCKER_PID" exit 0 fi echo " Waiting for Docker to start... (\$i/15)" if [[ \$i -eq 5 ]] || [[ \$i -eq 10 ]]; then echo " Checking daemon status..." if ! kill -0 \$DOCKER_PID 2>/dev/null; then echo " ❌ Docker daemon died, checking logs:" tail -n 10 ~/.docker/docker.log fi fi sleep 2 done echo " ❌ Docker installation failed" echo " Docker log:" cat ~/.docker/docker.log 2>/dev/null || echo " No log file found" exit 1 EOF if [[ $? -eq 0 ]]; then log "Rootless Docker installation completed for $username" else error "Failed to install Docker for $username" fi # Enable privileged ports after installation enable_privileged_ports # Restart Docker to apply capability changes log "Restarting Docker to apply configuration..." if systemctl_user_safe "$username" "restart docker.service"; then log " Docker service restarted via systemctl" else log " Systemctl restart failed, but Docker should still be functional" fi } configure_docker() { local username="$1" log "Configuring Docker for $username..." # Create Docker config directory and local data directory sudo -u "$username" mkdir -p "/home/$username/.docker" sudo -u "$username" mkdir -p "/home/$username/.local/share/docker" # Create daemon.json with optimized settings # Always disable cgroup management for rootless Docker to avoid systemd delegation issues # Use local data-root to avoid NFS permission issues with XDG_DATA_HOME on NFS # This is the most reliable approach for tinsnip deployments sudo -u "$username" mkdir -p "/home/$username/.local/share/docker" sudo -u "$username" tee "/home/$username/.docker/daemon.json" > /dev/null << EOF { "data-root": "/home/$username/.local/share/docker", "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" }, "storage-driver": "overlay2", "exec-opts": ["native.cgroupdriver=none"] } EOF # Ensure proper ownership of Docker directories sudo -u "$username" chmod 755 "/home/$username/.local/share/docker" # Check if Docker is already working before attempting restart log "Checking Docker status before configuration restart..." if sudo -u "$username" -i bash -c "docker version &>/dev/null"; then log " Docker is already working, skipping restart to avoid issues" else log " Docker not responding, attempting restart..." if systemctl_user_safe "$username" "restart docker.service"; then log " Docker service restarted via systemctl" # Give Docker time to start up after restart log "Waiting for Docker to start..." sleep 10 else log " Systemctl restart failed, Docker may already be running in non-systemd mode" fi fi # Set up Docker environment variables in .env file setup_docker_environment "$username" # Create Docker context for rootless socket (before starting daemon) log "Setting up Docker context..." local user_uid user_uid=$(id -u "$username") sudo -u "$username" bash -c "docker context create rootless --docker 'host=unix:///run/user/$user_uid/docker.sock' >/dev/null 2>&1 || true" sudo -u "$username" bash -c "docker context use rootless >/dev/null 2>&1 || true" log "Docker configuration complete" # Ensure Docker daemon is running after configuration ensure_docker_running "$username" } ensure_docker_running() { local username="$1" local user_uid=$(id -u "$username") log "Ensuring Docker daemon is running for $username..." # Check if Docker is already responding if sudo -u "$username" -i bash -c "docker version &>/dev/null"; then log " ✓ Docker daemon already running" return 0 fi log " Docker not responding, starting daemon..." # Try systemd first if systemctl_user_safe "$username" "start docker.service"; then log " Started via systemd" # Wait for systemd start for i in {1..10}; do if sudo -u "$username" -i bash -c "docker version &>/dev/null"; then log " ✓ Docker daemon started successfully" return 0 fi sleep 2 done fi # Fall back to manual start log " Starting manually with explicit environment..." sudo -u "$username" -i bash << EOF # Kill any existing dockerd processes for this user first pkill -f "dockerd-rootless" 2>/dev/null || true sleep 2 # Explicitly set environment variables that dockerd-rootless.sh needs export XDG_RUNTIME_DIR="/run/user/$user_uid" export DOCKER_HOST="unix:///run/user/$user_uid/docker.sock" export PATH="/home/$username/bin:\$PATH" # Clear any environment variables that might override daemon.json unset DOCKER_ROOT DOCKER_DATA_ROOT echo "Environment check:" echo " XDG_RUNTIME_DIR: '\$XDG_RUNTIME_DIR'" echo " DOCKER_HOST: '\$DOCKER_HOST'" echo " Directory exists: \$(test -d "\$XDG_RUNTIME_DIR" && echo "yes" || echo "no")" echo " Directory writable: \$(test -w "\$XDG_RUNTIME_DIR" && echo "yes" || echo "no")" # Start Docker daemon with explicit environment mkdir -p ~/.docker nohup env -u DOCKER_ROOT -u DOCKER_DATA_ROOT -u XDG_DATA_HOME XDG_RUNTIME_DIR="/run/user/$user_uid" DOCKER_HOST="unix:///run/user/$user_uid/docker.sock" dockerd-rootless.sh --data-root="/home/$username/.local/share/docker" > ~/.docker/docker.log 2>&1 & DOCKER_PID=\$! echo "Docker daemon started with PID: \$DOCKER_PID" # Wait for Docker to be ready for i in {1..15}; do if XDG_RUNTIME_DIR="/run/user/$user_uid" DOCKER_HOST="unix:///run/user/$user_uid/docker.sock" docker version >/dev/null 2>&1; then echo "✓ Docker daemon ready" exit 0 fi echo " Waiting for Docker... (\$i/15)" sleep 2 done echo "❌ Failed to start Docker daemon" echo "Docker log:" tail -n 20 ~/.docker/docker.log 2>/dev/null || echo "No log file found" exit 1 EOF if [[ $? -eq 0 ]]; then log " ✓ Docker daemon started manually" else log " ❌ Failed to start Docker daemon" return 1 fi } verify_installation() { local username="$1" log "Verifying Docker installation for $username..." # Use the service .env file (source of truth) local service_env_file="/mnt/$username/.env" # Docker context already set up in configure_docker() # Debug verification process log "Debugging verification process..." log " Service env file: $service_env_file" if [[ -f "$service_env_file" ]]; then log " Environment variables in service .env:" sudo -u "$username" grep "DOCKER\|XDG_RUNTIME" "$service_env_file" | while read line; do log " $line" done else log " WARNING: Service .env file not found!" fi # Test Docker verification with detailed output log " Testing Docker command with environment..." if sudo -u "$username" bash -c "source '$service_env_file' 2>/dev/null && docker version"; then log "Docker verification successful!" local docker_version docker_version=$(sudo -u "$username" bash -c "source '$service_env_file' && docker --version") log "Installed: $docker_version" else log "Docker verification failed - showing detailed error:" sudo -u "$username" bash -c "source '$service_env_file' 2>/dev/null && docker version" 2>&1 | while read line; do log " ERROR: $line" done error "Docker verification failed for $username" fi log "Service available for user: $username" log "Privileged ports: enabled" } main() { # Validate parameters if [[ -z "$SERVICE_USER" ]]; then usage fi # Check if user exists if ! id "$SERVICE_USER" &>/dev/null; then error "User $SERVICE_USER does not exist. Create the user first." fi log "Installing rootless Docker for service user: $SERVICE_USER" # Install dependencies install_dependencies # Initialize systemd user session (handles lingering and XDG_RUNTIME_DIR) init_systemd_user_session "$SERVICE_USER" # Configure Docker (before installation to set up proper data directory) configure_docker "$SERVICE_USER" # Install rootless Docker for the service user install_rootless_docker "$SERVICE_USER" # Verify installation verify_installation "$SERVICE_USER" log "" log "Rootless Docker installation complete!" log "User: $SERVICE_USER" log "To test: sudo -u $SERVICE_USER docker run hello-world" } main "$@"