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
51install_dependencies() {
52 log "Installing rootless Docker dependencies..."
53
54 if sudo apt-get update -qq >/dev/null 2>&1; then
55 log " Package lists updated"
56 else
57 error "Failed to update package lists"
58 fi
59
60 if sudo apt-get install -y uidmap dbus-user-session systemd-container fuse-overlayfs slirp4netns >/dev/null 2>&1; then
61 log " Docker dependencies installed"
62 else
63 error "Failed to install Docker dependencies"
64 fi
65}
66
67enable_privileged_ports() {
68 log "Enabling privileged port binding for rootless Docker..."
69
70 # Find rootlesskit binary path
71 local rootlesskit_path
72 if rootlesskit_path=$(which rootlesskit 2>/dev/null); then
73 log "Setting CAP_NET_BIND_SERVICE on rootlesskit..."
74 sudo setcap cap_net_bind_service=ep "$rootlesskit_path"
75 log "Privileged ports enabled"
76 else
77 log "WARNING: rootlesskit not found, will enable after Docker installation"
78 fi
79}
80
81setup_docker_environment() {
82 local username="$1"
83
84 log "Setting up Docker environment for $username"
85
86 # Find the service .env file (should exist from mount_nas.sh)
87 local service_env_file="/mnt/$username/.env"
88
89 if [[ ! -f "$service_env_file" ]]; then
90 log "WARNING: Service .env file not found at $service_env_file"
91 log "Docker environment will not be persistent across hosts"
92 return 1
93 fi
94
95 # Detect if we need systemd or manual setup
96 if sudo -u "$username" -i bash -c "systemctl --user status >/dev/null 2>&1"; then
97 # Systemd environment
98 local xdg_runtime="/run/user/\$(id -u)"
99 local docker_host="unix:///run/user/\$(id -u)/docker.sock"
100
101 # Start Docker daemon if not running
102 sudo -u "$username" -i bash << 'EOF'
103if ! pgrep -x dockerd >/dev/null; then
104 systemctl --user start docker.service
105fi
106EOF
107 else
108 # Manual/non-systemd environment
109 local xdg_runtime="/home/$username/.docker/run"
110 local docker_host="unix:///home/$username/.docker/run/docker.sock"
111
112 # Create runtime directory first
113 sudo -u "$username" -i bash -c 'mkdir -p "$HOME/.docker/run"'
114
115 # Start Docker daemon if not running (non-systemd)
116 sudo -u "$username" -i bash << 'EOF'
117if ! pgrep -x dockerd >/dev/null; then
118 export XDG_RUNTIME_DIR="$HOME/.docker/run"
119 export DOCKER_HOST="unix://$XDG_RUNTIME_DIR/docker.sock"
120 dockerd-rootless.sh > ~/.docker/docker.log 2>&1 &
121 sleep 5
122fi
123EOF
124 fi
125
126 # Append Docker environment variables to service .env file
127 log "Adding Docker environment to service .env file"
128 sudo -u "$username" -i bash << EOF
129# Remove any existing Docker environment variables from .env
130grep -v "^XDG_RUNTIME_DIR=\|^DOCKER_HOST=\|^PATH=" "$service_env_file" > "${service_env_file}.tmp" && mv "${service_env_file}.tmp" "$service_env_file"
131
132# Append Docker environment variables
133cat >> "$service_env_file" << DOCKER_EOF
134
135# Docker rootless environment
136XDG_RUNTIME_DIR=$xdg_runtime
137DOCKER_HOST=$docker_host
138PATH=/home/$username/bin:\$PATH
139DOCKER_EOF
140EOF
141
142 log "Docker environment added to $service_env_file"
143}
144
145install_rootless_docker() {
146 local username="$1"
147
148 log "Installing rootless Docker for user: $username"
149
150 # Lingering is handled in main function
151
152 # Check if already installed
153 if sudo -u "$username" -i bash -c "command -v docker &>/dev/null"; then
154 log "Docker already installed for $username"
155 # Still need to ensure environment is set up
156 setup_docker_environment "$username"
157 return 0
158 fi
159
160 # Install rootless Docker
161 sudo -u "$username" -i bash << 'EOF'
162# Set up environment
163export XDG_RUNTIME_DIR=/run/user/$(id -u)
164
165# Download and install rootless Docker
166curl -fsSL https://get.docker.com/rootless | sh
167
168# Add Docker to PATH (insert at top to avoid interactive shell return)
169add_to_bashrc_top() {
170 local var="$1"
171 if ! grep -q "^$var" ~/.bashrc 2>/dev/null; then
172 echo "$var" | cat - ~/.bashrc > ~/.bashrc.tmp && mv ~/.bashrc.tmp ~/.bashrc
173 fi
174}
175
176add_to_bashrc_top 'export PATH=$HOME/bin:$PATH'
177add_to_bashrc_top 'export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock'
178add_to_bashrc_top 'export XDG_RUNTIME_DIR=/run/user/$(id -u)'
179
180# Source the new PATH
181source ~/.bashrc
182
183# Enable systemd user session if needed
184if ! systemctl --user status >/dev/null 2>&1; then
185 echo " ⚠️ Systemd user session not available, using manual start..."
186 # Set up for manual Docker start (insert at top to avoid interactive shell return)
187 add_to_bashrc_top 'export XDG_RUNTIME_DIR="$HOME/.docker/run"'
188 add_to_bashrc_top 'export DOCKER_HOST="unix://$HOME/.docker/run/docker.sock"'
189
190 # Create XDG_RUNTIME_DIR
191 mkdir -p "$HOME/.docker/run"
192 export XDG_RUNTIME_DIR="$HOME/.docker/run"
193 export DOCKER_HOST="unix://$XDG_RUNTIME_DIR/docker.sock"
194
195 # Start Docker manually
196 dockerd-rootless.sh &
197 sleep 5
198else
199 # Systemd is available
200 loginctl enable-linger $(whoami)
201 systemctl --user enable docker.service
202 systemctl --user start docker.service
203fi
204
205# Wait for Docker to start
206sleep 5
207
208# Test Docker
209if docker version &>/dev/null; then
210 echo " Docker rootless installation successful"
211else
212 echo " ❌ Docker installation failed"
213 exit 1
214fi
215EOF
216
217 if [[ $? -eq 0 ]]; then
218 log "Rootless Docker installation completed for $username"
219 else
220 error "Failed to install Docker for $username"
221 fi
222
223 # Enable privileged ports after installation
224 enable_privileged_ports
225
226 # Restart Docker to apply capability changes
227 log "Restarting Docker to apply configuration..."
228 if systemctl_user_safe "$username" "restart docker.service"; then
229 log " Docker service restarted via systemctl"
230 else
231 log " Systemctl restart failed, but Docker should still be functional"
232 fi
233}
234
235configure_docker() {
236 local username="$1"
237
238 log "Configuring Docker for $username..."
239
240 # Create Docker config directory
241 sudo -u "$username" mkdir -p "/home/$username/.docker"
242
243 # Create daemon.json with optimized settings
244 # Always disable cgroup management for rootless Docker to avoid systemd delegation issues
245 # This is the most reliable approach for tinsnip deployments
246 sudo -u "$username" tee "/home/$username/.docker/daemon.json" > /dev/null << 'EOF'
247{
248 "log-driver": "json-file",
249 "log-opts": {
250 "max-size": "10m",
251 "max-file": "3"
252 },
253 "storage-driver": "overlay2",
254 "exec-opts": ["native.cgroupdriver=none"]
255}
256EOF
257
258 # Check if Docker is already working before attempting restart
259 log "Checking Docker status before configuration restart..."
260 if sudo -u "$username" -i bash -c "docker version &>/dev/null"; then
261 log " Docker is already working, skipping restart to avoid issues"
262 else
263 log " Docker not responding, attempting restart..."
264 if systemctl_user_safe "$username" "restart docker.service"; then
265 log " Docker service restarted via systemctl"
266 # Give Docker time to start up after restart
267 log "Waiting for Docker to start..."
268 sleep 10
269 else
270 log " Systemctl restart failed, Docker may already be running in non-systemd mode"
271 fi
272 fi
273
274 log "Docker configuration complete"
275}
276
277verify_installation() {
278 local username="$1"
279
280 log "Verifying Docker installation for $username..."
281
282 # Use the service .env file (source of truth)
283 local service_env_file="/mnt/$username/.env"
284
285 if sudo -u "$username" bash -c "source '$service_env_file' && docker version &>/dev/null"; then
286 log "Docker verification successful!"
287 local docker_version
288 docker_version=$(sudo -u "$username" bash -c "source '$service_env_file' && docker --version")
289 log "Installed: $docker_version"
290 else
291 error "Docker verification failed for $username"
292 fi
293
294 log "Service available for user: $username"
295 log "Privileged ports: enabled"
296}
297
298main() {
299 # Validate parameters
300 if [[ -z "$SERVICE_USER" ]]; then
301 usage
302 fi
303
304 # Check if user exists
305 if ! id "$SERVICE_USER" &>/dev/null; then
306 error "User $SERVICE_USER does not exist. Create the user first."
307 fi
308
309 log "Installing rootless Docker for service user: $SERVICE_USER"
310
311 # Install dependencies
312 install_dependencies
313
314 # Enable systemd lingering for the service user
315 log "Enabling systemd lingering for $SERVICE_USER"
316 if sudo loginctl enable-linger "$SERVICE_USER" 2>/dev/null; then
317 log " Systemd lingering enabled"
318 else
319 log " ⚠️ Could not enable systemd lingering (may not be supported)"
320 fi
321
322 # Install rootless Docker for the service user
323 install_rootless_docker "$SERVICE_USER"
324
325 # Configure Docker
326 configure_docker "$SERVICE_USER"
327
328 # Verify installation
329 verify_installation "$SERVICE_USER"
330
331 log ""
332 log "Rootless Docker installation complete!"
333 log "User: $SERVICE_USER"
334 log "To test: sudo -u $SERVICE_USER docker run hello-world"
335}
336
337main "$@"