如何用 Shell 命令自动化创建 Postgres 的 Docker 镜像
如果项目里有些代码反复出现,有个很标准的解决办法:把它封装成函数。
但要是项目的文件配置一次次重复出现,又该怎么办呢?
答案就是 bash 脚本。Bash 脚本可以在你的机器上自动执行一系列命令,全程无需人工干预。这就是所谓的工作流自动化,也是本文要教你的内容。
目录
前置条件
要跟着本文操作,你需要:
- 在机器上安装 Git-bash
- 有一定使用终端或命令行执行命令的经验(有帮助但非必需)
- 理解基本编程概念(如条件判断和循环)
- 在机器上安装 Docker
- 对 Docker 基本概念(如镜像和容器)有基本了解
- 用过 Postgres(可选但推荐)
这个项目的由来
我最近在几个项目里都要搭建 Postgres 的 Docker 镜像,发现每次创建镜像的流程几乎一样,唯一的区别只是数据库凭证和数据库名不同。
我还注意到,如今很多快速开发的应用都倾向于用 Supabase 做后端,但它可能存在资源限制,比如数据库存储空间、带宽、并发连接数和 API 请求配额(具体取决于服务套餐和项目规模)。而自己搭建数据库就能解决这些问题。
于是我想,何不作个 CLI 工具,来自动化创建 Postgres 的 Docker 镜像?正是在这时,我学到了如何用 Bash 编写可执行的 Shell 命令,实现工作流自动化。
本指南所用的完整可运行示例,见此处 GitHub 仓库。
简介
Bash 是一种 Shell 和脚本语言,用于编写和执行在计算机上执行操作的命令或脚本,例如创建目录和文件、处理文件、自动化任务、管理系统进程等。
本指南将教你自动化创建 Postgres 数据库的 Docker 镜像,免去每个新项目都从零构建镜像的麻烦。
Bash 脚本文件设置
首先,打开你常用的文本编辑器,在其中创建一个目录。然后在该目录下新建一个后缀为 .sh 的文件。
在文件开头输入:
#!/bin/bash
#!/bin/bash(即“shebang”)会告知操作系统使用 Bash Shell 运行此脚本,使文件内的命令以 Bash/终端命令的形式执行。
Bash 中的条件
建好脚本文件并加上 shebang 后,我们先来看看 Bash 中 if 语句的用法。
if 的语法如下:
if [[ condition ]];
then
command to execute
fi
Bash 里的 if 和其他编程语言中的用法并无二致:
- 用双中括号([])加入要检查的条件,并以分号(;)结尾。
- 在
if声明后使用then,指定条件为真时应执行的命令。 - 使用
fi结束条件语句。
在中括号内书写条件时,务必在条件的前后各留一个空格(如上文代码所示)。若省略这些空格,就会报错。
示例说明:
既然我们要自动化创建 Postgres 数据库的 Docker 镜像,先检查目标文件是否存在且包含内容。若有内容,则结束脚本;若无,则继续执行下一段脚本。
#!/bin/bash
if [[ -s docker-compose.yml ]];
then
echo "The file already exists"
exit 0
fi
echo "Next command"
在 if 条件中,我们用 -s 标志来检查文件是否存在且非空。这类文件标志用于检查文件的状态和权限,常见的有:
- -f:检查文件是否存在
- -s:检查文件是否存在且非空
- -d:检查目录是否存在
- -r:检查文件是否可读
- -w:检查文件是否可写
- -x:检查文件是否可执行
在这个脚本中,我们用 -s 检查当前执行命令的目录下是否存在名为 docker-compose.yml 的文件。条件为假时,通过 echo 关键字打印文本 "Next command";条件为真时,打印文本 "The file already exists"。
exit 相当于 bash 脚本中的 return 关键字,0 表示成功,1 表示失败。
我们来测试一下:在编写 bash 脚本的同一目录下创建一个 Docker Compose 文件并写入一些内容。然后在终端输入 bash name-of-file.sh,应该会看到 "The file already exists":
但如果删掉文件内容后再运行命令,就会看到输出 "Next command":
交互式 Shell
要让自动化脚本更动态、灵活,就需要接收用户输入。以数据库名称为例:数据库名并非固定不变,它会随项目而变化。
因此,在为项目创建新的 Docker 镜像时,需要能使用指定的数据库名称,这可以通过 read 关键字来读取输入实现。
根据操作的不同,输入也分为不同类型,可以用相应的标志来指定预期的输入类型,例如:
- -p:提示用户输入文本 翻译如下:
- -s:用于密码场景,在终端输入时隐藏用户输入内容
- -a:当需要用户传入多个输入或期望接收一个数组时使用
读取用户输入的语法如下:
read -p 'input_label: ' variable_name
实例演示:
接下来我们编写脚本,接收用户关于要创建的 Postgres 数据库镜像的输入。
#!/bin/bash
# 数据库凭证
read -p 'DataBase-name(*): ' db
read -p 'DataBase-user(*): ' user
read -sp 'DataBase-password(*): ' pwd
echo
echo "your database is -> $db with a username of $user and a password of $pwd"
脚本在 read 命令上使用 -p 参数来提示一段文本,使用 -sp 则提示用户输入并隐藏内容(因为 -s 会隐藏所键入的字符)。
如果不同时使用 -p 和 -s,将无法显示提示用户输入密码的标签。单独使用 -s 虽能隐藏输入,但若不通过 echo 或 printf 打印,就不会显示任何提示。
要打印或使用用户添加的输入,在变量名前加美元符号($)。例如,上面我们将获取数据库名称的变量命名为 db,因此访问用户输入的数据库名时使用 $db。
如果在终端中运行该脚本,你会看到一系列等待输入的终端提示。填写完毕后,所有输入值都会显示在终端中:
如何验证和清理输入
在执行任何操作前,你还需要验证输入。这一点很重要,因为关键字段可能会缺失或拼写错误。例如,你可能忘记输入正在创建的镜像所需的数据库名称。
验证输入时,你可以使用以下标志之一(以及其他标志):
- -z:检查变量值是否为空
- -n:检查变量值是否不为空
- -lt:检查一个数是否小于给定数字
- -gt:检查一个数是否大于给定数字
- -eq:检查两个值是否相等
对于需要确保非空的每一项输入,你都可以用 -z 标志来检查,通过后再进入下一步:
#!/bin/bash
# 数据库凭据
read -p 'DataBase-name(*): ' db
if [[ -z $db ]]
then
echo "The database field cannot be empty!"
exit 1
fi
read -p 'DataBase-user(*): ' user
if [[ -z $user ]]
then
echo "The user field cannot be empty!"
exit 1
fi
read -sp 'DataBase-password(*): ' pwd
if [[ -z $pwd ]]
then
echo "The password field cannot be empty!"
exit 1
fi
这个脚本可以确保在进行下一项输入之前,所有关键的数据库信息都已填写。
默认值
有些字段你可能希望使用默认值(比如镜像的容器名、容器的端口或卷)。这时可以用 := 给变量设置默认值:
#!/bin/bash
# 容器信息
read -p 'Container-name: ' container_name
read -p 'Container-port(if omitted, the container will use default postgresql default port of 5432): ' port
read -p 'Volume-name: ' vol_name
echo "container-name-> ${container_name:=cont_one}, port->${port:=5432}, volume_name->${vol_name:=backend_data}"
运行脚本时如果这三项输入都留空,bash 就会使用你设置的默认值作为兜底。
而如果为这些输入提供了值,bash 就会使用你传入的值。比如三项全部填写,看看输出的结果:
如何创建文件并写入内容
了解了如何为 Postgres 数据库创建 Docker 镜像之后,接下来需要创建一个 docker-compose.yml 文件,用于在启动和停止容器时执行。
在 Bash 中,创建文件有多种方式:
- 第一种是
> filename:文件不存在就创建,存在则清空重写。 - 还有
>> filename:往文件追加内容,文件不存在则先创建。 - 最后是
touch filename:文件不存在就创建,存在则保留原内容,只更新文件的时间戳。
如何用 > 创建并写入文件
如果想在创建文件的同时写入内容,可以在 > 前面加上 echo,后面跟上文件名,像这样:
#!/bin/bash
echo Hello> test.txt
这会创建一个 test.txt 文件,内容为 "hello"。
如何用 >> 向已有文件追加内容
要向已有文件追加内容,把内容放在 >> 前面,后面跟上目标文件名即可。比如在上一步创建的 test.txt 文件中,往 Hello 后面追加 Africa:
#!/bin/bash
echo Africa!>> test.txt
现在 test.txt 里会有 Hello,换行后是 Africa!。
接下来就用这些符号创建一个 docker-compose.yml 文件,用来存放 Postgres 数据库的相关配置。
#!/bin/bash
> docker-compose.yml
这样,目录里就生成了 docker-compose 文件。
如何向文件写入多段不同内容
你还可以用 cat 来写入文件。通常 cat 是用来读取文件内容的,但配合 <<EOF 和刚才讲到的追加符号 >>,就可以按自己想要的格式往文件里写入多段内容。语法如下:
# writing docker compose contents
cat <<EOF>> docker-compose.yml
# file contents
EOF
<<EOF 告诉 bash 一直往文件写入内容,直到遇到 EOF(end of file)关键字为止,再用 >> filename 把这些内容追加到文件中。用在 Docker compose 配置上就是这样的:
#!/bin/bash
# 创建并写入文件
> docker-compose.yml
cat<<EOF>>docker-compose.yml
services:
postgres:
image: postgres:18-alpine
container_name: ${container_name:= $db}
environment:
POSTGRES_USER: $user
POSTGRES_PASSWORD: $pwd
POSTGRES_database: $db
ports:
- ${port:=5432}:5432
volumes:
- ${vol_name:=database-data}:/var/postgresql
volumes:
${vol_name:=database-data}:
EOF
Bash 参数与选项
使用 Bash 参数,可以在执行脚本的同一行命令中传入值。具体做法就是在执行脚本时把值直接跟在后面,像这样:
bash script val1 val2
要读取参数的值,使用美元符号($)加上对应值的位置编号(位置从 1 开始,而不是 0)。
在上面的例子中,如果想输出第二个参数的值,可以使用 echo $2。
为了更好地理解,我们来优化一下数据输入体验:通过参数在执行时直接传入数据库凭据:
#!/bin/bash
# database credentials
if [[ -z $1 ]]; then
read -p 'DataBase-name(*): ' db
else
db="$1"
fi
# database checking and validating
if [[ -z $2 ]]; then
read -p 'DataBase-user(*): ' user
else
user="$2"
fi
# user checking and validating
if [[ -z $1 ]]; then
read -sp 'DataBase-password(*): ' pwd
else
pwd=$3
fi
# password vaidation and checking
这段代码会先检查是否传入了参数。如果没有,就通过提示符让用户输入值;如果有,就把参数值赋给对应的变量(数据库名、密码等)。这样就可以带参数运行脚本了:
bash image.sh customers ABC-Company secret123!
运行上述脚本后创建的镜像将包含一个名为 customers 的数据库,用户为 ABC-Company,密码为 secret123!。
Bash 标志/选项
脚本里有一段用于收集镜像相关信息,但这部分信息可以省略。借助 bash 的选项/标志(flags),你可以通过 -D 选项跳过提示输入容器信息的步骤,让脚本在运行前直接沿用默认的容器信息。
举个例子:
#!/bin/bash
while getopts "ny" options; do
case "$options" in
n) echo "You choose no";;
y) echo "You choose yes";;
*) echo "No such option/flag"; exit 1;;
esac
done
shift $((OPTIND - 1))
在上面的脚本中:
- while 循环会一直运行,直到
getopts` 为空 getopts负责获取脚本接收到的选项"ny"是我们期望用户在运行脚本时传入的选项options是一个变量,用来记录用户在 "n" 和 "y" 之间输入的选项do表示循环开始case通过in关键字检查所选选项是否在已定义的选项列表中- 接着我们定义调用脚本时传入
-n或-y选项后要做的事,并用双分号 (;;) 结束每个选项的定义 *用于处理未定义的输入选项esac结束 case 语句done结束循环,避免循环无限运行下去shift $((OPTIND - 1))会把剩余参数左移,越过那些标志位,从而将其从参数列表中丢弃,避免脚本还需要接收其他参数时可能出现的冲突。
你可以在脚本里套用这个语法,在执行时传入 -D 选项来跳过询问容器信息的步骤。
#!/bin/bash
ask_container_info=true
while getopts "D" options; do
case "$options" in
D) ask_container_info=false;;
*) echo "No such option/flag"; exit 1;;
esac
done
shift $((OPTIND - 1))
if [[ $ask_container_info == true ]]; then
#Prompt the user to input custom container information
read -p 'Container-name: ' container_name
read -p 'Container-port(if omitted, the container will use default postgresql default port of 5432): ' port
read -p 'Volume-name: ' vol_name
fi
这里只有一个选项,所以选项部分只有 D。ask_container_info 初始值为 true,也就是说,除非传入该选项,脚本都会提示用户输入容器信息。当执行脚本时传入 -D,我们就把 ask_container_info 的值改为 false。
如何让脚本可执行并添加到电脑的 Path 中
下一步是让脚本在你电脑上的每个终端都能使用和执行。目前,脚本只能在你编写它的当前目录下运行。
我们先把脚本文件设为可执行,并去掉文件扩展名,这样就不必每次都带着 .sh 扩展名来调用脚本。
如何让文件可执行
要让文件可执行,可以使用 Linux 的 chmod 命令(change mode 的缩写)。Mode 包含写、读、执行等文件权限。
chmod +x image
这里,x 表示把文件设为可执行,后面是你想要设为可执行的文件名(image)。
如何把你的目录添加到 system/machine Path 中
要让脚本在你打开的任何终端、任何目录下都能使用,就需要把你的脚本注册到存放全局脚本和命令的系统路径中。
为此,先复制你目录的完整路径,然后在终端输入以下命令:
export PATH="$PATH:<YOUR_DIRECTORY_LOCATION>"
把 <YOUR_DIRECTORY_LOCATION> 替换成你的目录。这条命令会把脚本目录添加到当前终端中。但如果你切换到另一个终端再运行脚本,就会失败。
现在我们把目录配置为全局的,让每个终端都能执行脚本:
nano ~/.bash_profile
执行后你应该会看到类似这样的内容:
从上图可以看到,我已经在文件中声明了自己的路径(见选中文本部分)。
如果文件里没有这行内容,可以直接把 export PATH="$PATH:<YOUR_DIRECTORY_LOCATION>" 复制粘贴进去。完成后按 Ctrl+O 再按 enter 保存,然后按 Ctrl+x 退出 nano 编辑器。
接着重新加载机器的路径配置,让脚本目录生效:
source ~/.bash_profile
这样之后,你就可以在任何终端里直接用脚本文件名来运行脚本了(我的文件名是 image):
结语
在本指南中,你学会了如何编写自己的脚本来实现工作流自动化。希望能帮你减少在重复性任务上浪费的时间和精力,这些其实都可以通过脚本命令来自动完成。
本指南示例中使用的完整 CLI 工具可以在这个 git 仓库中找到,里面有安装指南和使用说明。如果觉得有帮助,欢迎给仓库点个 Star,也可以分享给可能有需要的同事。
谢谢!