Bash as Glue Code Between PHP, SQL, Docker and Git
AI generated
Bash · PHP · Docker · Git · SQL · DevOps
Bash as glue code between
PHP, SQL, Docker and Git

No single tool masters every aspect of a modern deployment stack. Bash as glue code connects PHP scripts, SQL dump workflows, Docker build chains and Git hooks into one coherent automation system, with robust exit code handling, targeted data forwarding and clear error escalation that surfaces problems early.

15 min read PHP · MySQL · Docker · Git · Exit Codes · Pipes Bash 4.x · 5.x · Linux · CI/CD

1. What glue code delivers and where it fails

Bash as glue code is not a programming language for complex logic, it is an orchestration tool for connecting programs that each master their own domain. PHP is good for application logic and database access, MySQL for relational operations, Docker for container management, Git for version control. Bash as glue code connects these tools, forwards data between them, evaluates their results and makes simple control decisions based on exit codes.

Where Bash as glue code fails: when the orchestration logic itself becomes complex. Complex conditional logic, deep data structure manipulation and extensive error handling across many tools are signs that the script should migrate to a higher level language, Python, Go or a dedicated build tool. The failure of Bash as glue code is rarely a technical matter, it is almost always a signal that the abstraction level no longer fits. The boundary is subjective but practical: once a script grows past 300 lines and mostly contains logic rather than orchestration, the discussion is worth having.

The most important principle for Bash as glue code: exit codes are the only reliable communication between processes. A tool that exits with code 0 has reported success. Anything else is an error. Bash must catch these codes, evaluate them and escalate them. Without set -euo pipefail and explicit exit code handling, Bash as glue code is deaf to the error messages of its children.

2. Exit code handling: the language of tools

Exit codes are the protocol between Bash glue code and the tools it calls. Code 0 means success, everything else means failure, though some tools use differentiated codes: grep returns 0 on a match, 1 on no match, 2 on an error. The pattern grep -q "pattern" file || { echo "Pattern not found"; exit 1; } does not distinguish between "no match" and "file not readable". The correct glue code pattern checks the exit code and evaluates it against the known semantics: exit_code=$?; if (( exit_code == 1 )); then echo "no match"; elif (( exit_code == 2 )); then echo "error"; fi.

In a pipe, Bash by default loses every exit code except the one from the last command. The array PIPESTATUS holds the exit codes of every command in a pipe, in execution order. With set -o pipefail, Bash stops execution if any command in a pipe fails, but PIPESTATUS is still useful for identifying which command in the chain failed. For Bash glue code spanning multiple tools, that is the crucial diagnostic.


#!/usr/bin/env bash
# glue_exit_codes.sh: correct exit-code handling in Bash glue code
set -euo pipefail

# Capture exit code explicitly without triggering set -e
run_with_status() {
  local cmd=("$@")
  local exit_code=0
  "${cmd[@]}" || exit_code=$?
  echo "$exit_code"
}

# PHP CLI with exit-code check
php_exit=$(run_with_status php bin/magento setup:upgrade 2>&1 | tee /tmp/magento.log; echo "${PIPESTATUS[0]}")
if [[ "$php_exit" != "0" ]]; then
  echo "[ERROR] Magento setup:upgrade failed (exit $php_exit)" >&2
  exit "$php_exit"
fi

# Check PIPESTATUS after a pipeline, which step failed?
mysqldump --single-transaction magento 2>/dev/null | gzip -9 > /backup/magento.sql.gz
dump_status="${PIPESTATUS[0]}"
gzip_status="${PIPESTATUS[1]}"

if (( dump_status != 0 )); then
  echo "[ERROR] mysqldump failed with code $dump_status" >&2; exit 1
fi
if (( gzip_status != 0 )); then
  echo "[ERROR] gzip compression failed with code $gzip_status" >&2; exit 1
fi
echo "[OK] Database backup completed successfully"

3. Bash and PHP: orchestrating CLI scripts

PHP applications like Magento 2 expose powerful tools for deployment tasks through their CLI commands (bin/magento), which get orchestrated by Bash as glue code. The pattern: Bash calls PHP commands in the right order, checks every exit code, collects output in a log and decides on the next step based on the result. What matters here is that PHP communicates through exceptions and error output that must land in Bash logs, without pulling PHP output into Bash logic or parsing it wherever that can be avoided.

