从零散Docker容器一键生成 docker-compose 编排配置的自动化方案

前言

许多开发者在早期使用Docker时,习惯通过手动执行docker run命令启动容器。随着项目复杂度增加,这种方式的问题逐渐暴露:配置难以复现、迁移成本高、团队协作困难。本文将重点介绍如何通过自动化工具,从现有容器中一键生成规范的docker-compose.yml配置,实现快速迁移和标准化部署。

核心自动化工具

1. docker-autocompose:命令行利器

docker-autocompose是一个专为逆向工程设计的命令行工具,能够从现有容器自动生成docker-compose.yml配置。

基本用法

1
2
3
4
# 分析指定容器并生成配置
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
ghcr.io/red5d/docker-autocompose:latest \
wp-site1 wp-site2 mysql-db

保存到文件

1
2
3
4
# 将输出重定向到文件
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
ghcr.io/red5d/docker-autocompose:latest \
wp-site1 wp-site2 mysql-db > docker-compose.yml

高级特性

  • 支持已停止的容器:工具通过读取Docker引擎的元数据工作,不要求容器处于运行状态
  • 保留所有配置:自动提取端口映射、环境变量、数据卷挂载、网络设置等
  • 多容器分析:可一次性分析多个相互关联的容器

2. 交互式工具:compose-generator

对于偏好图形界面的用户,compose-generator提供了交互式逆向生成体验。

通过Docker运行

1
2
3
docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock \
-v $(pwd):/cg/out \
ghcr.io/compose-generator/compose-generator:latest

工作流程

  1. 启动后选择”From running project”
  2. 在列表中选择要分析的容器
  3. 跟随向导完成配置生成
  4. 生成的docker-compose.yml将输出到当前目录

实战案例:WordPress站点自动化迁移

场景描述

现有三个运行中的容器:两个WordPress站点(wp-site1wp-site2)共用一个MySQL数据库(mysql-db)。

自动化步骤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 第一步:一键生成编排配置
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
ghcr.io/red5d/docker-autocompose:latest \
wp-site1 wp-site2 mysql-db > docker-compose.yml

# 第二步:自动生成环境变量模板
cat > .env << 'EOF'
# 自动生成的配置模板
# 请替换为实际的安全密码
DB_PASSWORD=your_secure_password_here
MYSQL_ROOT_PASSWORD=your_root_password_here
EOF

# 第三步:验证生成的配置
docker-compose config

生成结果示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 自动生成的docker-compose.yml
version: '3.8'
services:
wp-site1:
image: wordpress:latest
ports:
- "8081:80"
environment:
WORDPRESS_DB_HOST: mysql-db
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: ${DB_PASSWORD}
WORDPRESS_DB_NAME: wordpress_site1
volumes:
- wp_site1_data:/var/www/html
networks:
- wp_network
depends_on:
- mysql-db

wp-site2:
image: wordpress:latest
# ... 类似配置
mysql-db:
image: mysql:latest
# ... 数据库配置

进阶:自动化版本号检测与替换

自动化工具生成的配置中,镜像标签通常是latest。为了确保环境一致性,建议固化版本号。

