/
/
/
1#!/bin/bash
2# ==============================================================================
3# Docker Update Script
4# ==============================================================================
5#
6# Description: Update Docker containers and images with comprehensive management
7# Usage: ./docker-update.sh [all|service] [check|pull|update|cleanup|rollback]
8#
9# This script is automatically generated by Ansible - DO NOT EDIT MANUALLY
10# Template: docker-update.sh.j2
11#
12# ==============================================================================
13
14set -euo pipefail
15
16# Configuration
17DOCKER_COMPOSE_DIR="{{ docker_base_path }}"
18LOG_FILE="/var/log/docker-update.log"
19BACKUP_DIR="{{ docker_base_path }}/backups"
20UPDATE_TIMEOUT=300
21
22# Service order (dependency order)
23SERVICES=(
24 "{{ unbound_service_name }}"
25 "{{ pihole_service_name }}"
26 "{{ nginx_proxy_db_service_name }}"
27 "{{ nginx_proxy_service_name }}"
28 "{{ wireguard_service_name }}"
29)
30
31# Logging function
32log() {
33 echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "${LOG_FILE}"
34}
35
36# Error handling function
37error_exit() {
38 log "ERROR: $1"
39 exit 1
40}
41
42# Check if container exists
43container_exists() {
44 docker inspect "$1" >/dev/null 2>&1
45}
46
47# Check if container is running
48container_running() {
49 docker inspect -f '{{.State.Running}}' "$1" 2>/dev/null | grep -q "true"
50}
51
52# Get current image information
53get_image_info() {
54 local service="$1"
55 docker inspect -f '{{.Config.Image}}|{{.Image}}' "${service}" 2>/dev/null || echo "N/A|N/A"
56}
57
58# Create backup before update
59create_backup() {
60 local service="$1"
61 local backup_file="${BACKUP_DIR}/${service}-backup-$(date +%Y%m%d-%H%M%S).tar.gz"
62
63 log "Creating backup for ${service}"
64
65 mkdir -p "${BACKUP_DIR}"
66
67 # Backup container configuration and data
68 local compose_file="${DOCKER_COMPOSE_DIR}/${service}/docker-compose.yml"
69 local env_file="${DOCKER_COMPOSE_DIR}/${service}/.env"
70
71 tar -czf "${backup_file}" \
72 "${compose_file}" \
73 "${env_file}" \
74 "${DOCKER_COMPOSE_DIR}/${service}/config" \
75 "${DOCKER_COMPOSE_DIR}/${service}/data" \
76 2>/dev/null || true
77
78 if [[ -f "${backup_file}" ]]; then
79 log "Backup created: ${backup_file} ($(du -h "${backup_file}" | cut -f1))"
80 echo "${backup_file}"
81 else
82 log "WARNING: Failed to create backup for ${service}"
83 echo ""
84 fi
85}
86
87# Check for available updates
88check_updates() {
89 local service="${1:-all}"
90
91 log "Checking for available updates"
92
93 local services_to_check=()
94
95 if [[ "${service}" == "all" ]]; then
96 services_to_check=("${SERVICES[@]}")
97 else
98 services_to_check=("${service}")
99 fi
100
101 local update_count=0
102
103 for svc in "${services_to_check[@]}"; do
104 if ! container_exists "${svc}"; then
105 log "${svc}: Container does not exist"
106 continue
107 fi
108
109 local current_image=$(docker inspect -f '{{.Config.Image}}' "${svc}" 2>/dev/null)
110 if [[ -z "${current_image}" ]]; then
111 log "${svc}: Cannot determine current image"
112 continue
113 fi
114
115 log "${svc}: Checking updates for ${current_image}"
116
117 # Pull latest image to check for updates
118 docker pull "${current_image}" >/dev/null 2>&1 || true
119
120 local current_id=$(docker inspect -f '{{.Image}}' "${svc}" 2>/dev/null)
121 local latest_id=$(docker inspect -f '{{.Id}}' "${current_image}" 2>/dev/null)
122
123 if [[ "${current_id}" != "${latest_id}" ]]; then
124 log "${svc}: UPDATE AVAILABLE"
125 update_count=$((update_count + 1))
126
127 # Show version information if available
128 local current_version=$(docker inspect -f '{{.Config.Labels.version}}' "${svc}" 2>/dev/null || echo "unknown")
129 local latest_version=$(docker inspect -f '{{.Config.Labels.version}}' "${current_image}" 2>/dev/null || echo "unknown")
130
131 echo " Current: ${current_version}"
132 echo " Latest: ${latest_version}"
133 echo " Image: ${current_image}"
134 else
135 log "${svc}: Up to date"
136 fi
137 done
138
139 echo ""
140 echo "Update check completed: ${update_count} service(s) have updates available"
141}
142
143# Pull latest images without updating
144pull_images() {
145 local service="${1:-all}"
146
147 log "Pulling latest images"
148
149 local services_to_pull=()
150
151 if [[ "${service}" == "all" ]]; then
152 services_to_pull=("${SERVICES[@]}")
153 else
154 services_to_pull=("${service}")
155 fi
156
157 local pulled_count=0
158 local failed_count=0
159
160 for svc in "${services_to_pull[@]}"; do
161 if ! container_exists "${svc}"; then
162 log "${svc}: Container does not exist, skipping"
163 continue
164 fi
165
166 local current_image=$(docker inspect -f '{{.Config.Image}}' "${svc}" 2>/dev/null)
167 if [[ -z "${current_image}" ]]; then
168 log "${svc}: Cannot determine image, skipping"
169 continue
170 fi
171
172 log "${svc}: Pulling ${current_image}"
173
174 if docker pull "${current_image}"; then
175 log "${svc}: Image pulled successfully"
176 pulled_count=$((pulled_count + 1))
177 else
178 log "${svc}: Failed to pull image"
179 failed_count=$((failed_count + 1))
180 fi
181 done
182
183 echo ""
184 echo "Pull completed: ${pulled_count} successful, ${failed_count} failed"
185}
186
187# Update a specific service
188update_service() {
189 local service="$1"
190 local force="${2:-false}"
191
192 if ! container_exists "${service}"; then
193 error_exit "Container does not exist: ${service}"
194 fi
195
196 log "Starting update for ${service}"
197
198 # Create backup
199 local backup_file=$(create_backup "${service}")
200
201 # Get current state
202 local was_running=false
203 if container_running "${service}"; then
204 was_running=true
205 log "${service} is running, stopping for update"
206
207 # Stop the service
208 docker-compose -f "${DOCKER_COMPOSE_DIR}/${service}/docker-compose.yml" down --timeout 30 2>/dev/null || true
209 sleep 2
210 fi
211
212 # Pull latest image
213 local current_image=$(docker inspect -f '{{.Config.Image}}' "${service}" 2>/dev/null)
214 if [[ -n "${current_image}" ]]; then
215 log "Pulling latest image: ${current_image}"
216 docker pull "${current_image}" || log "WARNING: Failed to pull image"
217 fi
218
219 # Update the service
220 local compose_file="${DOCKER_COMPOSE_DIR}/${service}/docker-compose.yml"
221
222 if [[ -f "${compose_file}" ]]; then
223 log "Updating ${service} using compose file"
224
225 if [[ "${force}" == "true" ]]; then
226 docker-compose -f "${compose_file}" up -d --force-recreate --remove-orphans 2>/dev/null || true
227 else
228 docker-compose -f "${compose_file}" up -d 2>/dev/null || true
229 fi
230 else
231 log "WARNING: Compose file not found, recreating container"
232
233 # Fallback: recreate container with same configuration
234 local container_config=$(docker inspect "${service}" 2>/dev/null)
235 if [[ -n "${container_config}" ]]; then
236 docker rm -f "${service}" 2>/dev/null || true
237 sleep 2
238
239 # TODO: Implement container recreation from inspect data
240 log "ERROR: Automatic container recreation not implemented"
241 return 1
242 fi
243 fi
244
245 # Wait for service to start
246 if [[ "${was_running}" == "true" ]]; then
247 log "Waiting for ${service} to start"
248 sleep 5
249
250 if container_running "${service}"; then
251 log "${service} started successfully"
252 else
253 log "WARNING: ${service} failed to start after update"
254 fi
255 fi
256
257 # Verify update
258 local new_image=$(docker inspect -f '{{.Config.Image}}' "${service}" 2>/dev/null)
259 local new_id=$(docker inspect -f '{{.Image}}' "${service}" 2>/dev/null)
260 local old_id=$(docker inspect -f '{{.Id}}' "${current_image}" 2>/dev/null)
261
262 if [[ "${new_id}" != "${old_id}" ]]; then
263 log "${service}: Update successful - image changed"
264 else
265 log "${service}: No image change detected"
266 fi
267
268 echo "Backup: ${backup_file}"
269}
270
271# Update all services
272update_all_services() {
273 local force="${1:-false}"
274
275 log "Starting update of all services"
276
277 local updated_count=0
278 local failed_count=0
279
280 # Update services in reverse order (shutdown order)
281 for ((i=${#SERVICES[@]}-1; i>=0; i--)); do
282 local service="${SERVICES[i]}"
283
284 if update_service "${service}" "${force}"; then
285 updated_count=$((updated_count + 1))
286 else
287 failed_count=$((failed_count + 1))
288 fi
289
290 # Add delay between updates
291 sleep 5
292 done
293
294 echo ""
295 echo "Update completed: ${updated_count} successful, ${failed_count} failed"
296
297 if [[ ${failed_count} -gt 0 ]]; then
298 return 1
299 fi
300
301 return 0
302}
303
304# Cleanup old images and containers
305cleanup_docker() {
306 local remove_all="${1:-false}"
307
308 log "Cleaning up Docker resources"
309
310 # Remove stopped containers
311 local stopped_containers=$(docker ps -aq -f status=exited | wc -l)
312 if [[ ${stopped_containers} -gt 0 ]]; then
313 log "Removing ${stopped_containers} stopped containers"
314 docker ps -aq -f status=exited | xargs docker rm -v 2>/dev/null || true
315 fi
316
317 # Remove dangling images
318 local dangling_images=$(docker images -q -f dangling=true | wc -l)
319 if [[ ${dangling_images} -gt 0 ]]; then
320 log "Removing ${dangling_images} dangling images"
321 docker images -q -f dangling=true | xargs docker rmi 2>/dev/null || true
322 fi
323
324 # Remove unused volumes
325 local unused_volumes=$(docker volume ls -q -f dangling=true | wc -l)
326 if [[ ${unused_volumes} -gt 0 ]]; then
327 log "Removing ${unused_volumes} unused volumes"
328 docker volume ls -q -f dangling=true | xargs docker volume rm 2>/dev/null || true
329 fi
330
331 # Remove old images if requested
332 if [[ "${remove_all}" == "true" ]]; then
333 local unused_images=$(docker images -q | wc -l)
334 if [[ ${unused_images} -gt 0 ]]; then
335 log "Removing ${unused_images} unused images"
336 docker images -q | xargs docker rmi 2>/dev/null || true
337 fi
338 fi
339
340 # Cleanup builder cache
341 log "Cleaning builder cache"
342 docker builder prune -f 2>/dev/null || true
343
344 log "Cleanup completed"
345}
346
347# Rollback to previous version
348rollback_service() {
349 local service="$1"
350 local backup_file="${2:-}"
351
352 if [[ -z "${backup_file}" ]]; then
353 # Find latest backup
354 backup_file=$(ls -t "${BACKUP_DIR}/${service}-backup-*.tar.gz" 2>/dev/null | head -1)
355
356 if [[ -z "${backup_file}" ]]; then
357 error_exit "No backup found for ${service}"
358 fi
359 fi
360
361 if [[ ! -f "${backup_file}" ]]; then
362 error_exit "Backup file not found: ${backup_file}"
363 fi
364
365 log "Rolling back ${service} using backup: ${backup_file}"
366
367 # Stop the service
368 if container_running "${service}"; then
369 log "Stopping ${service}"
370 docker-compose -f "${DOCKER_COMPOSE_DIR}/${service}/docker-compose.yml" down --timeout 30 2>/dev/null || true
371 fi
372
373 # Remove current container
374 if container_exists "${service}"; then
375 log "Removing current container"
376 docker rm -f "${service}" 2>/dev/null || true
377 fi
378
379 # Extract backup
380 log "Extracting backup"
381 tar -xzf "${backup_file}" -C "/" 2>/dev/null || true
382
383 # Start the service
384 local compose_file="${DOCKER_COMPOSE_DIR}/${service}/docker-compose.yml"
385 if [[ -f "${compose_file}" ]]; then
386 log "Starting service from backup"
387 docker-compose -f "${compose_file}" up -d 2>/dev/null || true
388 else
389 error_exit "Compose file not found after rollback"
390 fi
391
392 log "Rollback completed for ${service}"
393}
394
395# Show usage
396usage() {
397 cat << EOF
398Docker Update Script
399
400Usage: $0 [service|all] [command] [options]
401
402Commands:
403 check Check for available updates
404 pull Pull latest images without updating
405 update Update service(s) to latest version
406 cleanup Cleanup Docker resources
407 rollback Rollback to previous version
408 help Show this help message
409
410Options:
411 --force Force recreation of containers
412 --all-images Remove all unused images during cleanup
413
414Examples:
415 $0 all check
416 $0 {{ nginx_proxy_service_name }} pull
417 $0 {{ pihole_service_name }} update
418 $0 all update --force
419 $0 cleanup --all-images
420 $0 {{ unbound_service_name }} rollback
421 $0 help
422
423Service Order (update order):
424 1. {{ wireguard_service_name }} (VPN)
425 2. {{ nginx_proxy_service_name }} (Reverse proxy)
426 3. {{ nginx_proxy_db_service_name }} (Database)
427 4. {{ pihole_service_name }} (DNS/ad-blocker)
428 5. {{ unbound_service_name }} (DNS resolver)
429EOF
430}
431
432# Main execution
433main() {
434 local service="${1:-all}"
435 local command="${2:-check}"
436 local option="${3:-}"
437
438 case "${command}" in
439 check)
440 check_updates "${service}"
441 ;;
442 pull)
443 pull_images "${service}"
444 ;;
445 update)
446 if [[ "${service}" == "all" ]]; then
447 if [[ "${option}" == "--force" ]]; then
448 update_all_services "true"
449 else
450 update_all_services "false"
451 fi
452 else
453 if [[ "${option}" == "--force" ]]; then
454 update_service "${service}" "true"
455 else
456 update_service "${service}" "false"
457 fi
458 fi
459 ;;
460 cleanup)
461 if [[ "${option}" == "--all-images" ]]; then
462 cleanup_docker "true"
463 else
464 cleanup_docker "false"
465 fi
466 ;;
467 rollback)
468 rollback_service "${service}" "${option}"
469 ;;
470 help|*)
471 usage
472 ;;
473 esac
474}
475
476# Run main function with all arguments
477main "$@"