A common mistake in Bash glue code for PHP: parsing PHP output with grep and awk to determine whether a command succeeded. That is fragile because output formats change and can be localized. The exit code is always more reliable. If a PHP script does not deliver a reliable exit code, that is a bug in the PHP script that should be fixed there, not worked around in Bash glue code through output parsing. Respecting this boundary between layers is the foundation of maintainable Bash glue code.

4. SQL dumps, imports and database migrations

Database operations are a classic use case for Bash as glue code: create a dump, compress it, transfer it elsewhere, import it and run migrations, each of these operations is its own standalone tool that Bash connects. The basic pattern for a safe dump: mysqldump --single-transaction --routines --triggers dbname | gzip -9 > dump.sql.gz followed by a PIPESTATUS check. --single-transaction is essential for InnoDB tables to produce consistent snapshots without exclusive locks.

For imports in Bash glue code: always check the target database first, then catch the import with an explicit exit code. The pattern mysql -e "SELECT 1" dbname >/dev/null 2>&1 || { echo "DB not accessible"; exit 1; } checks reachability and access rights before an hours long import begins. After the import, check the row count in critical tables against expected minimums, a structurally correct import with 0 rows in the product table is an error that exit code 0 will not catch.


#!/usr/bin/env bash
# db_glue.sh: Bash glue code for database operations
set -euo pipefail

DB_HOST="${DB_HOST:-localhost}"
DB_USER="${DB_USER:?DB_USER not set}"
DB_PASS="${DB_PASS:?DB_PASS not set}"
DB_NAME="${DB_NAME:?DB_NAME not set}"
BACKUP_DIR="${BACKUP_DIR:-/backups/db}"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)