自动化版本检测脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash
# 自动检测并替换latest标签为具体版本
for container in wp-site1 wp-site2 mysql-db; do
image_name=$(docker inspect -f '{{.Config.Image}}' $container)
if [[ "$image_name" == *:latest ]]; then
actual_version=$(docker image inspect $image_name | grep -i version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+|[0-9]+\.[0-9]+' | head -1)
if [ -n "$actual_version" ]; then
# 在docker-compose.yml中替换版本
base_image=${image_name%:*}
sed -i "s|image: $base_image:latest|image: $base_image:$actual_version|g" docker-compose.yml
fi
fi
done

Dockerfile的自动化逆向生成

对于需要重构镜像构建过程的场景,也可以自动化生成Dockerfile。

使用dfimage工具

1
2
3
4
5
6
7
# 一键生成近似的Dockerfile
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
alpine/dfimage -sV=1.36 nginx:latest

# 或使用whaler
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
pegleg/whaler nginx:latest > Dockerfile.approximate

一键式集成脚本

结合上述工具,可以创建一个完整的自动化迁移脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#!/bin/bash
# migrate-docker-to-compose.sh
# 一键从现有容器迁移到编排配置

set -e

echo "🔍 开始Docker容器逆向迁移"

# 1. 使用docker-autocompose生成基础配置
echo "📦 生成docker-compose.yml..."
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
ghcr.io/red5d/docker-autocompose:latest $(docker ps -a --format "{{.Names}}") \
> docker-compose.yml

# 2. 生成环境变量模板
echo "🔐 创建环境变量模板..."
cat > .env.example << 'EOF'
# 自动生成的环境变量配置
# 请根据实际情况修改以下值
EOF

# 3. 生成各服务的Dockerfile(如需要)
echo "🐳 生成服务Dockerfile..."
for service in $(grep -E '^ [a-zA-Z]' docker-compose.yml | sed 's/://'); do
image=$(grep -A5 "^ $service:" docker-compose.yml | grep "image:" | cut -d: -f2- | tr -d ' ')
if [ -n "$image" ]; then
echo "# 为 $service 生成Dockerfile参考"
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
alpine/dfimage -sV=1.36 $image > "Dockerfile.$service" 2>/dev/null || true
fi
done

echo "✅ 迁移完成!"
echo "📁 生成的文件:"
echo " - docker-compose.yml (主编排文件)"
echo " - .env.example (环境变量模板)"
echo " - Dockerfile.* (各服务构建参考)"
echo ""
echo "🚀 使用方式:"
echo " 1. 编辑 .env 文件,设置实际值"
echo " 2. 运行: docker-compose up -d"
echo " 3. 验证: docker-compose ps"

验证与测试

生成配置后,务必进行验证:

1
2
3
4
5
6
7
8
9
10
# 语法验证
docker-compose config

# 测试启动(不实际运行)
docker-compose up --dry-run

# 实际测试运行
docker-compose up -d
docker-compose ps
docker-compose logs

总结

通过自动化工具,原本需要数小时的手动配置工作可以在几分钟内完成。关键工具包括:

  1. docker-autocompose:命令行工具,适合批量处理和脚本集成
  2. compose-generator:交互式工具,适合新手和复杂场景
  3. dfimage/whaler:Dockerfile逆向生成,用于镜像重构

自动化逆向工程不仅提高了迁移效率,更重要的是建立了标准的、可版本控制的部署配置。无论是应对服务器迁移、环境标准化,还是团队协作,这套自动化方案都能显著降低运维成本,提升部署的可靠性

附加

我借助AI生成了一套交互式逆推脚本,它可以将本机所有已创建的容器进行逆推,或者指定具体某几个容器,脚本代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
 #!/bin/bash
# docker-compose-archaeologist-final-fixed.sh
# 最终修复版:修复序号选择和版本检测
# 版本:v2.4-final-fixed

set -e

# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
MAGENTA='\033[0;35m'
CYAN='\033[0;36m'
NC='\033[0m'
BOLD='\033[1m'
UNDERLINE='\033[4m'

# 输出函数
error() { echo -e "${RED}$1${NC}" >&2; }
success() { echo -e "${GREEN}$1${NC}"; }
info() { echo -e "${BLUE}$1${NC}"; }
warning() { echo -e "${YELLOW}$1${NC}"; }
step() { echo -e "\n${CYAN}$1${NC}"; }
header() { echo -e "\n${MAGENTA}▬▬▬▬▬▬▬▬▬▬ $1 ▬▬▬▬▬▬▬▬▬▬${NC}"; }

# 配置
AUTOCOMPOSE_IMAGE="ghcr.io/red5d/docker-autocompose:latest"
OUTPUT_FILE="docker-compose.yml"
TEMP_FILE="docker-compose.tmp.yml"
BACKUP_TIMESTAMP=$(date +%Y%m%d_%H%M%S)

# 打印横幅
print_banner() {
clear
echo -e "${BOLD}${CYAN}"
cat << "EOF"
╔══════════════════════════════════════════════════════════════╗
║ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ║
║ ▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌ ║
║ ▐░█▀▀▀▀▀▀▀▀▀ ▐░█▀▀▀▀▀▀▀█░▌▐░█▀▀▀▀▀▀▀█░▌ ▀▀▀▀█░█▀▀▀▀ ║
║ ▐░▌ ▐░▌ ▐░▌▐░▌ ▐░▌ ▐░▌ ║
║ ▐░█▄▄▄▄▄▄▄▄▄ ▐░█▄▄▄▄▄▄▄█░▌▐░█▄▄▄▄▄▄▄█░▌ ▐░▌ ║
║ ▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌ ▐░▌ ║
║ ▀▀▀▀▀▀▀▀▀█░▌▐░█▀▀▀▀█░█▀▀ ▐░█▀▀▀▀▀▀▀█░▌ ▐░▌ ║
║ ▐░▌▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ║
║ ▄▄▄▄▄▄▄▄▄█░▌▐░▌ ▐░▌ ▐░▌ ▐░▌ ▄▄▄▄█░█▄▄▄▄ ║
║ ▐░░░░░░░░░░░▌▐░▌ ▐░▌▐░▌ ▐░▌▐░░░░░░░░░░░▌ ║
║ ▀▀▀▀▀▀▀▀▀▀▀ ▀ ▀ ▀ ▀ ▀▀▀▀▀▀▀▀▀▀▀ ║
║ ║
║ Docker Compose 考古学家 v2.4 ║
║ 自动逆向生成版本化编排配置 ║
╚══════════════════════════════════════════════════════════════╝
EOF
echo -e "${NC}"

echo -e "${YELLOW}📦 自动发现并迁移 Docker 容器到编排配置${NC}"
echo -e "${YELLOW}🔍 支持版本号自动检测和替换${NC}"
echo ""
}

# 检查是否安装jq
check_jq() {
if command -v jq &> /dev/null; then
HAS_JQ=true
JQ_VERSION=$(jq --version | grep -oE '[0-9]+\.[0-9]+')
success "jq $JQ_VERSION (JSON处理已启用)"
else
HAS_JQ=false
warning "未检测到 jq,使用基础功能 (建议安装: sudo apt install jq)"
fi
echo ""
}

# 获取所有容器
get_all_containers() {
step "扫描 Docker 容器"

if [ "$HAS_JQ" = true ]; then
CONTAINER_DATA=$(docker ps -a --format '{"Names":"{{.Names}}","Image":"{{.Image}}","Status":"{{.Status}}","State":"{{.State}}"}' | jq -s '.')
CONTAINER_COUNT=$(echo "$CONTAINER_DATA" | jq length)
echo "使用jq模式,找到 $CONTAINER_COUNT 个容器"
else
CONTAINER_LIST=$(docker ps -a --format "{{.Names}}\t{{.Image}}\t{{.Status}}")
CONTAINER_COUNT=$(echo "$CONTAINER_LIST" | wc -l | tr -d ' ')
echo "使用基础模式,找到 $CONTAINER_COUNT 个容器"
fi

if [ "$CONTAINER_COUNT" -eq 0 ] || [ -z "$CONTAINER_COUNT" ]; then
error "未发现任何Docker容器"
exit 1
fi

success "发现 $CONTAINER_COUNT 个容器"
}

# 显示容器列表
display_containers() {
header "容器列表"

echo "┌─────┬──────────────────────┬──────────────────────────┬─────────────────┐"
echo "│ 序号 │ 容器名称 │ 镜像 │ 状态 │"
echo "├─────┼──────────────────────┼──────────────────────────┼─────────────────┤"

if [ "$HAS_JQ" = true ]; then
for i in $(seq 0 $((CONTAINER_COUNT-1))); do
name=$(echo "$CONTAINER_DATA" | jq -r ".[$i].Names")
image=$(echo "$CONTAINER_DATA" | jq -r ".[$i].Image")
status=$(echo "$CONTAINER_DATA" | jq -r ".[$i].Status")

name_display=$(printf "%-20s" "$name" | cut -c1-20)
image_display=$(printf "%-24s" "$image" | cut -c1-24)
status_display=$(printf "%-15s" "$status" | cut -c1-15)

printf "│ %3d │ %-20s │ %-24s │ %-15s │\n" \
$((i+1)) "$name_display" "$image_display" "$status_display"
done
else
i=1
while IFS=$'\t' read -r name image status; do
[ -z "$name" ] && continue

name_display=$(printf "%-20s" "$name" | cut -c1-20)
image_display=$(printf "%-24s" "$image" | cut -c1-24)
status_display=$(printf "%-15s" "$status" | cut -c1-15)

printf "│ %3d │ %-20s │ %-24s │ %-15s │\n" \
"$i" "$name_display" "$image_display" "$status_display"
((i++))
done <<< "$CONTAINER_LIST"
fi

echo "└─────┴──────────────────────┴──────────────────────────┴─────────────────┘"
echo ""
}

# 选择模式
select_mode() {
header "选择模式"

echo "请选择操作方式:"
echo ""
echo " 1) 📋 选择所有容器 ($CONTAINER_COUNT 个)"
echo " 2) 🔢 选择特定容器 (支持单个、多个、范围选择)"
echo " 3) 🎯 按状态筛选 (运行中/已停止)"
echo " 4) 🔍 搜索容器"
echo " 5) ❌ 退出"
echo ""

read -p "请输入选择 [1-5]: " mode

case $mode in
1)
select_all_containers
;;
2)
select_specific_containers
;;
3)
select_by_status
;;
4)
search_containers
;;
5)
info "退出程序"
exit 0
;;
*)
error "无效选择,使用默认模式"
select_all_containers
;;
esac
}

# 选择所有容器
select_all_containers() {
info "选择所有容器"

if [ "$HAS_JQ" = true ]; then
SELECTED_CONTAINERS=($(echo "$CONTAINER_DATA" | jq -r '.[].Names'))
else
SELECTED_CONTAINERS=()
while IFS=$'\t' read -r name _; do
[ -n "$name" ] && SELECTED_CONTAINERS+=("$name")
done <<< "$CONTAINER_LIST"
fi

echo "已选择 ${#SELECTED_CONTAINERS[@]} 个容器"
}

# 解析范围选择
parse_range_selection() {
local selection="$1"
local result=()

IFS=',' read -ra parts <<< "$selection"
for part in "${parts[@]}"; do
part=$(echo "$part" | tr -d ' ')

if [[ "$part" =~ ^([0-9]+)-([0-9]+)$ ]]; then
start=${BASH_REMATCH[1]}
end=${BASH_REMATCH[2]}

if [ "$start" -le "$end" ] && [ "$start" -ge 1 ] && [ "$end" -le "$CONTAINER_COUNT" ]; then
for ((i=start; i<=end; i++)); do
result+=("$i")
done
else
warning "范围 $part 无效,跳过"
fi
elif [[ "$part" =~ ^[0-9]+$ ]]; then
result+=("$part")
else
warning "无效输入: $part,跳过"
fi
done

echo "${result[@]}" | tr ' ' '\n' | sort -nu | tr '\n' ' '
}

