homelab infrastructure services
1#!/bin/bash
2# Docker installation and management functions for tinsnip
3
4# Source core functions
5LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6source "$LIB_DIR/core.sh"
7
8# Helper function to run systemctl with fallback if systemd not available
9systemctl_user_safe() {
10 local username="$1"
11 shift
12 local systemctl_command="$*"
13
14 sudo -u "$username" -i bash << EOF
15 if systemctl --user status >/dev/null 2>&1; then
16 systemctl --user $systemctl_command
17 exit 0
18 else
19 exit 1
20 fi
21EOF
22
23 local result=$?
24 if [[ $result -eq 1 ]]; then
25 log_with_prefix "Docker Setup" "⚠️ Systemd not available, skipping: systemctl --user $systemctl_command"
26 fi
27
28 return $result
29}
30
31# Install Docker for a specific service user (rootless)
32install_docker_for_user() {
33 local service_user="$1"
34
35 if [[ -z "$service_user" ]]; then
36 error_with_prefix "Docker Setup" "Service user parameter required"
37 fi
38
39 log_with_prefix "Docker Setup" "Installing rootless Docker for user: $service_user"
40
41 # Check if user exists
42 if ! id "$service_user" &>/dev/null; then
43 error_with_prefix "Docker Setup" "User $service_user does not exist"
44 fi
45
46 # Install system Docker if not present (needed for rootless setup)
47 if ! command -v docker >/dev/null 2>&1; then
48 log_with_prefix "Docker Setup" "Installing system Docker..."
49
50 # Add Docker's official GPG key
51 sudo apt-get update -qq
52 sudo apt-get install -y ca-certificates curl gnupg
53 sudo install -m 0755 -d /etc/apt/keyrings
54 curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
55 sudo chmod a+r /etc/apt/keyrings/docker.gpg
56
57 # Add the repository to Apt sources
58 echo "deb [arch=\"$(dpkg --print-architecture)\" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo \"$VERSION_CODENAME\") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
59
60 sudo apt-get update -qq
61 sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
62
63 log_with_prefix "Docker Setup" "System Docker installed"
64 else
65 log_with_prefix "Docker Setup" "System Docker already installed"
66 fi
67
68 # Install rootless Docker for the service user
69 log_with_prefix "Docker Setup" "Setting up rootless Docker for $service_user..."
70
71 # Enable lingering for the user (allows user services to run without login)
72 sudo loginctl enable-linger "$service_user" 2>/dev/null || true
73
74 # Install rootless Docker as the service user
75 sudo -u "$service_user" -i bash << 'EOF'
76 # Check if rootless Docker is already installed
77 if [[ -f "$HOME/bin/docker" ]] && "$HOME/bin/docker" --version >/dev/null 2>&1; then
78 echo "Rootless Docker already installed for $(whoami)"
79 exit 0
80 fi
81
82 # Download and install rootless Docker
83 # Pin to version 26.1.4 to avoid Docker 27.0+ regression with rootless mode
84 # See: https://github.com/moby/moby/issues/48064
85
86 # Manual install to pin specific version
87 DOCKER_VERSION="26.1.4"
88 ARCH=$(uname -m)
89
90 # Download Docker binaries
91 mkdir -p "$HOME/bin"
92 cd /tmp
93 curl -fsSL "https://download.docker.com/linux/static/stable/${ARCH}/docker-${DOCKER_VERSION}.tgz" -o docker.tgz
94 tar xzf docker.tgz --strip-components=1 -C "$HOME/bin" docker/docker docker/dockerd docker/docker-init docker/docker-proxy
95 rm docker.tgz
96
97 # Download rootless extras
98 curl -fsSL "https://download.docker.com/linux/static/stable/${ARCH}/docker-rootless-extras-${DOCKER_VERSION}.tgz" -o docker-rootless-extras.tgz
99 tar xzf docker-rootless-extras.tgz --strip-components=1 -C "$HOME/bin"
100 rm docker-rootless-extras.tgz
101
102 chmod +x "$HOME/bin/"*
103
104 # Run rootless setup tool
105 export SKIP_IPTABLES=1
106 "$HOME/bin/dockerd-rootless-setuptool.sh" install --skip-iptables
107
108 # Add Docker bindir to PATH in shell configs
109 grep -q 'export PATH="$HOME/bin:$PATH"' ~/.bashrc || echo 'export PATH="$HOME/bin:$PATH"' >> ~/.bashrc
110 grep -q 'export PATH="$HOME/bin:$PATH"' ~/.profile || echo 'export PATH="$HOME/bin:$PATH"' >> ~/.profile
111
112 # Set up Docker environment variables
113 grep -q 'export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock' ~/.bashrc || echo 'export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock' >> ~/.bashrc
114 grep -q 'export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock' ~/.profile || echo 'export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock' >> ~/.profile
115EOF
116
117 # Start rootless Docker daemon
118 log_with_prefix "Docker Setup" "Starting rootless Docker daemon..."
119 if systemctl_user_safe "$service_user" "start docker"; then
120 log_with_prefix "Docker Setup" "Docker daemon started"
121
122 # Enable Docker to start on boot
123 if systemctl_user_safe "$service_user" "enable docker"; then
124 log_with_prefix "Docker Setup" "Docker daemon enabled for auto-start"
125 fi
126 else
127 warn_with_prefix "Docker Setup" "Could not start Docker daemon via systemctl"
128 fi
129
130 # Verify installation
131 log_with_prefix "Docker Setup" "Verifying Docker installation..."
132 if sudo -u "$service_user" -i bash -c 'export PATH="$HOME/bin:$PATH" && docker --version' >/dev/null 2>&1; then
133 log_with_prefix "Docker Setup" "✅ Docker installation verified"
134
135 # Configure Docker to use local disk (not NFS) for data storage
136 configure_docker_data_root "$service_user"
137
138 # Setup environment integration (.env file)
139 setup_docker_environment "$service_user"
140
141 # Ensure Docker daemon is running
142 ensure_docker_running "$service_user"
143
144 # Test with hello-world (optional, may fail if daemon not ready)
145 sudo -u "$service_user" -i bash -c 'export PATH="$HOME/bin:$PATH" && docker run --rm hello-world' >/dev/null 2>&1 && \
146 log_with_prefix "Docker Setup" "✅ Docker test container ran successfully" || \
147 warn_with_prefix "Docker Setup" "Docker test container failed (daemon may need time to start)"
148 else
149 error_with_prefix "Docker Setup" "Docker installation verification failed"
150 return 1
151 fi
152
153 log_with_prefix "Docker Setup" "Rootless Docker installation completed for $service_user"
154}
155
156# Setup Docker environment variables and machine.env file integration
157setup_docker_environment() {
158 local username="$1"
159
160 log_with_prefix "Docker Setup" "Setting up Docker environment for $username"
161
162 # Find machine.env via .machine symlink (created by metadata setup)
163 local machine_env_file="/mnt/$username/.machine/machine.env"
164
165 if [[ ! -f "$machine_env_file" ]]; then
166 warn_with_prefix "Docker Setup" "Machine environment file not found at $machine_env_file"
167 warn_with_prefix "Docker Setup" "Docker environment will not be persistent across hosts"
168 return 1
169 fi
170
171 # Detect runtime directory based on systemd availability
172 local user_uid
173 user_uid=$(id -u "$username")
174 local xdg_runtime_dir
175 local docker_host
176
177 # Check if systemd is available on the system (not user session, which may not exist yet)
178 if systemctl --version >/dev/null 2>&1 && [[ -d /run/systemd/system ]]; then
179 # Systemd available - Docker will use standard runtime directory when it runs
180 xdg_runtime_dir="/run/user/$user_uid"
181 docker_host="unix:///run/user/$user_uid/docker.sock"
182 log_with_prefix "Docker Setup" "Systemd detected - using standard runtime directory: $xdg_runtime_dir"
183 else
184 # No systemd - use home directory (rootless Docker fallback)
185 xdg_runtime_dir="/home/$username/.docker/run"
186 docker_host="unix:///home/$username/.docker/run/docker.sock"
187 log_with_prefix "Docker Setup" "Systemd not detected - using home directory: $xdg_runtime_dir"
188 fi
189
190 # Update Docker environment variables in machine.env file (as station-prod)
191 log_with_prefix "Docker Setup" "Adding Docker environment to machine.env"
192
193 # Remove existing Docker variables and add correct ones
194 sudo -u station-prod bash -c "grep -v '^XDG_RUNTIME_DIR=\|^DOCKER_HOST=\|^PATH=\|^# Docker' '$machine_env_file' > '${machine_env_file}.tmp' 2>/dev/null && mv '${machine_env_file}.tmp' '$machine_env_file'"
195
196 # Add Docker environment variables
197 sudo -u station-prod tee -a "$machine_env_file" > /dev/null << DOCKER_EOF
198
199# Docker Environment (added after Docker installation)
200DOCKER_HOST=$docker_host
201PATH=/home/$username/.local/bin:/usr/local/bin:/usr/bin:/bin
202DOCKER_EOF
203
204 log_with_prefix "Docker Setup" "Docker environment added to $machine_env_file"
205 return 0
206}
207
208# Configure Docker daemon to use local disk (not NFS) for data storage
209configure_docker_data_root() {
210 local username="$1"
211
212 log_with_prefix "Docker Setup" "Configuring Docker data root for $username"
213
214 # Create daemon config directory
215 sudo -u "$username" mkdir -p "/home/$username/.config/docker"
216
217 # Set data-root to local disk to avoid NFS permission issues
218 # See: https://github.com/moby/moby/issues/47962
219 # When systemd is not available, use cgroupfs driver
220 local config_file="/home/$username/.config/docker/daemon.json"
221
222 cat << 'EOF' | sudo -u "$username" tee "$config_file" > /dev/null
223{
224 "data-root": "/home/$USER/.local/share/docker",
225 "storage-driver": "fuse-overlayfs",
226 "exec-opts": ["native.cgroupdriver=cgroupfs"]
227}
228EOF
229
230 # Expand $USER variable in the config file
231 sudo -u "$username" bash -c "sed -i 's|\$USER|$username|g' '$config_file'"
232
233 log_with_prefix "Docker Setup" "Docker configured to use local disk: /home/$username/.local/share/docker"
234}
235
236# Ensure Docker daemon is running for the user
237ensure_docker_running() {
238 local username="$1"
239
240 log_with_prefix "Docker Setup" "Ensuring Docker daemon is running for $username"
241
242 local user_uid
243 user_uid=$(id -u "$username")
244 local xdg_runtime_dir
245 local docker_host
246
247 # Check if systemd is available on the system (not user session, which may not exist yet)
248 if systemctl --version >/dev/null 2>&1 && [[ -d /run/systemd/system ]]; then
249 # Systemd available - Docker will use standard runtime directory when it runs
250 xdg_runtime_dir="/run/user/$user_uid"
251 docker_host="unix:///run/user/$user_uid/docker.sock"
252 else
253 # No systemd - use home directory (rootless Docker fallback)
254 xdg_runtime_dir="/home/$username/.docker/run"
255 docker_host="unix:///home/$username/.docker/run/docker.sock"
256 fi
257
258 # Start Docker daemon if not running
259 if ! sudo -u "$username" -i bash -c "export XDG_RUNTIME_DIR='$xdg_runtime_dir' && export DOCKER_HOST='$docker_host' && docker version &>/dev/null"; then
260 log_with_prefix "Docker Setup" "Starting Docker daemon for $username..."
261
262 sudo -u "$username" -i bash << EOF
263# Check if this user's dockerd is already running
264if ! pgrep -u "$user_uid" -x dockerd >/dev/null 2>&1; then
265 mkdir -p ~/.docker
266 # Unset data-related environment variables to prevent Docker from using NFS
267 # Pass data-root and cgroup settings explicitly via command line
268 nohup env -u DOCKER_ROOT -u DOCKER_DATA_ROOT -u XDG_DATA_HOME \
269 XDG_RUNTIME_DIR="$xdg_runtime_dir" \
270 DOCKER_HOST="$docker_host" \
271 dockerd-rootless.sh \
272 --data-root="/home/$username/.local/share/docker" \
273 --exec-opt native.cgroupdriver=cgroupfs \
274 > ~/.docker/docker.log 2>&1 &
275 sleep 5
276else
277 echo "Docker daemon already running for $username"
278fi
279EOF
280
281 # Verify it started
282 if sudo -u "$username" -i bash -c "export XDG_RUNTIME_DIR='$xdg_runtime_dir' && export DOCKER_HOST='$docker_host' && docker version &>/dev/null"; then
283 log_with_prefix "Docker Setup" "✅ Docker daemon started successfully"
284 else
285 warn_with_prefix "Docker Setup" "Docker daemon may need more time to start"
286 fi
287 else
288 log_with_prefix "Docker Setup" "✅ Docker daemon already running"
289 fi
290}