Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Ubuntu Bash Scripting Guide

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.

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/ with chmod 700
  • Deploy hooks: beside your deploy.php or 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.

Ubuntu Bash Script LifecycleWrite Script.sh in editorShellChecklint + fixchmod +xmake executableRun / Cronmanual or autoProduction Safety Layerset -euo pipefailQuoted variables + explicit exit codesLog to /var/log/ with rotationTest as cron user before scheduling
Ubuntu Bash Scripting Guide lifecycle: write, lint, permission, then schedule with safety flags enabled.

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"
set -euo pipefail Breakdown-e errexitExit on any command failure-u nounsetError on unset variables-o pipefailCatch failures in pipesWithout flagsSilent partial failuresResult: script stops before corrupt backup or bad deployAlways combine with quoted vars and trap cleanup handlers
Strict mode in this Ubuntu Bash Scripting Guide prevents silent failures during pipes and unset variables.

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.

  1. Backups — database dumps, tarball rotation, off-site rsync
  2. Health checks — disk, queue workers, SSL expiry, HTTP smoke tests
  3. Deploy helpers — cache clears, permission fixes, PHP-FPM reload
  4. 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.

PatternDev / quick fixProduction script
Error handlingIgnore failuresset -euo pipefail + explicit exit
VariablesUnquoted $varAlways "$var", defaults via ${var:-default}
ConfigHard-coded pathsEnv file or /etc/default/appname
ConcurrencyRun twice manuallyLock file with flock
SecretsPassword in script.env, systemd credentials, or vault
Loggingecho onlyTimestamped 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.

When to Use Bash vs Python on UbuntuBash — best fitCron backupsDeploy hooksGlue for CLI toolsQuick server fixesPython — best fitComplex JSON APIsHeavy data parsingLarge test suitesShared librariesRule of thumb for Ubuntu adminsIf it is mostly calling existing CLI tools — use bashIf it needs structured logic — consider PythonKeep bash scripts under ~200 lines
Ubuntu Bash Scripting Guide decision chart: bash for glue and cron, Python when complexity grows.

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.sh after 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-data for app scripts. Root-owned uploads break Laravel storage.
  • No log rotation: Verbose cron output fills /var/log. Use logrotate or truncate old logs.
  • Parsing ls: Never for f in $(ls *.log). Use globs or find with -print0.
  • Missing dependency checks: Test for mysqldump, php, or curl before 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.

Production Bash Script Use CasesNightly DB Dumpcron + gzip + rotateDeploy Hookcache + fpm reloadSSL Renewalcertbot + nginx testLaravel + Ubuntu Server StackApache/Nginx + PHP-FPM 8.3/8.4 + MySQL 8.4Bash scripts tie cron, Deployer, and monitoring togetherUsed on booking + legal-tech production sitesSee portfolio: Adventure Third Pole Trek deployment
Ubuntu Bash Scripting Guide applied to Laravel stacks: backups, deploy hooks, and certificate renewal on Ubuntu servers.

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 bash and set -euo pipefail.
  • Quote all variable expansions and test scripts with ShellCheck before scheduling.
  • Simulate cron's minimal environment with env -i before 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

An Ubuntu Bash Scripting Guide teaches you to write shell scripts that run reliably on Ubuntu 22.04 and 24.04 production servers. It targets developers and Linux admins who automate backups, deploy hooks, health checks, and log maintenance on real boxes — the same tasks used daily for Laravel deployments, nightly database dumps, and Deployer-style release workflows. The focus is scripts that fail visibly, log clearly, and survive unattended cron runs at 2 AM.

Create a plain text file with a shebang line, enable strict error handling, then mark it executable and run it from the terminal or cron. Start with this skeleton: #!/usr/bin/env bash, set -euo pipefail, IFS set to newline and tab, a SCRIPT_DIR variable resolved from the script path, a timestamped log function, and a main function that wraps your commands. Save system-wide utilities in /usr/local/bin, project scripts under /var/www/example.com/scripts/, and root-only maintenance in /root/bin with chmod 700. Test with ShellCheck before scheduling.

It turns on strict mode: -e exits on any command failure, -u errors on unset variables, and pipefail makes a pipeline fail if any command in the pipe fails — not just the last one.

Ubuntu links /bin/sh to dash, a lightweight POSIX shell that lacks bash arrays, [[ ]] tests, and brace expansion. Use #!/usr/bin/env bash when you need those features. #!/bin/bash works on most Ubuntu systems but env bash is safer across containers and different install paths. Only use #!/bin/sh when you deliberately target dash POSIX behaviour.

Follow predictable conventions: system-wide utilities go in /usr/local/bin/script-name, project-specific scripts in /var/www/example.com/scripts/, root-only maintenance in /root/bin with chmod 700, and deploy hooks beside your deploy.php or GitLab CI config. Predictable locations matter when multiple people maintain the same server six months later — ad-hoc paths in home directories get lost during handoffs.