# 修复:选择特定容器 - 确保不会直接退出
select_specific_containers() {
while true; do
info "选择特定容器"

display_containers

echo ""
echo "📝 选择方式:"
echo " - 单个容器: 1"
echo " - 多个容器: 1 3 5"
echo " - 范围选择: 1-5"
echo " - 混合选择: 1,3-5,7"
echo " - 全部选择: all"
echo " - 返回菜单: back"
echo ""

read -p "请输入选择: " selection

if [ "$selection" = "back" ]; then
info "返回主菜单"
select_mode
return
fi

if [ "$selection" = "all" ]; then
select_all_containers
return
fi

if [ -z "$selection" ]; then
error "输入为空,请重新输入"
continue
fi

parsed_selection=$(parse_range_selection "$selection")

if [ -z "$parsed_selection" ]; then
error "无效的选择,请重新输入"
continue
fi

SELECTED_CONTAINERS=()
selected_count=0

for num in $parsed_selection; do
idx=$((num-1))

if [ "$HAS_JQ" = true ]; then
if [ $idx -ge 0 ] && [ $idx -lt $CONTAINER_COUNT ]; then
container=$(echo "$CONTAINER_DATA" | jq -r ".[$idx].Names")
SELECTED_CONTAINERS+=("$container")
((selected_count++))
success "✓ 已选择: $container"
else
warning "⚠ 序号 $num 无效,跳过"
fi
else
container=$(echo "$CONTAINER_LIST" | sed -n "${num}p" | cut -f1)
if [ -n "$container" ]; then
SELECTED_CONTAINERS+=("$container")
((selected_count++))
success "✓ 已选择: $container"
else
warning "⚠ 序号 $num 无效,跳过"
fi
fi
done

if [ ${#SELECTED_CONTAINERS[@]} -eq 0 ]; then
error "未选择任何有效容器,请重新输入"
continue
fi

success "已选择 $selected_count 个容器"

# 显示选择结果
echo ""
echo "📋 已选择的容器:"
for container in "${SELECTED_CONTAINERS[@]}"; do
echo " - $container"
done
echo ""

# 修复:添加确认步骤,但确保不会因为确认而退出
read -p "确认选择? [Y/重新选择(r)/返回菜单(b)]: " confirm

case $confirm in
[rR])
info "重新选择"
continue
;;
[bB])
info "返回主菜单"
select_mode
return
;;
*)
# 用户确认,继续执行
return
;;
esac
done
}

# 按状态筛选
select_by_status() {
while true; do
info "按状态筛选"

echo "请选择状态:"
echo " 1) 🟢 运行中 (running)"
echo " 2) 🔴 已停止 (exited)"
echo " 3) 🟡 所有状态"
echo " 4) ↩ 返回菜单"
echo ""

read -p "请输入 [1-4]: " status_choice

case $status_choice in
1)
status_filter="Up"
status_display="运行中"
;;
2)
status_filter="Exited"
status_display="已停止"
;;
3)
status_filter="all"
status_display="所有状态"
;;
4)
info "返回主菜单"
select_mode
return
;;
*)
error "无效选择"
continue
;;
esac

SELECTED_CONTAINERS=()

if [ "$status_filter" = "all" ]; then
select_all_containers
return
fi

if [ "$HAS_JQ" = true ]; then
if [ "$status_filter" = "Up" ]; then
SELECTED_CONTAINERS=($(echo "$CONTAINER_DATA" | jq -r ".[] | select(.Status | startswith(\"Up\")) | .Names"))
elif [ "$status_filter" = "Exited" ]; then
SELECTED_CONTAINERS=($(echo "$CONTAINER_DATA" | jq -r ".[] | select(.Status | startswith(\"Exited\")) | .Names"))
fi
else
while IFS=$'\t' read -r name image status; do
[ -z "$name" ] && continue

if [ "$status_filter" = "Up" ] && [[ "$status" == Up* ]]; then
SELECTED_CONTAINERS+=("$name")
elif [ "$status_filter" = "Exited" ] && [[ "$status" == Exited* ]]; then
SELECTED_CONTAINERS+=("$name")
fi
done <<< "$CONTAINER_LIST"
fi

