From a06d4f4199e30e5c686993606ccc8058caf0547e Mon Sep 17 00:00:00 2001 From: Rogee Date: Fri, 11 Sep 2026 14:10:39 +0800 Subject: [PATCH] feat(deploy): add production one-click deployment --- deploy/docker/README.dist.md | 23 ++++++----- deploy/docker/deploy.sh | 70 +++++++++++++++++++++++++++++++++ deploy/docker/preflight.sh | 42 +++++++++++++++++++- deploy/docker/preflight_test.sh | 2 + scripts/build-production.sh | 1 + 5 files changed, 126 insertions(+), 12 deletions(-) create mode 100755 deploy/docker/deploy.sh diff --git a/deploy/docker/README.dist.md b/deploy/docker/README.dist.md index cef123fc..c1763926 100644 --- a/deploy/docker/README.dist.md +++ b/deploy/docker/README.dist.md @@ -9,19 +9,22 @@ sha256sum -c SHA256SUMS docker load --input images/gochat-*-images.tgz cp .env.example .env # Replace every CHANGE_ME value and create the external secret/mount paths. -docker compose --env-file .env -f deploy/docker/docker-compose.prod.yml config --quiet -docker compose --env-file .env -f deploy/docker/docker-compose.prod.yml --profile ops run --rm migrate -docker compose --env-file .env -f deploy/docker/docker-compose.prod.yml up -d --wait +deploy/docker/deploy.sh .env --allow-local-images +``` + +`--allow-local-images` is only for this SHA256-verified bundle, whose loaded +application images use source-versioned local tags. For registry promotion, +replace both application image references with immutable `@sha256:` digests and +run the same command without that flag: + +```bash +deploy/docker/deploy.sh .env ``` The bundle uses source-versioned local image tags so it can start immediately -after `docker load`; `SHA256SUMS` protects the handoff. Before registry-based -production promotion, push both images, replace the two image references in -`.env` with immutable `@sha256:` references, then run: - -```bash -deploy/docker/preflight.sh .env -``` +after `docker load`; `SHA256SUMS` protects the handoff. Registry-based +production promotion requires immutable `@sha256:` references and the strict +preflight performed by `deploy.sh` without `--allow-local-images`. See `docs/ops/02-production-operations.md` for TLS, backup, rollback, and drill requirements. diff --git a/deploy/docker/deploy.sh b/deploy/docker/deploy.sh new file mode 100755 index 00000000..010ca6a9 --- /dev/null +++ b/deploy/docker/deploy.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +set -euo pipefail +export LC_ALL=C + +script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +bundle_root=$(cd "$script_dir/../.." && pwd) +env_file= +allow_local_images=false + +usage() { + cat <<'EOF' +Usage: deploy/docker/deploy.sh [env-file] [--allow-local-images] + +Use --allow-local-images only with a SHA256-verified production bundle whose +application images were loaded from its images/ archive. +EOF +} + +while (($#)); do + case $1 in + --allow-local-images) + allow_local_images=true + shift + ;; + -h | --help) + usage + exit 0 + ;; + -*) + echo "unknown option: $1" >&2 + usage >&2 + exit 2 + ;; + *) + if [[ -n $env_file ]]; then + echo "only one env file may be specified" >&2 + usage >&2 + exit 2 + fi + env_file=$1 + shift + ;; + esac +done + +env_file=${env_file:-$bundle_root/.env} +if ! env_file=$(realpath -e -- "$env_file"); then + echo "environment file does not exist: $env_file" >&2 + exit 1 +fi + +if [[ -f $bundle_root/SHA256SUMS ]]; then + (cd "$bundle_root" && sha256sum -c SHA256SUMS) +elif [[ $allow_local_images == true ]]; then + echo "--allow-local-images requires a SHA256-verified production bundle" >&2 + exit 1 +fi + +preflight_args=() +if [[ $allow_local_images == true ]]; then + preflight_args+=(--allow-local-images) +fi +"$script_dir/preflight.sh" "${preflight_args[@]}" "$env_file" + +compose=(docker compose --env-file "$env_file" -f "$script_dir/docker-compose.prod.yml") +"${compose[@]}" config --quiet +"${compose[@]}" --profile ops run --rm migrate +"${compose[@]}" up -d --wait + +echo "production deployment is ready" diff --git a/deploy/docker/preflight.sh b/deploy/docker/preflight.sh index 6434a468..98166ac5 100755 --- a/deploy/docker/preflight.sh +++ b/deploy/docker/preflight.sh @@ -4,9 +4,40 @@ export LC_ALL=C script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) compose_args=(-f "$script_dir/docker-compose.prod.yml") -if (($#)); then - env_file=$1 +env_file= +allow_local_images=false +while (($#)); do + case $1 in + --allow-local-images) + allow_local_images=true + shift + ;; + --) + shift + if (($# != 1)) || [[ -n $env_file ]]; then + echo "usage: $0 [--allow-local-images] [env-file]" >&2 + exit 2 + fi + env_file=$1 + shift + ;; + -*) + echo "unknown option: $1" >&2 + exit 2 + ;; + *) + if [[ -n $env_file ]]; then + echo "usage: $0 [--allow-local-images] [env-file]" >&2 + exit 2 + fi + env_file=$1 + shift + ;; + esac +done +if [[ -n $env_file ]]; then set -a + # shellcheck disable=SC1090 source "$env_file" set +a compose_args=(--env-file "$env_file" "${compose_args[@]}") @@ -112,6 +143,13 @@ while IFS= read -r image; do if [[ $image =~ @sha256:[0-9a-fA-F]{64}$ ]]; then continue fi + if [[ $allow_local_images == true ]]; then + if ! docker image inspect "$image" >/dev/null 2>&1; then + echo "local production image is not loaded: $image" >&2 + exit 1 + fi + continue + fi echo "production image must be pinned to a sha256 digest: $image" >&2 exit 1 done <<< "$images" diff --git a/deploy/docker/preflight_test.sh b/deploy/docker/preflight_test.sh index eef3fa79..58464482 100755 --- a/deploy/docker/preflight_test.sh +++ b/deploy/docker/preflight_test.sh @@ -101,6 +101,8 @@ printf '%s\n' 'https://alerts.example.test/gochat' >"$ALERTMANAGER_WEBHOOK_URL_F TEST_MUTABLE_IMAGE=1 export TEST_MUTABLE_IMAGE expect_failure 'a mutable production image' 'must be pinned to a sha256 digest' +"$root/deploy/docker/preflight.sh" --allow-local-images > "$tmp/output" 2>&1 +grep -F 'production preflight passed' "$tmp/output" >/dev/null unset TEST_MUTABLE_IMAGE export GOCHAT_DATABASE_DSN='postgres://external_user:external_password@db.example.test:5432/gochat?sslmode=disable' diff --git a/scripts/build-production.sh b/scripts/build-production.sh index f10971dd..a482f3ab 100644 --- a/scripts/build-production.sh +++ b/scripts/build-production.sh @@ -80,6 +80,7 @@ files=( deploy/docker/database_client_entrypoint.sh deploy/docker/database_host_rejection_cases.txt deploy/docker/preflight.sh + deploy/docker/deploy.sh deploy/fluentd/fluent.conf deploy/prometheus/alertmanager.yml deploy/prometheus/blackbox.yml