commit 20808e8a5563682294aa7f3c6b81dffd380374d6 Author: 严浩 Date: Mon Sep 22 23:33:52 2025 +0800 feat(docker): 初始化项目并配置定时备份 PostgreSQL 数据库示例 diff --git a/.env b/.env new file mode 100644 index 0000000..1dcc4aa --- /dev/null +++ b/.env @@ -0,0 +1 @@ +TZ=Asia/Shanghai \ No newline at end of file diff --git a/.github/workflows/docker-push.yaml b/.github/workflows/docker-push.yaml new file mode 100644 index 0000000..9cc0ae3 --- /dev/null +++ b/.github/workflows/docker-push.yaml @@ -0,0 +1,42 @@ +# name: _打包推送镜像 +on: + workflow_dispatch: + push: +permissions: + packages: write +env: + TZ: Asia/Shanghai + +jobs: + build-and-push-ghcr: + runs-on: ubuntu-latest + env: + # https://github.com/docker/metadata-action/tree/v5/?tab=readme-ov-file#semver + # Event: push, Ref: refs/head/main, Tags: main + # Event: push tag, Ref: refs/tags/v1.2.3, Tags: 1.2.3, 1.2, 1, latest + # Event: push tag, Ref: refs/tags/v2.0.8-rc1, Tags: 2.0.8-rc1 + metadata-action-tags: | + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + + steps: + - name: 🔑 登录 GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: 🐳 构建并推送 Docker 镜像 + uses: yanhao98/composite-actions/docker-build-push@main + with: + file: ./Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + load: false + meta_images: ghcr.io/${{ github.repository }} + meta_tags: ${{ env.metadata-action-tags }} + cache-from: type=gha,scope=${{ github.workflow }} + cache-to: type=gha,scope=${{ github.workflow }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f603c68 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/backups +.DS_Store \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2311ddc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM alpine:3.22.1 AS base + +# 解决时区问题 +ENV TZ=Asia/Shanghai +RUN apk add alpine-conf && \ + setup-timezone -z Asia/Shanghai && \ + apk del alpine-conf + +FROM base AS final + +# 安装 sqlite 工具,cron 默认已包含 (dcron) +RUN apk add --no-cache tini + +# 时区 +ENV TZ=Asia/Shanghai + +# 启动脚本与任务脚本 +COPY entrypoint.sh run-cron-tasks.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/run-cron-tasks.sh && \ + mkdir -p /docker-entrypoint-init.d /docker-cron.d /var/lib/cron-init + +ENTRYPOINT ["/sbin/tini", "--", "entrypoint.sh"] diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..31d69f2 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,43 @@ +volumes: + tmp: + external: false +services: + postgres17: + container_name: postgres17 + restart: unless-stopped + environment: + - POSTGRES_USER=pguser + - POSTGRES_PASSWORD=pgpass + - POSTGRES_DB=_ + volumes: + - ./backups:/backups:rw + - tmp:/var/lib/postgresql/data + image: postgres:17 + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 30s + timeout: 5s + retries: 5 + start_period: 10s + + backup-cron: + depends_on: + postgres17: + condition: service_healthy + image: ghcr.io/yanhao98/docker-cron:main + build: + context: . + dockerfile: Dockerfile + environment: + - APK_PACKAGES=curl jq + - CRON_SCHEDULE=*/1 * * * * + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - ./backups:/backups:ro + # - ./examples/init:/docker-entrypoint-init.d + - type: bind + source: ./examples/cron/10-postgres-dump-docker-sock.sh + target: /docker-cron.d/10-postgres-dump-docker-sock.sh + bind: + create_host_path: false + restart: unless-stopped diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..8c08402 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,85 @@ +#!/bin/sh +set -eu + +log_info() { + printf '%s\n' "cron-entrypoint: $*" +} + +log_error() { + printf '%s\n' "cron-entrypoint: $*" >&2 +} + +if ! set -o pipefail 2>/dev/null; then + log_info "shell does not support pipefail; continuing without it" +fi + +CRON_SCHEDULE="${CRON_SCHEDULE:-*/30 * * * *}" +INIT_SCRIPTS_DIR="${INIT_SCRIPTS_DIR:-/docker-entrypoint-init.d}" +CRON_TASKS_DIR="${CRON_TASKS_DIR:-/docker-cron.d}" +APK_PACKAGES="${APK_PACKAGES:-}" + +STDOUT_FD="/proc/1/fd/1" +STDERR_FD="/proc/1/fd/2" +CRON_SCRIPT="/usr/local/bin/run-cron-tasks.sh" + +readonly CRON_SCHEDULE INIT_SCRIPTS_DIR CRON_TASKS_DIR APK_PACKAGES STDOUT_FD STDERR_FD CRON_SCRIPT + +if [ -z "$CRON_SCHEDULE" ]; then + log_error "CRON_SCHEDULE is empty; refusing to start." + exit 1 +fi + +mkdir -p /var/spool/cron/crontabs "$INIT_SCRIPTS_DIR" "$CRON_TASKS_DIR" + +if [ -n "$APK_PACKAGES" ]; then + log_info "ensuring apk packages: $APK_PACKAGES" + # shellcheck disable=SC2086 + if ! wget -qO- https://Git.1-H.CC/Scripts/Linux/raw/branch/main/ensure-apk-packages.sh | \ + sh -s -- $APK_PACKAGES; then + log_error "failed to install requested apk packages" + exit 1 + fi +else + log_info "no additional apk packages requested" +fi + +log_info "starting initialization from $INIT_SCRIPTS_DIR" +set -- "$INIT_SCRIPTS_DIR"/* +if [ "$1" = "$INIT_SCRIPTS_DIR/*" ]; then + log_info "no initialization scripts found" +else + for script in "$@"; do + if [ ! -f "$script" ]; then + log_info "skipping non-regular entry $script" + continue + fi + + script_name=$(basename "$script") + log_info "running initialization script $script_name" + if [ -x "$script" ]; then + if ! "$script" >>"$STDOUT_FD" 2>>"$STDERR_FD"; then + log_error "initialization script $script_name failed" + exit 1 + fi + else + if ! /bin/sh "$script" >>"$STDOUT_FD" 2>>"$STDERR_FD"; then + log_error "initialization script $script_name failed" + exit 1 + fi + fi + done +fi + +log_info "initialization complete" + +cron_env_file=/var/spool/cron/crontabs/root +{ + printf 'SHELL=/bin/sh\n' + printf 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\n' + printf 'CRON_TASKS_DIR=%s\n' "$CRON_TASKS_DIR" + printf '%s %s\n' "$CRON_SCHEDULE" "$CRON_SCRIPT >> $STDOUT_FD 2>> $STDERR_FD" +} >"$cron_env_file" +chmod 0600 "$cron_env_file" + +log_info "starting crond with schedule '$CRON_SCHEDULE'" +exec crond -f diff --git a/examples/cron/10-postgres-dump-docker-sock.sh b/examples/cron/10-postgres-dump-docker-sock.sh new file mode 100755 index 0000000..9e3a874 --- /dev/null +++ b/examples/cron/10-postgres-dump-docker-sock.sh @@ -0,0 +1,7 @@ +curl -fsSL https://Git.1-H.CC/Scripts/Linux/raw/branch/main/postgres-dump-zstd-docker-sock.sh | + sh -s -- \ + --socket=/var/run/docker.sock \ + --api-version=v1.51 \ + --backup-dir=/backups \ + --container=postgres17 \ + --backup-prefix=postgres17_all_databases_zstd_ diff --git a/examples/init/00-install-deps.sh b/examples/init/00-install-deps.sh new file mode 100755 index 0000000..84124d3 --- /dev/null +++ b/examples/init/00-install-deps.sh @@ -0,0 +1,4 @@ +#!/bin/sh +# Dependency installation moved to entrypoint.sh. +# Define APK_PACKAGES environment variable (e.g. "curl jq") to +# make the entrypoint ensure the listed Alpine packages are present. diff --git a/run-cron-tasks.sh b/run-cron-tasks.sh new file mode 100644 index 0000000..5ea287f --- /dev/null +++ b/run-cron-tasks.sh @@ -0,0 +1,69 @@ +#!/bin/sh +set -u + +CRON_TASKS_DIR="${CRON_TASKS_DIR:-/docker-cron.d}" +STDOUT_FD="/proc/1/fd/1" +STDERR_FD="/proc/1/fd/2" + +readonly CRON_TASKS_DIR STDOUT_FD STDERR_FD + +log_info() { + printf '%s\n' "run-cron-tasks: $*" +} + +log_error() { + printf '%s\n' "run-cron-tasks: $*" >&2 +} + +if [ ! -d "$CRON_TASKS_DIR" ]; then + log_info "tasks directory $CRON_TASKS_DIR not found; skipping" + exit 0 +fi + +run_started_at=$(date -u '+%Y-%m-%dT%H:%M:%SZ') +log_info "===== cron run started $run_started_at =====" + +set -- "$CRON_TASKS_DIR"/* +if [ "$1" = "$CRON_TASKS_DIR/*" ]; then + log_info "no task scripts found in $CRON_TASKS_DIR" + run_finished_at=$(date -u '+%Y-%m-%dT%H:%M:%SZ') + log_info "===== cron run finished $run_finished_at (exit 0) =====" + exit 0 +fi + +max_rc=0 +for task in "$@"; do + if [ ! -f "$task" ]; then + log_info "skipping non-regular entry $task" + continue + fi + + task_name=$(basename "$task") + log_info "starting task $task_name" + + if [ ! -x "$task" ]; then + if ! chmod +x "$task" 2>>"$STDERR_FD"; then + log_error "failed to chmod +x $task_name" + if [ "$max_rc" -lt 1 ]; then + max_rc=1 + fi + continue + fi + fi + + if "$task" >>"$STDOUT_FD" 2>>"$STDERR_FD"; then + rc=0 + log_info "task $task_name completed successfully" + else + rc=$? + log_error "task $task_name exited with status $rc" + fi + + if [ "$rc" -gt "$max_rc" ]; then + max_rc=$rc + fi +done + +run_finished_at=$(date -u '+%Y-%m-%dT%H:%M:%SZ') +log_info "===== cron run finished $run_finished_at (exit $max_rc) =====" +exit "$max_rc"