if [ ${#SELECTED_CONTAINERS[@]} -eq 0 ]; then
warning "未找到状态为 '$status_display' 的容器"
echo ""
read -p "是否重新选择? [Y/n] " -n 1 -r
echo ""
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
continue
else
select_mode
return
fi
else
success "找到 ${#SELECTED_CONTAINERS[@]} 个 '$status_display' 状态的容器"

echo ""
echo "📋 找到的容器:"
for container in "${SELECTED_CONTAINERS[@]}"; do
echo " - $container"
done
echo ""

read -p "确认选择? [Y/n/重新选择(r)]: " confirm
case $confirm in
[nN]|[rR])
continue
;;
*)
return
;;
esac
fi
done
}

# 搜索容器
search_containers() {
while true; do
info "搜索容器"

echo "搜索选项:"
echo " 1) 搜索容器名称"
echo " 2) 搜索镜像名称"
echo " 3) 搜索全部字段"
echo " 4) ↩ 返回菜单"
echo ""

read -p "请选择搜索模式 [1-4]: " search_mode

case $search_mode in
1) search_type="name" ;;
2) search_type="image" ;;
3) search_type="all" ;;
4)
info "返回主菜单"
select_mode
return
;;
*)
error "无效选择"
continue
;;
esac

read -p "请输入搜索关键词: " keyword

if [ -z "$keyword" ]; then
warning "搜索词为空,返回所有容器"
select_all_containers
return
fi

SELECTED_CONTAINERS=()

if [ "$HAS_JQ" = true ]; then
case $search_type in
"name")
SELECTED_CONTAINERS=($(echo "$CONTAINER_DATA" | jq -r ".[] | select(.Names | contains(\"$keyword\")) | .Names"))
;;
"image")
SELECTED_CONTAINERS=($(echo "$CONTAINER_DATA" | jq -r ".[] | select(.Image | contains(\"$keyword\")) | .Names"))
;;
"all")
SELECTED_CONTAINERS=($(echo "$CONTAINER_DATA" | jq -r ".[] | select(.Names | contains(\"$keyword\")) or select(.Image | contains(\"$keyword\")) | .Names"))
;;
esac
else
while IFS=$'\t' read -r name image status; do
[ -z "$name" ] && continue

local match=false
case $search_type in
"name")
[[ "$name" == *"$keyword"* ]] && match=true
;;
"image")
[[ "$image" == *"$keyword"* ]] && match=true
;;
"all")
[[ "$name" == *"$keyword"* ]] || [[ "$image" == *"$keyword"* ]] && match=true
;;
esac

[ "$match" = true ] && SELECTED_CONTAINERS+=("$name")
done <<< "$CONTAINER_LIST"
fi

if [ ${#SELECTED_CONTAINERS[@]} -eq 0 ]; then
warning "未找到包含 '$keyword' 的容器"
echo ""
read -p "是否重新搜索? [Y/n] " -n 1 -r
echo ""
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
continue
else
select_mode
return
fi
else
success "找到 ${#SELECTED_CONTAINERS[@]} 个匹配的容器"

echo ""
echo "🔍 搜索结果:"
for container in "${SELECTED_CONTAINERS[@]}"; do
echo " - $container"
done
echo ""

read -p "确认选择? [Y/n/重新搜索(r)]: " confirm
case $confirm in
[nN]|[rR])
continue
;;
*)
return
;;
esac
fi
done
}

# 备份现有文件
backup_existing() {
if [ -f "$OUTPUT_FILE" ]; then
backup_file="${OUTPUT_FILE}.backup.$BACKUP_TIMESTAMP"
cp "$OUTPUT_FILE" "$backup_file"
info "已备份现有文件: $backup_file"
fi
}