Always double-quote variable expansions like "$VAR" and use [[ ]] for tests inside bash scripts. Know positional arguments ($1, $2, "$@"), conditionals with file and directory checks, for and while loops, and functions that return exit status via return. The script's final exit code matters for cron email alerts and CI pipelines. Understand file permissions before scripts that chown web roots — wrong ownership is a top cause of post-deploy Laravel failures on Ubuntu servers.

Run with tracing enabled using bash -x ./scripts/backup.sh or temporarily add set -x inside the script — then remove it before scheduling because verbose logs fill disks fast. Install ShellCheck with apt and run shellcheck -x on every script; treat warnings as blockers on scripts touching databases or payment callbacks. Simulate cron's minimal environment with sudo -u www-data env -i and an explicit PATH before going live, because a script that works in SSH may fail at 2 AM when mysqldump is not found.

Cron runs with a minimal PATH that lacks binaries your interactive shell finds automatically. Set full binary paths inside scripts — for example MYSQLDUMP="/usr/bin/mysqldump" and PHP="/usr/bin/php8.3" — or export PATH at the top of the script and in the cron file itself. Test by running the script under env -i with a stripped environment matching what cron actually sees. Redirect both stdout and stderr to a log file because cron only emails root on non-zero exits when MAILTO is set.

For recurring tasks, add a cron entry in /etc/cron.d/ with SHELL=/bin/bash, an explicit PATH, the run schedule, the target user, and output redirected to a log file. Example: run a backup as www-data at 2 AM daily with >> /var/log/app/backup.log 2>&1. For boot-time or long-running tasks, use a systemd service unit in /etc/systemd/system/ with ExecStart pointing to your script, then enable it with systemctl. Cron @reboot works for one-shot boot scripts but offers less logging control than systemd.

Production scripts use set -euo pipefail, always quoted variables with defaults via ${var:-default}, config read from /etc/default/appname instead of hard-coded paths, flock lock files to prevent overlapping cron runs, trap handlers for cleanup on failure, and timestamped logs under /var/log/. Use the : "${VAR:?message}" idiom to exit with a clear error if a required variable is empty. Without flock, a slow backup plus cron overlap can corrupt a half-written dump file. Always call artisan up in a trap if artisan down ran earlier during deploy maintenance.

Use a script with set -euo pipefail, variables for DB_NAME, BACKUP_ROOT, and RETAIN_DAYS, then run mysqldump --single-transaction --routines piped to gzip -9. Name files with a date stamp like ${DB_NAME}_${STAMP}.sql.gz, mkdir -p the backup directory, and delete files older than RETAIN_DAYS with find and -mtime. Pair this with off-site rsync from the article's backup strategies and test restore monthly — a backup script that never restores is wishful thinking. Add flock locking so overlapping runs cannot corrupt the dump.

Set APP_DIR to /var/www/current and PHP_BIN to /usr/bin/php8.3, cd into the app directory, then run artisan down --retry=60, queue:restart, config:cache, route:cache, and view:cache before artisan up. Run all artisan commands as the web user with sudo -u www-data, never as root — root-owned uploads break Laravel storage. On sites using Deployer 7, post-deploy hooks call scripts like this after the symlink swap. Wrap artisan down/up pairs in a trap so maintenance mode is cleared even if a cache command fails mid-script.

The recurring failures are Windows CRLF line endings breaking shebangs — fix with dos2unix, assuming cron inherits your user PATH, running app scripts as root instead of www-data, verbose cron output filling /var/log without rotation, parsing ls output in loops instead of globs or find -print0, and skipping dependency checks for mysqldump, php, or curl before use. Never curl bash installers from the internet on production. After writing automation, wire monitoring because a script exiting 0 but producing empty backups still needs alerting.

Bash suits single-server cron jobs, quick deploy hooks, and glue around existing CLI tools like mysqldump, artisan, and rsync. Ansible scales better when you manage many servers and need idempotent playbooks with inventory. Many teams use both — bash locally for project-specific deploy hooks and backup rotation, Ansible for provisioning and multi-server configuration. Keep individual bash scripts under roughly 200 lines; reach for Python when JSON APIs or complex parsing dominate the workload.

Store secrets outside the script in .env files, /etc/default/ config, systemd credentials, or your CI secret store — never hard-code passwords. Pin versions and verify checksums instead of piping curl into bash from the internet. Run application scripts as www-data with sudo -u, not root. Follow Ubuntu security hardening and server hardening guides before layering automation. Set explicit binary paths, use flock to prevent concurrent runs, and add monitoring so empty or failed backups trigger alerts even when exit codes look successful.

Share this article

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.

Quick Contact Options
Choose how you want to connect me: