What you will learn

Drive the same deploy script against dev, staging, and production by reading each environment's settings from a config file, instead of maintaining three near-duplicate scripts that drift apart.

Before you begin

Keep secrets (API tokens, passwords) out of the config file itself; reference an external secret store or environment variable name instead. This example uses a simple key=value file per environment.

1. Define one config file per environment

# config/production.env
APP_HOST="prod01.internal"
APP_USER="deploy"
RELEASE_DIR="/opt/myapp/releases"
HEALTH_URL="https://myapp.internal/healthz"
REQUIRE_APPROVAL="true"

2. Load it defensively — never source untrusted input blindly

#!/usr/bin/env bash
set -Eeuo pipefail

ENV_NAME="${1:?usage: deploy.sh <environment>}"
CONFIG_FILE="config/${ENV_NAME}.env"

[[ -f "$CONFIG_FILE" ]] || { echo "no config for '${ENV_NAME}'" >&2; exit 1; }

# Allow lines matching KEY=VALUE only — reject anything else before sourcing.
if grep -qvE '^\s*(#.*)?$|^[A-Za-z_][A-Za-z0-9_]*=.*$' "$CONFIG_FILE"; then
    echo "refusing to load ${CONFIG_FILE}: unexpected content" >&2
    exit 1
fi

# shellcheck source=/dev/null
source "$CONFIG_FILE"

The grep -qv guard rejects a config file containing anything beyond simple assignments — command substitution, function calls — before it's ever sourced, closing the classic "config file as arbitrary code execution" hole.

3. Gate risky environments behind an explicit confirmation

if [[ "${REQUIRE_APPROVAL:-false}" == "true" ]]; then
    read -r -p "Deploy to ${ENV_NAME} (${APP_HOST})? Type the environment name to confirm: " confirm
    [[ "$confirm" == "$ENV_NAME" ]] || { echo "confirmation mismatch, aborting" >&2; exit 1; }
fi

echo "deploying to ${ENV_NAME} (${APP_HOST}) as ${APP_USER}"
ssh "${APP_USER}@${APP_HOST}" "mkdir -p ${RELEASE_DIR}/$(date -u +%Y%m%d%H%M%S)"
# ... rsync release, run migrations, restart service ...

if retry_curl="$(curl -fsS --max-time 5 "$HEALTH_URL")"; then
    echo "post-deploy health check ok: ${retry_curl}"
else
    echo "post-deploy health check FAILED" >&2
    exit 1
fi

Requiring the operator to type the environment's exact name (not just "y") is a deliberate friction point for production — it stops a fat-fingered deploy.sh production typed while meaning staging.

Example output

Deploy to production (prod01.internal)? Type the environment name to confirm: production
deploying to production (prod01.internal) as deploy
post-deploy health check ok: {"status":"ok"}

Verify the result

Run against a throwaway dev config first end-to-end. Confirm the config-content guard actually rejects a file containing $(rm -rf /) before trusting it with production credentials.

Troubleshooting

If source fails with "command not found," the guard's regex is too strict for a legitimate value containing = inside quotes — extend the allowed pattern deliberately rather than removing the guard.

Next steps

Route deploy failures through webhook-alert-dispatcher, and consider staged-pipeline-runner once the deploy grows beyond a single linear script.