# 优化:使用简单的docker inspect获取版本号
get_image_version_simple() {
local image_name="$1"
local image_tag="$2"
local full_image="${image_name}:${image_tag}"

if ! docker image inspect "$full_image" &> /dev/null; then
echo ""
return
fi

# 使用简单的 docker inspect | grep -i version 方法
local version_info=$(docker image inspect "$full_image" 2>/dev/null | \
grep -i "version" | \
grep -oE '[0-9]+\.[0-9]+\.[0-9]+|[0-9]+\.[0-9]+' | \
head -1)

if [ -n "$version_info" ]; then
echo "$version_info"
return
fi

# 尝试从常见的环境变量中获取
local env_version=$(docker image inspect "$full_image" 2>/dev/null | \
grep -oE '"VERSION="[^"]*|"NGINX_VERSION="[^"]*|"POSTGRES_VERSION="[^"]*|"MYSQL_VERSION="[^"]*|"REDIS_VERSION="[^"]*' | \
cut -d'"' -f4 | \
head -1)

if [ -n "$env_version" ]; then
echo "$env_version"
return
fi

# 从创建时间推断
local created_date=$(docker image inspect "$full_image" 2>/dev/null | \
grep -o '"Created"[^,]*' | \
cut -d '"' -f4 | \
head -1 | \
cut -d'T' -f1)

if [ -n "$created_date" ]; then
echo "$created_date" | grep -oE '^[0-9]{4}-[0-9]{2}' | tr '-' '.'
return
fi

echo ""
}

