homelab infrastructure services
1#!/bin/bash
2
3# Service-specific rootless Docker installation
4# Implements DEPLOYMENT_STRATEGY.md conventions
5
6set -euo pipefail
7
8# Required parameter
9SERVICE_USER="${1:-}"
10
11log() {
12 echo "[Docker Install] $*"
13}
14
15error() {
16 log "ERROR: $*" >&2
17 exit 1
18}
19
20usage() {
21 echo "Usage: $0 <service_user>"
22 echo " service_user: User to install rootless Docker for (e.g., tinsnip-test)"
23 echo ""
24 echo "Example: $0 tinsnip-test"
25 exit 1
26}
27
28# Helper function to run systemctl with fallback if systemd not available
29systemctl_user_safe() {
30 local username="$1"
31 shift
32 local systemctl_command="$*"
33
34 sudo -u "$username" -i bash << EOF
35 if systemctl --user status >/dev/null 2>&1; then
36 systemctl --user $systemctl_command
37 exit 0
38 else
39 exit 1
40 fi
41EOF
42
43 local result=$?
44 if [[ $result -eq 1 ]]; then
45 log " ⚠️ Systemd not available, skipping: systemctl --user $systemctl_command"
46 fi
47
48 return $result
49}
50
51# Initialize systemd user session for a service user
52init_systemd_user_session() {
53 local username="$1"
54 local user_uid=$(id -u "$username")
55
56 log "Initializing systemd user session for $username..."
57
58 # Create XDG_RUNTIME_DIR if it doesn't exist
59 if [[ ! -d "/run/user/$user_uid" ]]; then
60 log " Creating XDG_RUNTIME_DIR for $username"
61 sudo mkdir -p "/run/user/$user_uid"
62 sudo chown "$username:$username" "/run/user/$user_uid"
63 sudo chmod 700 "/run/user/$user_uid"
64 fi
65
66 # Enable lingering (creates persistent systemd user instance)
67 log " Enabling systemd lingering..."
68 sudo loginctl enable-linger "$username"
69
70 # Start user@.service if not running
71 if ! systemctl is-active --quiet "user@$user_uid.service"; then
72 log " Starting user@$user_uid.service..."
73 sudo systemctl start "user@$user_uid.service"
74 fi
75
76 # Wait for user session to be ready
77 log " Waiting for systemd user session to initialize..."
78 local max_attempts=10
79 local attempt=0
80
81 while [[ $attempt -lt $max_attempts ]]; do
82 if sudo -u "$username" -i bash -c "XDG_RUNTIME_DIR=/run/user/$user_uid systemctl --user status >/dev/null 2>&1"; then
83 log " ✓ Systemd user session ready"
84 return 0
85 fi
86 sleep 1
87 ((attempt++))
88 done
89
90 log " WARNING: Systemd user session may not be fully initialized"
91 return 1
92}
93
94install_dependencies() {
95 log "Installing rootless Docker dependencies..."
96
97 if sudo apt-get update -qq >/dev/null 2>&1; then
98 log " Package lists updated"
99 else
100 error "Failed to update package lists"
101 fi
102
103 # Install systemd and dbus components first for minimal systems
104 log " Installing systemd components for user sessions..."
105 if ! sudo apt-get install -y systemd systemd-sysv dbus dbus-user-session >/dev/null 2>&1; then
106 log " WARNING: Some systemd components may already be installed"
107 fi
108
109 # Install Docker dependencies
110 if sudo apt-get install -y uidmap systemd-container fuse-overlayfs slirp4netns >/dev/null 2>&1; then
111 log " Docker dependencies installed"
112 else
113 error "Failed to install Docker dependencies"
114 fi
115
116 # Ensure dbus is running (needed for systemd user sessions)
117 if ! systemctl is-active --quiet dbus; then
118 log " Starting dbus service..."
119 sudo systemctl start dbus
120 sudo systemctl enable dbus
121 fi
122}
123
124enable_privileged_ports() {
125 log "Enabling privileged port binding for rootless Docker..."
126
127 # Find rootlesskit binary path
128 local rootlesskit_path
129 if rootlesskit_path=$(which rootlesskit 2>/dev/null); then
130 log "Setting CAP_NET_BIND_SERVICE on rootlesskit..."
131 sudo setcap cap_net_bind_service=ep "$rootlesskit_path"
132 log "Privileged ports enabled"
133 else
134 log "WARNING: rootlesskit not found, will enable after Docker installation"
135 fi
136}
137
138setup_docker_environment() {
139 local username="$1"
140
141 log "Setting up Docker environment for $username"
142
143 # Find the service .env file (should exist from mount_nas.sh)
144 local service_env_file="/mnt/$username/.env"
145
146 if [[ ! -f "$service_env_file" ]]; then
147 log "WARNING: Service .env file not found at $service_env_file"
148 log "Docker environment will not be persistent across hosts"
149 return 1
150 fi
151
152 # Always use systemd runtime directory since lingering is enabled
153 local user_uid
154 user_uid=$(id -u "$username")
155 local xdg_runtime_dir="/run/user/$user_uid"
156 local docker_host="unix:///run/user/$user_uid/docker.sock"
157
158 # Start Docker daemon if not running
159 sudo -u "$username" -i bash << EOF
160if ! pgrep -x dockerd >/dev/null; then
161 export XDG_RUNTIME_DIR="$xdg_runtime_dir"
162 export DOCKER_HOST="$docker_host"
163 dockerd-rootless.sh > ~/.docker/docker.log 2>&1 &
164 sleep 5
165fi
166EOF
167
168 # Update Docker environment variables in service .env file
169 log "Adding Docker environment to service .env file"
170 local service_env_file="/mnt/$username/.env"
171
172 if [[ -f "$service_env_file" ]]; then
173 # Remove existing Docker variables and add correct ones
174 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'"
175
176 # Add Docker environment variables using systemd runtime directory
177 sudo -u "$username" bash -c "cat >> '$service_env_file' << 'DOCKER_EOF'
178
179# Docker rootless environment
180XDG_RUNTIME_DIR=$xdg_runtime_dir
181DOCKER_HOST=$docker_host
182PATH=/home/$username/bin:\$PATH
183DOCKER_EOF"
184
185 log "Docker environment added to $service_env_file"
186 else
187 log "WARNING: Service .env file not found at $service_env_file"
188 fi
189}
190
191install_rootless_docker() {
192 local username="$1"
193 local user_uid=$(id -u "$username")
194
195 log "Installing rootless Docker for user: $username"
196
197 # Check if already installed AND working
198 if sudo -u "$username" -i bash -c "command -v docker &>/dev/null && docker version &>/dev/null"; then
199 log "Docker already installed and working for $username"
200 return 0
201 elif sudo -u "$username" -i bash -c "command -v docker &>/dev/null"; then
202 log "Docker client installed but daemon not running for $username"
203 setup_docker_environment "$username"
204 # Continue to start the daemon below
205 else
206 log "Installing Docker client for $username"
207 fi
208
209 # Install rootless Docker with proper environment handling
210 sudo -u "$username" -i bash << EOF
211# Set up environment explicitly
212export XDG_RUNTIME_DIR=/run/user/$user_uid
213export PATH=\$HOME/bin:/usr/bin:\$PATH
214export DOCKER_HOST=unix:///run/user/$user_uid/docker.sock
215
216echo "Installing rootless Docker with environment:"
217echo " XDG_RUNTIME_DIR: \$XDG_RUNTIME_DIR"
218echo " DOCKER_HOST: \$DOCKER_HOST"
219
220# Download and install rootless Docker
221curl -fsSL https://get.docker.com/rootless | sh
222
223# Try systemd setup first
224if systemctl --user status >/dev/null 2>&1; then
225 echo " Setting up systemd service..."
226 \$HOME/bin/dockerd-rootless-setuptool.sh install >/dev/null 2>&1 || echo " Systemd setup failed, using manual start"
227
228 systemctl --user daemon-reload >/dev/null 2>&1
229 systemctl --user enable docker.service >/dev/null 2>&1
230 systemctl --user start docker.service >/dev/null 2>&1
231
232 # Wait for systemd Docker to start
233 for i in {1..5}; do
234 if docker version >/dev/null 2>&1; then
235 echo " ✓ Docker started via systemd"
236 exit 0
237 fi
238 sleep 2
239 done
240 echo " Systemd start failed, falling back to manual..."
241fi
242
243# Manual startup with explicit environment
244echo " Starting Docker daemon manually..."
245# Create .docker directory first
246mkdir -p ~/.docker
247
248# Start with explicit environment and proper logging
249nohup dockerd-rootless.sh > ~/.docker/docker.log 2>&1 &
250DOCKER_PID=\$!
251echo " Docker daemon started with PID: \$DOCKER_PID"
252
253# Wait for manual Docker to start
254for i in {1..15}; do
255 if docker version >/dev/null 2>&1; then
256 echo " ✓ Docker rootless installation successful (manual startup)"
257 echo " Docker daemon PID: \$DOCKER_PID"
258 exit 0
259 fi
260 echo " Waiting for Docker to start... (\$i/15)"
261 if [[ \$i -eq 5 ]] || [[ \$i -eq 10 ]]; then
262 echo " Checking daemon status..."
263 if ! kill -0 \$DOCKER_PID 2>/dev/null; then
264 echo " ❌ Docker daemon died, checking logs:"
265 tail -n 10 ~/.docker/docker.log
266 fi
267 fi
268 sleep 2
269done
270
271echo " ❌ Docker installation failed"
272echo " Docker log:"
273cat ~/.docker/docker.log 2>/dev/null || echo " No log file found"
274exit 1
275EOF
276
277 if [[ $? -eq 0 ]]; then
278 log "Rootless Docker installation completed for $username"
279 else
280 error "Failed to install Docker for $username"
281 fi
282
283 # Enable privileged ports after installation
284 enable_privileged_ports
285
286 # Restart Docker to apply capability changes
287 log "Restarting Docker to apply configuration..."
288 if systemctl_user_safe "$username" "restart docker.service"; then
289 log " Docker service restarted via systemctl"
290 else
291 log " Systemctl restart failed, but Docker should still be functional"
292 fi
293}
294
295configure_docker() {
296 local username="$1"
297
298 log "Configuring Docker for $username..."
299
300 # Create Docker config directory and local data directory
301 sudo -u "$username" mkdir -p "/home/$username/.docker"
302 sudo -u "$username" mkdir -p "/home/$username/.local/share/docker"
303
304 # Create daemon.json with optimized settings
305 # Always disable cgroup management for rootless Docker to avoid systemd delegation issues
306 # Use local data-root to avoid NFS permission issues with XDG_DATA_HOME on NFS
307 # This is the most reliable approach for tinsnip deployments
308 sudo -u "$username" mkdir -p "/home/$username/.local/share/docker"
309 sudo -u "$username" tee "/home/$username/.docker/daemon.json" > /dev/null << EOF
310{
311 "data-root": "/home/$username/.local/share/docker",
312 "log-driver": "json-file",
313 "log-opts": {
314 "max-size": "10m",
315 "max-file": "3"
316 },
317 "storage-driver": "overlay2",
318 "exec-opts": ["native.cgroupdriver=none"]
319}
320EOF
321
322 # Ensure proper ownership of Docker directories
323 sudo -u "$username" chmod 755 "/home/$username/.local/share/docker"
324
325 # Check if Docker is already working before attempting restart
326 log "Checking Docker status before configuration restart..."
327 if sudo -u "$username" -i bash -c "docker version &>/dev/null"; then
328 log " Docker is already working, skipping restart to avoid issues"
329 else
330 log " Docker not responding, attempting restart..."
331 if systemctl_user_safe "$username" "restart docker.service"; then
332 log " Docker service restarted via systemctl"
333 # Give Docker time to start up after restart
334 log "Waiting for Docker to start..."
335 sleep 10
336 else
337 log " Systemctl restart failed, Docker may already be running in non-systemd mode"
338 fi
339 fi
340
341 # Set up Docker environment variables in .env file
342 setup_docker_environment "$username"
343
344 # Create Docker context for rootless socket (before starting daemon)
345 log "Setting up Docker context..."
346 local user_uid
347 user_uid=$(id -u "$username")
348 sudo -u "$username" bash -c "docker context create rootless --docker 'host=unix:///run/user/$user_uid/docker.sock' >/dev/null 2>&1 || true"
349 sudo -u "$username" bash -c "docker context use rootless >/dev/null 2>&1 || true"
350
351 log "Docker configuration complete"
352
353 # Ensure Docker daemon is running after configuration
354 ensure_docker_running "$username"
355}
356
357ensure_docker_running() {
358 local username="$1"
359 local user_uid=$(id -u "$username")
360
361 log "Ensuring Docker daemon is running for $username..."
362
363 # Check if Docker is already responding
364 if sudo -u "$username" -i bash -c "docker version &>/dev/null"; then
365 log " ✓ Docker daemon already running"
366 return 0
367 fi
368
369 log " Docker not responding, starting daemon..."
370
371 # Try systemd first
372 if systemctl_user_safe "$username" "start docker.service"; then
373 log " Started via systemd"
374 # Wait for systemd start
375 for i in {1..10}; do
376 if sudo -u "$username" -i bash -c "docker version &>/dev/null"; then
377 log " ✓ Docker daemon started successfully"
378 return 0
379 fi
380 sleep 2
381 done
382 fi
383
384 # Fall back to manual start
385 log " Starting manually with explicit environment..."
386 sudo -u "$username" -i bash << EOF
387# Kill any existing dockerd processes for this user first
388pkill -f "dockerd-rootless" 2>/dev/null || true
389sleep 2
390
391# Explicitly set environment variables that dockerd-rootless.sh needs
392export XDG_RUNTIME_DIR="/run/user/$user_uid"
393export DOCKER_HOST="unix:///run/user/$user_uid/docker.sock"
394export PATH="/home/$username/bin:\$PATH"
395
396# Clear any environment variables that might override daemon.json
397unset DOCKER_ROOT DOCKER_DATA_ROOT
398
399echo "Environment check:"
400echo " XDG_RUNTIME_DIR: '\$XDG_RUNTIME_DIR'"
401echo " DOCKER_HOST: '\$DOCKER_HOST'"
402echo " Directory exists: \$(test -d "\$XDG_RUNTIME_DIR" && echo "yes" || echo "no")"
403echo " Directory writable: \$(test -w "\$XDG_RUNTIME_DIR" && echo "yes" || echo "no")"
404
405# Start Docker daemon with explicit environment
406mkdir -p ~/.docker
407nohup 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 &
408DOCKER_PID=\$!
409echo "Docker daemon started with PID: \$DOCKER_PID"
410
411# Wait for Docker to be ready
412for i in {1..15}; do
413 if XDG_RUNTIME_DIR="/run/user/$user_uid" DOCKER_HOST="unix:///run/user/$user_uid/docker.sock" docker version >/dev/null 2>&1; then
414 echo "✓ Docker daemon ready"
415 exit 0
416 fi
417 echo " Waiting for Docker... (\$i/15)"
418 sleep 2
419done
420
421echo "❌ Failed to start Docker daemon"
422echo "Docker log:"
423tail -n 20 ~/.docker/docker.log 2>/dev/null || echo "No log file found"
424exit 1
425EOF
426
427 if [[ $? -eq 0 ]]; then
428 log " ✓ Docker daemon started manually"
429 else
430 log " ❌ Failed to start Docker daemon"
431 return 1
432 fi
433}
434
435verify_installation() {
436 local username="$1"
437
438 log "Verifying Docker installation for $username..."
439
440 # Use the service .env file (source of truth)
441 local service_env_file="/mnt/$username/.env"
442
443 # Docker context already set up in configure_docker()
444
445 # Debug verification process
446 log "Debugging verification process..."
447 log " Service env file: $service_env_file"
448
449 if [[ -f "$service_env_file" ]]; then
450 log " Environment variables in service .env:"
451 sudo -u "$username" grep "DOCKER\|XDG_RUNTIME" "$service_env_file" | while read line; do
452 log " $line"
453 done
454 else
455 log " WARNING: Service .env file not found!"
456 fi
457
458 # Test Docker verification with detailed output
459 log " Testing Docker command with environment..."
460 if sudo -u "$username" bash -c "source '$service_env_file' 2>/dev/null && docker version"; then
461 log "Docker verification successful!"
462 local docker_version
463 docker_version=$(sudo -u "$username" bash -c "source '$service_env_file' && docker --version")
464 log "Installed: $docker_version"
465 else
466 log "Docker verification failed - showing detailed error:"
467 sudo -u "$username" bash -c "source '$service_env_file' 2>/dev/null && docker version" 2>&1 | while read line; do
468 log " ERROR: $line"
469 done
470 error "Docker verification failed for $username"
471 fi
472
473 log "Service available for user: $username"
474 log "Privileged ports: enabled"
475}
476
477main() {
478 # Validate parameters
479 if [[ -z "$SERVICE_USER" ]]; then
480 usage
481 fi
482
483 # Check if user exists
484 if ! id "$SERVICE_USER" &>/dev/null; then
485 error "User $SERVICE_USER does not exist. Create the user first."
486 fi
487
488 log "Installing rootless Docker for service user: $SERVICE_USER"
489
490 # Install dependencies
491 install_dependencies
492
493 # Initialize systemd user session (handles lingering and XDG_RUNTIME_DIR)
494 init_systemd_user_session "$SERVICE_USER"
495
496 # Configure Docker (before installation to set up proper data directory)
497 configure_docker "$SERVICE_USER"
498
499 # Install rootless Docker for the service user
500 install_rootless_docker "$SERVICE_USER"
501
502 # Verify installation
503 verify_installation "$SERVICE_USER"
504
505 log ""
506 log "Rootless Docker installation complete!"
507 log "User: $SERVICE_USER"
508 log "To test: sudo -u $SERVICE_USER docker run hello-world"
509}
510
511main "$@"