
September 11, 2026
12 min read
By Kokil Thapa | Last reviewed: September 2026
A broken backup cron job or a deploy script that silently fails can take down a production Laravel site faster than a bad PHP upgrade. This Ubuntu Bash Scripting Guide walks you through writing scripts that actually survive real Ubuntu 22.04 and 24.04 servers — the kind I use daily for Linux system administration, nightly database dumps, and Deployer-style release workflows. You will learn syntax that matters, error handling that catches failures, and patterns that keep scripts maintainable six months later.
#!/usr/bin/env bash, set -euo pipefail, quoted variables, explicit exit codes, and scripts stored in /usr/local/bin or project scripts/ folders — then tested with ShellCheck before cron or CI runs them.How Do You Write Your First Bash Script on Ubuntu?
Every bash script on Ubuntu is a plain text file interpreted by the Bourne Again Shell. You do not compile it. You mark it executable and run it from the terminal or a scheduler like cron.
Start with a minimal skeleton that fails loudly instead of hiding errors:
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOG_FILE="/var/log/myapp/backup.log"
log() {
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" | tee -a "$LOG_FILE"
}
main() {
log "Starting backup"
# your commands here
log "Backup finished"
}
main "$@" Choose the right shebang
On Ubuntu, #!/bin/bash works on most systems. Prefer #!/usr/bin/env bash when scripts may run across different paths or containers. Avoid #!/bin/sh unless you deliberately target POSIX dash behaviour — Ubuntu links /bin/sh to dash, which lacks bash arrays and [[ ]] tests.
Save scripts in a predictable location
Use these conventions on production boxes:
- System-wide utilities:
/usr/local/bin/script-name - Project scripts:
/var/www/example.com/scripts/ - Root-only maintenance:
/root/bin/withchmod 700 - Deploy hooks: beside your
deploy.phpor GitLab CI config
If you are new to the terminal itself, read the essential Ubuntu terminal commands article first. It covers navigation and file operations this guide assumes you already know.
What Bash Syntax Should Every Ubuntu Admin Know?
Bash on Ubuntu is forgiving — until a space inside an unquoted variable wipes a directory. These constructs cover ninety percent of server automation work.
Variables, arguments, and quoting
APP_NAME="laravel-app"
BACKUP_DIR="/var/backups/${APP_NAME}"
TODAY="$(date +%F)"
# Positional args: $1 $2 ... all args: "$@"
if [[ $# -lt 1 ]]; then
echo "Usage: $0 <environment>" >&2
exit 1
fi
ENV="$1"
FILE_COUNT="$(find "$BACKUP_DIR" -type f -name "*.sql.gz" | wc -l)" Always double-quote variable expansions: "$VAR". Use [[ ]] for tests inside bash scripts. For string matching in log parsers, test patterns in the regex tester tool before embedding them.
Conditionals and loops
if [[ -f "/etc/letsencrypt/live/example.com/fullchain.pem" ]]; then
echo "Certificate exists"
elif [[ -d "$BACKUP_DIR" ]]; then
echo "Backup dir ready"
else
echo "Missing prerequisites" >&2
exit 1
fi
for site in /var/www/*/; do
[[ -d "$site" ]] || continue
echo "Processing $site"
done
while IFS= read -r line; do
echo "$line"
done < /var/log/nginx/access.log Functions and return codes
Functions return exit status via return. The script's final exit code matters for cron email alerts and CI pipelines.
check_disk() {
local threshold="${1:-90}"
local usage
usage="$(df / --output=pcent | tail -1 | tr -dc '0-9')"
if (( usage >= threshold )); then
echo "Disk usage ${usage}% exceeds ${threshold}%" >&2
return 1
fi
return 0
}
check_disk 85 || exit 2 Understand Ubuntu file permissions before scripts that chown web roots or rotate logs. Wrong ownership is a top cause of post-deploy Laravel failures.
How Do You Debug and Test Bash Scripts on Ubuntu?
Never deploy an untested script straight into root cron. Build a short feedback loop first.
Run with tracing enabled
bash -x ./scripts/backup.sh staging
# or inside the script temporarily:
set -x Tracing prints every expanded command. Remove set -x before production scheduling — verbose logs fill disks fast.
Install and run ShellCheck
sudo apt update
sudo apt install -y shellcheck
shellcheck -x scripts/backup.sh ShellCheck catches unquoted variables, useless cat, and deprecated syntax. The ShellCheck wiki explains each warning in plain language. Treat warnings as blockers on scripts that touch databases or payment callbacks.
Simulate the cron environment
Cron runs with a minimal PATH. A script that works in your SSH session may fail at 2 AM because mysqldump is not found.
sudo -u www-data env -i HOME=/var/www \
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
/var/www/example.com/scripts/backup.sh This mirrors what Ubuntu cron jobs actually see. Set explicit paths inside scripts when in doubt:
MYSQLDUMP="/usr/bin/mysqldump"
PHP="/usr/bin/php8.3" How Do You Automate Server Tasks With Bash on Ubuntu?
Most bash scripts I maintain on Ubuntu servers fall into four categories. Each needs slightly different error handling.
- Backups — database dumps, tarball rotation, off-site
rsync - Health checks — disk, queue workers, SSL expiry, HTTP smoke tests
- Deploy helpers — cache clears, permission fixes, PHP-FPM reload
- Log maintenance — truncate, compress, ship to central logging
Example: MySQL backup with retention
#!/usr/bin/env bash
set -euo pipefail
DB_NAME="app_production"
BACKUP_ROOT="/var/backups/mysql"
RETAIN_DAYS=14
STAMP="$(date +%F_%H%M)"
DEST="${BACKUP_ROOT}/${DB_NAME}_${STAMP}.sql.gz"
mkdir -p "$BACKUP_ROOT"
mysqldump --single-transaction --routines "$DB_NAME" | gzip -9 > "$DEST"
find "$BACKUP_ROOT" -name "${DB_NAME}_*.sql.gz" -mtime +"$RETAIN_DAYS" -delete
echo "Backup written to $DEST" Pair this pattern with the strategies in Ubuntu server backup strategies. Test restore monthly — a backup script that never restores is wishful thinking.
Example: Laravel queue and cache maintenance
#!/usr/bin/env bash
set -euo pipefail
APP_DIR="/var/www/current"
PHP_BIN="/usr/bin/php8.3"
cd "$APP_DIR"
$PHP_BIN artisan down --retry=60 || true
$PHP_BIN artisan queue:restart
$PHP_BIN artisan config:cache
$PHP_BIN artisan route:cache
$PHP_BIN artisan view:cache
$PHP_BIN artisan up Run artisan commands as the web user, not root. On sites sharing a Deployer 7 pipeline — like the legal-tech portals I maintain — post-deploy hooks often call a script like this after the symlink swap.
Schedule with cron or systemd timers
# /etc/cron.d/app-backup
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
0 2 * * * www-data /var/www/example.com/scripts/backup.sh >> /var/log/app/backup.log 2>&1 Redirect both stdout and stderr. Cron only emails root on non-zero exits if MAILTO is set — logging to a file is more reliable for small teams.
For deeper automation patterns — idempotency, lock files, retry loops — see bash scripting for DevOps patterns and pitfalls and the companion Ubuntu shell scripting tutorial.
Which Bash Scripting Patterns Work Best for Production Ubuntu Servers?
Scripts that survive handoffs share traits. Ad-hoc one-liners do not.
| Pattern | Dev / quick fix | Production script |
|---|---|---|
| Error handling | Ignore failures | set -euo pipefail + explicit exit |
| Variables | Unquoted $var | Always "$var", defaults via ${var:-default} |
| Config | Hard-coded paths | Env file or /etc/default/appname |
| Concurrency | Run twice manually | Lock file with flock |
| Secrets | Password in script | .env, systemd credentials, or vault |
| Logging | echo only | Timestamped logs under /var/log/ |
Use flock to prevent overlapping runs
#!/usr/bin/env bash
set -euo pipefail
LOCK_FILE="/var/lock/app-backup.lock"
exec 200>"$LOCK_FILE"
flock -n 200 || { echo "Another backup is running"; exit 0; }
# backup commands here Without locking, a slow backup plus cron overlap can corrupt a half-written dump file.
Trap cleanup on failure
cleanup() {
local code=$?
[[ -f "$TEMP_FILE" ]] && rm -f "$TEMP_FILE"
exit "$code"
}
trap cleanup EXIT INT TERM Traps matter when scripts create temp files in /tmp or put Laravel in maintenance mode. Always call artisan up in the trap if artisan down ran earlier.
Read config from a file
# /etc/default/myapp-backup
BACKUP_DIR="/var/backups/myapp"
RETAIN_DAYS=30 [[ -f /etc/default/myapp-backup ]] && source /etc/default/myapp-backup
: "${BACKUP_DIR:?BACKUP_DIR must be set}"
: "${RETAIN_DAYS:?RETAIN_DAYS must be set}" The : "${VAR:?message}" idiom exits with a clear error if a required variable is empty — even with set -u active.
What Are Common Bash Scripting Mistakes on Ubuntu?
These errors show up repeatedly during support and maintenance calls. Most are preventable with checklist discipline.
- Wrong line endings: Windows CRLF breaks shebangs. Run
dos2unix script.shafter editing on Windows. - Assuming PATH: Cron lacks your user PATH. Set full binary paths or export PATH at the top.
- Running as root: Use
sudo -u www-datafor app scripts. Root-owned uploads break Laravel storage. - No log rotation: Verbose cron output fills
/var/log. Uselogrotateor truncate old logs. - Parsing ls: Never
for f in $(ls *.log). Use globs orfindwith-print0. - Missing dependency checks: Test for
mysqldump,php, orcurlbefore use and exit early with a message.
Security matters too. Scripts that curl bash installers from the internet belong nowhere near production. Pin versions, verify checksums, and follow Ubuntu security hardening plus server hardening for Ubuntu web servers.
After writing automation, wire monitoring. A script that exits 0 but produces empty backups needs alerting — see Ubuntu server monitoring for disk and service checks.
For a full server baseline before layering scripts, start with the Ubuntu server setup guide. Install PHP and extensions via the install PHP on Ubuntu walkthrough. Lock down access with UFW firewall configuration and fail2ban setup.
The Ubuntu Server documentation covers package management and service units. For bash language semantics, the GNU Bash manual remains the authoritative reference.
On the Adventure Third Pole Trek booking platform — Laravel and Livewire on Ubuntu — bash scripts handle post-deploy cache warming and log rotation. Same patterns appear across sister sites on shared EC2 infrastructure.
Key Takeaways
- Start every production script with
#!/usr/bin/env bashandset -euo pipefail. - Quote all variable expansions and test scripts with ShellCheck before scheduling.
- Simulate cron's minimal environment with
env -ibefore going live. - Use
flock, traps, and timestamped logs for backup and deploy scripts. - Keep bash under roughly 200 lines; reach for Python when JSON APIs or complex parsing dominate.
- Store secrets outside the script — in
.env,/etc/default/, or your CI secret store.
People Also Ask
What is the difference between bash and sh on Ubuntu?
Ubuntu links /bin/sh to dash, a lightweight POSIX shell. Bash adds arrays, [[ ]], brace expansion, and process substitution. Scripts needing those features must use #!/usr/bin/env bash explicitly — not #!/bin/sh.
How do I run a bash script on startup in Ubuntu?
Use a systemd service unit for long-running or boot-time tasks. Place a .service file in /etc/systemd/system/, reference your script in ExecStart, then run sudo systemctl enable --now your-service. Cron @reboot works for one-shot boot scripts but offers less logging control.
Should I use bash or Ansible for Ubuntu server automation?
Bash suits single-server cron jobs, quick deploy hooks, and glue around existing CLI tools. Ansible scales better when you manage many servers and need idempotent playbooks with inventory. Many teams use both — bash locally, Ansible for provisioning.
Where can I learn more bash scripting for Ubuntu DevOps?
Read the Ubuntu shell scripting tutorial and DevOps patterns guide on this site. Practice on a staging VM, run ShellCheck on every script, and review Ubuntu user management before writing scripts that change ownership or sudo rules.
Put Bash Scripting to Work on Your Ubuntu Servers
A solid Ubuntu Bash Scripting Guide is not about clever one-liners. It is about scripts that fail visibly, log clearly, and survive the 2 AM cron run when nobody is watching. Start with the skeleton in this article, lint with ShellCheck, test under a stripped environment, then schedule with locks and rotation.
If you want help auditing cron jobs, backup scripts, or deploy automation on Ubuntu production boxes, contact us or explore Linux system administration services. You can also browse the full blog for related Ubuntu server performance and apt update guides.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