# MySQL connection test before any operation
mysql_args=(-h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" --batch --skip-column-names)
mysql "${mysql_args[@]}" -e "SELECT 1" "$DB_NAME" >/dev/null 2>&1 \
  || { echo "[ERROR] Cannot connect to database $DB_NAME" >&2; exit 1; }

# Dump with pipeline exit-code validation
DUMP_FILE="$BACKUP_DIR/${DB_NAME}-${TIMESTAMP}.sql.gz"
mysqldump "${mysql_args[@]}" --single-transaction --routines \
  "$DB_NAME" 2>/tmp/dump.err | gzip -9 > "$DUMP_FILE"

# Check both steps in the pipeline
if (( PIPESTATUS[0] != 0 )); then
  echo "[ERROR] mysqldump failed: $(cat /tmp/dump.err)" >&2; exit 1
fi

# Verify dump integrity
actual_size=$(stat --format="%s" "$DUMP_FILE")
(( actual_size < 1024 )) && { echo "[ERROR] Dump suspiciously small: $actual_size bytes" >&2; exit 1; }
echo "[OK] Dump written: $DUMP_FILE (${actual_size} bytes)"

# Row count verification after import
check_table_count() {
  local table="$1" min_rows="$2"
  local count
  count=$(mysql "${mysql_args[@]}" -e "SELECT COUNT(*) FROM $table" "$DB_NAME")
  (( count < min_rows )) && { echo "[ERROR] Table $table: $count rows < $min_rows expected" >&2; return 1; }
  echo "[OK] Table $table: $count rows"
}

check_table_count "catalog_product_entity" 1
check_table_count "customer_entity" 0

5. Orchestrating Docker workflows in Bash

Orchestrating Docker commands in Bash as glue code is a common use case in CI/CD pipelines and deployment scripts. The basic pattern: check container status, run the build, replace the container, wait for a health check. Every one of these steps produces exit codes and often output that must be evaluated. Waiting for container readiness is a particularly common stumbling block: docker run returns exit code 0 as soon as the container has started, not once it is fully up. Bash glue code must wait for real readiness with a retry loop.

The pattern for container readiness in Bash glue code: a loop with a configurable number of attempts and wait time that runs a specific health check, not just docker ps, but an actual connectivity test via docker exec or an HTTP request. For Magento this could be a docker exec app php bin/magento list call that confirms the PHP application is responsive. Only proceed to the next deployment step once readiness is confirmed.


#!/usr/bin/env bash
# docker_glue.sh: Bash glue code for Docker orchestration
set -euo pipefail

IMAGE_NAME="${1:?Usage: $0 IMAGE_NAME}"
CONTAINER_NAME="app"
HEALTH_TIMEOUT=60
HEALTH_INTERVAL=3

# Build with streaming output, capture exit code
echo "[INFO] Building image: $IMAGE_NAME"
if ! docker build -t "$IMAGE_NAME" .; then
  echo "[ERROR] Docker build failed" >&2; exit 1
fi

# Stop and remove existing container if running
if docker ps -q --filter "name=$CONTAINER_NAME" | grep -q .; then
  echo "[INFO] Stopping existing container: $CONTAINER_NAME"
  docker stop "$CONTAINER_NAME" >/dev/null
  docker rm "$CONTAINER_NAME" >/dev/null
fi

# Start new container
docker run -d \
  --name "$CONTAINER_NAME" \
  --restart unless-stopped \
  -e "APP_ENV=${APP_ENV:-production}" \
  "$IMAGE_NAME"

# Wait for container readiness, not just running, but healthy
echo "[INFO] Waiting for container readiness (max ${HEALTH_TIMEOUT}s)..."
elapsed=0
until docker exec "$CONTAINER_NAME" php -r "echo 'ok';" 2>/dev/null | grep -q "ok"; do
  sleep "$HEALTH_INTERVAL"
  elapsed=$((elapsed + HEALTH_INTERVAL))
  if (( elapsed >= HEALTH_TIMEOUT )); then
    echo "[ERROR] Container $CONTAINER_NAME not ready after ${HEALTH_TIMEOUT}s" >&2
    docker logs --tail 50 "$CONTAINER_NAME" >&2
    exit 1
  fi
  echo "[INFO] Still waiting... (${elapsed}s)"
done
echo "[OK] Container $CONTAINER_NAME is ready after ${elapsed}s"

6. Git hooks and automated Git workflows

Git hooks are shell scripts that Git invokes at certain points in the workflow, making them a natural home for Bash as glue code. The pre-commit hook runs before every commit and can run PHP syntax checks, PHPStan analysis or PHPCS checks. The pre-push hook can run tests before code reaches the remote repository. The post-receive hook on the server can trigger deployments. In every case the exit code decides the outcome: code 0 lets Git continue, anything else aborts the operation.

An important caveat for Bash glue code in Git hooks: hooks are not automatically versioned or shared when they live in the .git/hooks/ directory. The standard pattern for shared hooks in teams: store hooks in a versioned directory inside the repository (.githooks/) and enable them with git config core.hooksPath .githooks. That way hook scripts live in the repository, get updated with git pull, and can be reviewed in code reviews, just like any other glue code.

7. Forwarding data between processes

Data forwarding is the core competency of Bash as glue code. Pipes, redirections and process substitution determine how the output of one tool becomes the input of the next. The simplest pattern, cmd1 | cmd2, has the well known downside of losing the exit code without pipefail. More complex forwarding with tee allows sending one output to a file and to another process at the same time: mysqldump dbname | tee >(gzip > backup.gz) | md5sum > backup.md5. Process substitution lets the dump be stored compressed and checksummed at the same time, with no intermediate file.

Another powerful tool in Bash glue code for data forwarding: coproc. A coprocess runs as a background process and provides bidirectional communication through file descriptors. That makes it possible to start a long lived process and repeatedly exchange data with it, without forking a new child process for every request. For database connections, where every new connection creates overhead, a coproc mysql can keep the connection open across many queries.

8. Error escalation: from Bash to monitoring

Errors in Bash glue code need to produce more than local log entries. In a production stack, critical errors are expected to trigger alerts in monitoring systems, send Slack messages or create tickets. The standard pattern: a notify_failure function inside a trap cleanup EXIT routine that fires on a non zero exit code. Inside it, an HTTP webhook can be called (curl -X POST -d "..." https://hooks.slack.com/...), an email sent, or a monitoring system informed via API.

The pattern for structured error escalation in Bash glue scripts: collect error context (which phase failed, which exit code, which recent log lines) and forward it in a structured format. A JSON payload built with printf '{"status":"error","phase":"%s","exit_code":%d,"log":"%s"}\n' "$PHASE" "$EXIT_CODE" "$LOG_EXCERPT" lets monitoring systems parse the error automatically. The difference between "Deployment failed" and "Deployment failed in phase 'setup:upgrade', exit code 127, last line: Class X not found" is significant in production.

9. Glue code approaches compared

There are several approaches to orchestrating PHP, SQL, Docker and Git, and Bash as glue code is not always the best choice, but it is often the simplest.

Approach Strengths Weaknesses Recommendation
Bash glue code Available everywhere, no deps Complex for logic, hard to debug Up to about 200 lines of orchestration
Makefile Dependency graph, parallel No loops, quoting pitfalls Build targets, not flows
Python script Full language, testable Python version, venv required Complex logic, API calls
Ansible playbook Idempotent, declarative Overhead, YAML complexity Infrastructure configuration
CI/CD YAML Integrated, visual Only executable inside the pipeline For pipeline specific steps

In practice, the best architecture for complex deployments combines Bash glue code for simple orchestration (command sequencing, exit code checking, log collection) with a higher level tool for more complex logic. A Bash wrapper around a Python deployment script that sets environment variables, collects logs and escalates errors is often more maintainable than a monolithic Bash script that does everything itself.

Mironsoft

Deployment infrastructure, Bash automation and DevOps tooling

Deployment scripts that reliably connect PHP, Docker and Git?

We build Bash glue code that connects PHP applications, database operations, Docker workflows and Git hooks into one coherent deployment system, with complete exit code handling, structured logging and automatic error escalation.

Deployment scripts

Automate Magento/PHP deployments with robust glue code

DB orchestration

Dump, compression, import and migration workflows in Bash

Git hook integration

Pre-commit and pre-push hooks for PHP quality assurance

10. Summary

Bash as glue code between PHP, SQL, Docker and Git is most effective when it stays focused on its core job: controlling command order, evaluating exit codes and forwarding data between processes. set -euo pipefail and PIPESTATUS are the technical foundation for reliable exit code handling. Process substitution with >(cmd) enables simultaneous data forwarding without intermediate files. Readiness loops for Docker containers replace blind waiting after docker run. Git hooks stored in versioned directories turn quality assurance into an automatic part of the workflow.

The boundary of Bash as glue code is the complexity of the orchestration logic. Once a script starts managing complex conditions across many states or parsing deep into the output of the tools it calls, it is time to reassess the tool. Combining Bash for simple orchestration with a specialized tool for complex logic, an Ansible playbook, a Python script or a CI/CD system, is often the most maintainable architecture.

Bash as Glue Code: The Essentials at a Glance

Exit codes

PIPESTATUS[@] after pipes. set -o pipefail as the baseline. Understand exit code semantics per tool, grep 1 is not an error.

PHP & SQL

Do not parse output, evaluate exit codes instead. Test the DB connection before the dump. Verify row counts after import.

Docker

docker run returns exit code 0 on start, not on readiness. A retry loop with a real health check is mandatory.

Git hooks

Version hooks in .githooks/, set git config core.hooksPath. A nonzero exit aborts git commit/push.

11. FAQ: Bash as Glue Code Between PHP, SQL, Docker and Git

1What is Bash glue code?
A shell script that calls PHP CLI, MySQL, Docker and Git in the right order, evaluates exit codes and forwards data. Not application logic, orchestration.
2Reading exit codes from a Bash pipe?
PIPESTATUS[@] holds the exit codes of all pipe commands: cmd1 | cmd2; echo ${PIPESTATUS[0]} ${PIPESTATUS[1]}. set -o pipefail aborts on failure. PIPESTATUS shows which command failed.
3Waiting for real Docker readiness?
docker run exits 0 on start, not on readiness. A retry loop with a real health check is needed: docker exec container curl -sf localhost/health. On timeout: print docker logs and abort with a nonzero exit.
4Avoiding PHP output parsing?
Exit codes are more reliable than output parsing. No string matching on PHP output. Let PHP set correct exit codes. In the Bash script, only evaluate: php cmd; (( $? != 0 )) && exit 1.
5Versioning Git hooks?
Store hooks in .githooks/, set git config core.hooksPath .githooks. They get updated with git pull and are visible in code review. Do not store them in .git/hooks/, that is not versioned.
6When is Bash no longer suitable?
From around 200 lines with complex logic, deep output parsing or testability requirements. Then evaluate Python, Go or a CI/CD system. Bash remains ideal for sequential command chains with clear exit codes.
7Sending error alerts to Slack?
In trap cleanup EXIT: on exit_code != 0, curl -X POST a JSON payload to the webhook. Include phase, exit code and log excerpt. Every production error gets escalated automatically.
8Testing a MySQL connection before import?
mysql -e 'SELECT 1' $DB >/dev/null 2>&1 || { echo 'DB not accessible'; exit 1; }. Checks network, credentials and DB existence in one step. Always run before a long import.
9Forwarding data into two processes at once?
tee with process substitution: cmd | tee >(gzip > backup.gz) >(sha256sum > backup.sha). Process substitution >() starts processes and exposes virtual files. No temp file needed.
10Building a robust Magento deployment glue?
Phases: DB backup, code update, composer, setup:upgrade, static deploy, cache flush, health check. Each phase as a function. On failure: rollback in trap cleanup EXIT. Every phase with an exit code check.