# 替换版本号
replace_versions() {
step "版本号考古与替换"

mapfile -t IMAGE_LINES < <(grep -n -E '^\s+image:' "$TEMP_FILE" 2>/dev/null || true)

if [ ${#IMAGE_LINES[@]} -eq 0 ]; then
warning "未找到镜像定义"
return
fi

info "发现 ${#IMAGE_LINES[@]} 个镜像"

cp "$TEMP_FILE" "$OUTPUT_FILE"

local replaced_count=0

for line_info in "${IMAGE_LINES[@]}"; do
line_num=$(echo "$line_info" | cut -d: -f1)
line_content=$(echo "$line_info" | cut -d: -f2-)

image_spec=$(echo "$line_content" | sed 's/^[[:space:]]*//;s/image://;s/[[:space:]]*$//;s/#.*$//')

[ -z "$image_spec" ] && continue

if [[ "$image_spec" == *:* ]]; then
image_name="${image_spec%:*}"
current_tag="${image_spec#*:}"
else
image_name="$image_spec"
current_tag="latest"
fi

image_name=$(echo "$image_name" | sed "s/^['\"]//;s/['\"]$//")
current_tag=$(echo "$current_tag" | sed "s/^['\"]//;s/['\"]$//")

if [[ "$current_tag" == "latest" ]]; then
# 使用优化后的版本检测方法
version=$(get_image_version_simple "$image_name" "$current_tag")

if [ -n "$version" ] && [ "$version" != "latest" ]; then
# 显示调试信息
echo -e " ${BLUE}${NC} 检测镜像: ${image_name}:${current_tag}"
echo -e " ${GREEN}${NC} 找到版本: $version"

# 替换latest为具体版本
sed -i "s|^\(\s*image:\s*\)${image_name}:latest\b|\1${image_name}:${version}|g" "$OUTPUT_FILE"
sed -i "s|^\(\s*image:\s*\)['\"]\?${image_name}:latest['\"]\?\b|\1\"${image_name}:${version}\"|g" "$OUTPUT_FILE"

echo -e " ${GREEN}${NC} 替换: ${image_name}:latest → ${image_name}:${version}"
((replaced_count++))
else
echo -e " ${YELLOW}${NC} 无法确定版本: ${image_name}:latest (保持原样)"
fi
else
echo -e " ${BLUE}${NC} 已有具体版本: ${image_name}:${current_tag}"
fi
done

if [ $replaced_count -gt 0 ]; then
success "版本替换完成 ($replaced_count 个镜像已更新)"
else
info "没有需要替换的 latest 标签"
fi
}

# 生成Compose配置
generate_compose() {
local containers=("$@")

header "生成配置"

step "验证容器"
local valid_containers=()

for container in "${containers[@]}"; do
if docker inspect "$container" &> /dev/null; then
valid_containers+=("$container")
success " ✓ $container"
else
error " ✗ $container (不存在)"
fi
done

if [ ${#valid_containers[@]} -eq 0 ]; then
error "没有有效的容器可分析"
exit 1
fi

backup_existing

step "调用 AutoCompose"
echo "正在分析: ${valid_containers[*]}"

if ! docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
"$AUTOCOMPOSE_IMAGE" "${valid_containers[@]}" > "$TEMP_FILE" 2>&1; then
error "AutoCompose执行失败"
echo "错误信息:"
cat "$TEMP_FILE"
exit 1
fi

if [ ! -s "$TEMP_FILE" ]; then
error "生成的配置为空"
exit 1
fi

success "原始配置生成完成 ($(wc -l < "$TEMP_FILE") 行)"

replace_versions

header "完成"

echo -e "${GREEN}📁 输出文件: $OUTPUT_FILE${NC}"
echo -e "${BLUE}📊 文件大小: $(du -h "$OUTPUT_FILE" | cut -f1)${NC}"
echo -e "${BLUE}📈 总行数: $(wc -l < "$OUTPUT_FILE")${NC}"
echo -e "${BLUE}📦 容器数量: ${#valid_containers[@]}${NC}"
echo ""

echo -e "${YELLOW}🔍 生成的镜像配置:${NC}"
grep -E '^\s+image:' "$OUTPUT_FILE" 2>/dev/null | head -20 | sed 's/^/ /'

echo ""
echo -e "${CYAN}💡 使用建议:${NC}"
echo " 1. 🔍 检查配置: vim $OUTPUT_FILE"
echo " 2. ✅ 验证语法: docker-compose -f $OUTPUT_FILE config"
echo " 3. 🚀 测试启动: docker-compose -f $OUTPUT_FILE up -d"
echo " 4. 📊 查看状态: docker-compose -f $OUTPUT_FILE ps"
echo " 5. 🛑 停止清理: docker-compose -f $OUTPUT_FILE down"
echo ""

rm -f "$TEMP_FILE"

echo -e "${MAGENTA}┌──────────────────────────────────────────────────────┐${NC}"
echo -e "${MAGENTA}│ 📁 文件位置: $(pwd)/$OUTPUT_FILE${NC}"
echo -e "${MAGENTA}└──────────────────────────────────────────────────────┘${NC}"
}

# 修复:主函数 - 确保选择后不会直接退出
main() {
print_banner

check_jq

get_all_containers

display_containers

select_mode

# 修复:检查SELECTED_CONTAINERS是否为空
if [ ${#SELECTED_CONTAINERS[@]} -eq 0 ]; then
error "未选择任何容器,退出程序"
exit 1
fi

echo ""
echo -e "${CYAN}已选择以下 ${#SELECTED_CONTAINERS[@]} 个容器:${NC}"
for container in "${SELECTED_CONTAINERS[@]}"; do
echo " - $container"
done
echo ""

read -p "确认开始生成配置? [Y/n] " -n 1 -r
echo ""
if [[ $REPLY =~ ^[Nn]$ ]]; then
info "操作取消"
exit 0
fi

generate_compose "${SELECTED_CONTAINERS[@]}"
}

# 异常处理
trap 'echo ""; error "脚本被中断"; exit 1' INT TERM

# 运行主函数
main "$@"

将上面代码拷贝到xxx.sh 文件中,然后./xxx.sh 运行即可,运行前记得给予它执行权限:

1
chmod +x xxx.sh

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

0%