Linux Signals: Graceful Shutdowns and Process Control

Your application is running in production. You need to restart it for a config change. Do you: A) kill -9 and hope for the best B) Send a signal it can handle gracefully If you picked A, you’ve probably lost data. Let’s fix that. The Essential Signals Signal Number Default Action Use Case SIGTERM 15 Terminate Graceful shutdown request SIGINT 2 Terminate Ctrl+C, interactive stop SIGHUP 1 Terminate Config reload (by convention) SIGKILL 9 Terminate Force kill (cannot be caught) SIGUSR1/2 10/12 Terminate Application-defined SIGCHLD 17 Ignore Child process state change SIGTERM is the polite ask. “Please shut down when convenient.” SIGKILL is the eviction notice. No cleanup, no saving state, immediate death. ...

February 27, 2026 Â· 5 min Â· 985 words Â· Rob Washington

Cron Jobs That Don't Wake You Up at Night

Cron is deceptively simple. Five fields, a command, done. Until your job runs twice simultaneously, silently fails for a week, or fills your disk with output nobody reads. Here’s how to write cron jobs that actually work in production. The Basics Done Right 1 2 3 4 5 6 7 8 # Bad: No logging, no error handling 0 * * * * /opt/scripts/backup.sh # Better: Redirect output, capture errors 0 * * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1 # Best: Timestamped logging with chronic 0 * * * * chronic /opt/scripts/backup.sh chronic (from moreutils) only outputs when the command fails. Perfect for cron — silent success, loud failure. ...

February 27, 2026 Â· 5 min Â· 896 words Â· Rob Washington

Structured Logging: Stop Grepping, Start Querying

Unstructured logs are a trap. They look simple until you need to find something. [ [ [ 2 2 2 0 0 0 2 2 2 6 6 6 - - - 0 0 0 2 2 2 - - - 2 2 2 7 7 7 0 0 0 5 5 5 : : : 3 3 3 0 0 0 : : : 1 1 1 5 6 7 ] ] ] I E W N R A F R R O O N R U H s F i e a g r i h l j e m o d e h m n t o @ o r e y x p a r u m o s p c a l e g e s e . s c d o o e m r t d e l e c o r t g e g 1 d e 2 : d 3 4 8 i 5 7 n : % f c r o o n m n e 1 c 9 t 2 i . o 1 n 6 8 t . i 1 m . e 5 o 0 u t Quick: find all login failures from a specific IP range in the last hour. Now try parsing the order ID from error messages. Hope you enjoy regex. ...

February 27, 2026 Â· 6 min Â· 1228 words Â· Rob Washington

Shell Scripting Patterns That Prevent 3 AM Pages

Every ops engineer has a story about a shell script that worked perfectly — until it didn’t. Usually at 3 AM. Usually in production. These patterns won’t make your scripts bulletproof, but they’ll stop the most common failures. Start Every Script Right 1 2 3 #!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' This preamble should be muscle memory: set -e: Exit on any error (non-zero return code) set -u: Exit on undefined variables set -o pipefail: Catch errors in pipes (not just the last command) IFS=$'\n\t': Safer word splitting (no spaces) Without these, a typo like rm -rf $UNSET_VAR/ could wipe your root filesystem. ...

February 27, 2026 Â· 5 min Â· 1001 words Â· Rob Washington

Docker Multi-Stage Builds: Smaller Images, Cleaner Deploys

Your Docker images are probably too big. Mine were. Then I learned about multi-stage builds. The problem is simple: build tools bloat production images. You need Node.js and npm to build your React app, but you only need nginx to serve it. You need Go and its toolchain to compile, but the binary runs standalone. Every megabyte of build tooling in your production image is wasted space, slower deploys, and expanded attack surface. ...

February 27, 2026 Â· 4 min Â· 843 words Â· Rob Washington

Mastering systemd Service Units: From First Service to Production-Ready

If you’re running services on Linux, you’re almost certainly using systemd. But there’s a gap between knowing systemctl start nginx and actually writing your own robust service units. Let’s close that gap. The Anatomy of a Service Unit A systemd service unit lives in /etc/systemd/system/ and has three main sections: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 [Unit] Description=My Application Service After=network.target Wants=network-online.target [Service] Type=simple User=appuser Group=appgroup WorkingDirectory=/opt/myapp ExecStart=/opt/myapp/bin/server Restart=on-failure RestartSec=5 [Install] WantedBy=multi-user.target Let’s break down what matters: ...

February 27, 2026 Â· 4 min Â· 715 words Â· Rob Washington

Nginx Configuration Patterns for Modern Web Apps

Nginx is everywhere—reverse proxy, load balancer, static file server, SSL terminator. Here are the configuration patterns that work in production. Base Configuration 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 # /etc/nginx/nginx.conf user nginx; worker_processes auto; error_log /var/log/nginx/error.log warn; pid /run/nginx.pid; events { worker_connections 1024; use epoll; multi_accept on; } http { include /etc/nginx/mime.types; default_type application/octet-stream; # Logging log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for" ' 'rt=$request_time uct=$upstream_connect_time ' 'uht=$upstream_header_time urt=$upstream_response_time'; access_log /var/log/nginx/access.log main; # Performance sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; types_hash_max_size 2048; # Compression gzip on; gzip_vary on; gzip_proxied any; gzip_comp_level 6; gzip_types text/plain text/css text/xml application/json application/javascript application/xml+rss application/atom+xml image/svg+xml; # Security server_tokens off; include /etc/nginx/conf.d/*.conf; } Reverse Proxy Basic Proxy 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 # /etc/nginx/conf.d/app.conf upstream backend { server 127.0.0.1:3000; keepalive 32; } server { listen 80; server_name example.com; location / { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Connection ""; proxy_connect_timeout 60s; proxy_send_timeout 60s; proxy_read_timeout 60s; } } Load Balancing 1 2 3 4 5 6 7 8 9 upstream backend { least_conn; # or: round_robin, ip_hash, hash $request_uri server backend1.local:3000 weight=3; server backend2.local:3000 weight=2; server backend3.local:3000 backup; keepalive 32; } Health Checks (Commercial) 1 2 3 4 5 6 7 # nginx plus only upstream backend { server backend1:3000; server backend2:3000; health_check interval=5s fails=3 passes=2; } SSL/TLS Configuration Modern SSL Setup 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 server { listen 443 ssl http2; server_name example.com; ssl_certificate /etc/nginx/ssl/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/privkey.pem; # Modern configuration ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; ssl_prefer_server_ciphers off; # OCSP Stapling ssl_stapling on; ssl_stapling_verify on; ssl_trusted_certificate /etc/nginx/ssl/chain.pem; resolver 8.8.8.8 8.8.4.4 valid=300s; resolver_timeout 5s; # Session caching ssl_session_timeout 1d; ssl_session_cache shared:SSL:50m; ssl_session_tickets off; # ... rest of config } # Redirect HTTP to HTTPS server { listen 80; server_name example.com; return 301 https://$server_name$request_uri; } Let’s Encrypt with Certbot 1 2 3 4 5 6 7 8 9 10 11 12 server { listen 80; server_name example.com; location /.well-known/acme-challenge/ { root /var/www/certbot; } location / { return 301 https://$server_name$request_uri; } } Security Headers 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 server { # ... SSL config ... # Security headers add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';" always; # HSTS (be careful - hard to undo) add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; # Hide server version server_tokens off; } Rate Limiting 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # Define rate limit zones limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s; limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s; limit_conn_zone $binary_remote_addr zone=addr:10m; server { # General rate limit limit_req zone=general burst=20 nodelay; limit_conn addr 10; location /api/login { limit_req zone=login burst=5 nodelay; proxy_pass http://backend; } location /api/ { limit_req zone=general burst=50 nodelay; proxy_pass http://backend; } } Rate Limit Response 1 2 3 4 5 6 7 8 9 limit_req_status 429; limit_conn_status 429; error_page 429 /429.json; location = /429.json { internal; default_type application/json; return 429 '{"error": "Too many requests"}'; } Caching Proxy Cache 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=app_cache:10m max_size=1g inactive=60m use_temp_path=off; server { location /api/ { proxy_cache app_cache; proxy_cache_valid 200 10m; proxy_cache_valid 404 1m; proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504; proxy_cache_lock on; add_header X-Cache-Status $upstream_cache_status; proxy_pass http://backend; } } Static File Caching 1 2 3 4 5 6 7 8 9 10 11 location /static/ { alias /var/www/static/; expires 1y; add_header Cache-Control "public, immutable"; access_log off; } location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2)$ { expires 30d; add_header Cache-Control "public"; } WebSocket Support 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 map $http_upgrade $connection_upgrade { default upgrade; '' close; } server { location /ws/ { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $host; proxy_read_timeout 86400; } } API Gateway Patterns Path-Based Routing 1 2 3 4 5 6 7 8 9 10 11 12 13 server { location /api/users { proxy_pass http://users-service; } location /api/orders { proxy_pass http://orders-service; } location /api/inventory { proxy_pass http://inventory-service; } } API Versioning 1 2 3 4 5 6 7 8 9 10 11 12 location /api/v1/ { proxy_pass http://api-v1/; } location /api/v2/ { proxy_pass http://api-v2/; } # Default to latest location /api/ { proxy_pass http://api-v2/; } Request/Response Modification 1 2 3 4 5 6 7 8 9 10 11 location /api/ { # Add headers to upstream request proxy_set_header X-Request-ID $request_id; proxy_set_header X-Forwarded-Host $host; # Modify response headers proxy_hide_header X-Powered-By; add_header X-Request-ID $request_id; proxy_pass http://backend; } Static Site Hosting 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 server { listen 80; server_name example.com; root /var/www/html; index index.html; # SPA routing - try file, then directory, then fall back to index location / { try_files $uri $uri/ /index.html; } # Cache static assets location /assets/ { expires 1y; add_header Cache-Control "public, immutable"; } # Deny access to hidden files location ~ /\. { deny all; } # Custom error pages error_page 404 /404.html; error_page 500 502 503 504 /50x.html; } Debugging Debug Logging 1 2 3 4 5 6 7 error_log /var/log/nginx/error.log debug; # Or per-location location /api/ { error_log /var/log/nginx/api-debug.log debug; # ... } Request Inspection 1 2 3 4 location /debug { default_type text/plain; return 200 "Request: $request\nHost: $host\nURI: $uri\nArgs: $args\nRemote: $remote_addr\n"; } Test Configuration 1 2 3 4 nginx -t # Test config syntax nginx -T # Test and dump full config nginx -s reload # Reload configuration nginx -s reopen # Reopen log files Complete Production Example 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 upstream api { least_conn; server api1:3000 weight=2; server api2:3000 weight=2; server api3:3000 backup; keepalive 32; } limit_req_zone $binary_remote_addr zone=api:10m rate=20r/s; proxy_cache_path /var/cache/nginx/api levels=1:2 keys_zone=api_cache:10m max_size=1g; server { listen 80; server_name example.com; return 301 https://$server_name$request_uri; } server { listen 443 ssl http2; server_name example.com; ssl_certificate /etc/nginx/ssl/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_session_cache shared:SSL:10m; # Security add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header Strict-Transport-Security "max-age=31536000" always; # Static files location /static/ { alias /var/www/static/; expires 1y; add_header Cache-Control "public, immutable"; } # API location /api/ { limit_req zone=api burst=50 nodelay; proxy_cache api_cache; proxy_cache_valid 200 1m; add_header X-Cache-Status $upstream_cache_status; proxy_pass http://api; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Connection ""; } # SPA location / { root /var/www/html; try_files $uri $uri/ /index.html; } } Nginx configuration is declarative and predictable once you understand the patterns. Start with a solid base, add features incrementally, and always test with nginx -t before reloading. The patterns above cover 95% of production use cases—adapt them to your needs. ...

February 26, 2026 Â· 7 min Â· 1284 words Â· Rob Washington

Bash Scripting Patterns That Prevent Disasters

Bash scripts have a reputation for being fragile. They don’t have to be. Here are the patterns that separate scripts that work from scripts that work reliably. Start Every Script Right 1 2 3 #!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' What each does: set -e - Exit on any command failure set -u - Error on undefined variables set -o pipefail - Pipelines fail if any command fails IFS=$'\n\t' - Safer word splitting (no space splitting) Error Handling Basic Trap 1 2 3 4 5 6 7 8 9 10 11 12 #!/usr/bin/env bash set -euo pipefail cleanup() { echo "Cleaning up..." rm -f "$TEMP_FILE" } trap cleanup EXIT TEMP_FILE=$(mktemp) # Script continues... # cleanup runs automatically on exit, error, or interrupt Detailed Error Reporting 1 2 3 4 5 6 7 8 9 10 11 12 #!/usr/bin/env bash set -euo pipefail error_handler() { local line=$1 local exit_code=$2 echo "Error on line $line: exit code $exit_code" >&2 exit "$exit_code" } trap 'error_handler $LINENO $?' ERR # Now errors report their line number Log and Exit 1 2 3 4 5 6 7 die() { echo "ERROR: $*" >&2 exit 1 } # Usage [[ -f "$CONFIG_FILE" ]] || die "Config file not found: $CONFIG_FILE" Argument Parsing Simple Positional 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #!/usr/bin/env bash set -euo pipefail usage() { echo "Usage: $0 <environment> <version>" echo " environment: staging|production" echo " version: semver (e.g., 1.2.3)" exit 1 } [[ $# -eq 2 ]] || usage ENVIRONMENT=$1 VERSION=$2 [[ "$ENVIRONMENT" =~ ^(staging|production)$ ]] || die "Invalid environment" [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || die "Invalid version format" Flags with getopts 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #!/usr/bin/env bash set -euo pipefail VERBOSE=false DRY_RUN=false OUTPUT="" usage() { cat <<EOF Usage: $0 [options] <file> Options: -v Verbose output -n Dry run (don't make changes) -o FILE Output file -h Show this help EOF exit 1 } while getopts "vno:h" opt; do case $opt in v) VERBOSE=true ;; n) DRY_RUN=true ;; o) OUTPUT=$OPTARG ;; h) usage ;; *) usage ;; esac done shift $((OPTIND - 1)) [[ $# -eq 1 ]] || usage FILE=$1 Long Options (Manual Parsing) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #!/usr/bin/env bash set -euo pipefail VERBOSE=false CONFIG="" while [[ $# -gt 0 ]]; do case $1 in -v|--verbose) VERBOSE=true shift ;; -c|--config) CONFIG=$2 shift 2 ;; -h|--help) usage ;; --) shift break ;; -*) die "Unknown option: $1" ;; *) break ;; esac done Variable Safety Default Values 1 2 3 4 5 6 7 8 # Default if unset NAME=${NAME:-"default"} # Default if unset or empty NAME=${NAME:="default"} # Error if unset : "${REQUIRED_VAR:?'REQUIRED_VAR must be set'}" Safe Variable Expansion 1 2 3 4 5 6 7 8 # Always quote variables rm "$FILE" # Good rm $FILE # Bad - breaks on spaces # Check before using if [[ -n "${VAR:-}" ]]; then echo "$VAR" fi Arrays 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 FILES=("file1.txt" "file with spaces.txt" "file3.txt") # Iterate safely for file in "${FILES[@]}"; do echo "Processing: $file" done # Pass to commands cp "${FILES[@]}" /destination/ # Length echo "Count: ${#FILES[@]}" # Append FILES+=("another.txt") File Operations Safe Temporary Files 1 2 3 4 5 TEMP_DIR=$(mktemp -d) trap 'rm -rf "$TEMP_DIR"' EXIT TEMP_FILE=$(mktemp) trap 'rm -f "$TEMP_FILE"' EXIT Check Before Acting 1 2 3 4 5 6 7 8 9 10 11 12 13 14 # File exists [[ -f "$FILE" ]] || die "File not found: $FILE" # Directory exists [[ -d "$DIR" ]] || die "Directory not found: $DIR" # File is readable [[ -r "$FILE" ]] || die "Cannot read: $FILE" # File is writable [[ -w "$FILE" ]] || die "Cannot write: $FILE" # File is executable [[ -x "$FILE" ]] || die "Cannot execute: $FILE" Safe File Writing 1 2 3 4 5 6 7 8 9 10 # Atomic write with temp file write_config() { local content=$1 local dest=$2 local temp temp=$(mktemp) echo "$content" > "$temp" mv "$temp" "$dest" # Atomic on same filesystem } Command Execution Check Command Exists 1 2 3 4 5 6 7 require_command() { command -v "$1" >/dev/null 2>&1 || die "Required command not found: $1" } require_command jq require_command aws require_command docker Capture Output and Exit Code 1 2 3 4 5 6 7 8 9 10 11 # Capture output output=$(some_command 2>&1) # Capture exit code without exiting (despite set -e) exit_code=0 output=$(some_command 2>&1) || exit_code=$? if [[ $exit_code -ne 0 ]]; then echo "Command failed with code $exit_code" echo "Output: $output" fi Retry Logic 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 retry() { local max_attempts=$1 local delay=$2 shift 2 local cmd=("$@") local attempt=1 while [[ $attempt -le $max_attempts ]]; do if "${cmd[@]}"; then return 0 fi echo "Attempt $attempt/$max_attempts failed. Retrying in ${delay}s..." sleep "$delay" ((attempt++)) done return 1 } # Usage retry 3 5 curl -f https://api.example.com/health Logging 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 LOG_FILE="/var/log/myscript.log" log() { local level=$1 shift echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $*" | tee -a "$LOG_FILE" } info() { log INFO "$@"; } warn() { log WARN "$@"; } error() { log ERROR "$@" >&2; } # Usage info "Starting deployment" warn "Deprecated config option used" error "Failed to connect to database" Confirmation Prompts 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 confirm() { local prompt=${1:-"Continue?"} local response read -r -p "$prompt [y/N] " response [[ "$response" =~ ^[Yy]$ ]] } # Usage if confirm "Delete all files in $DIR?"; then rm -rf "$DIR"/* fi # With default yes confirm_yes() { local prompt=${1:-"Continue?"} local response read -r -p "$prompt [Y/n] " response [[ ! "$response" =~ ^[Nn]$ ]] } Parallel Execution 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # Simple background jobs for server in server1 server2 server3; do deploy_to "$server" & done wait # Wait for all background jobs # With job limiting MAX_JOBS=4 job_count=0 for item in "${ITEMS[@]}"; do process_item "$item" & ((job_count++)) if [[ $job_count -ge $MAX_JOBS ]]; then wait -n # Wait for any one job to complete ((job_count--)) fi done wait # Wait for remaining jobs Complete Script Template 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 #!/usr/bin/env bash # # Description: What this script does # Usage: ./script.sh [options] <args> # set -euo pipefail IFS=$'\n\t' # Constants readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" readonly SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")" # Defaults VERBOSE=false DRY_RUN=false # Logging log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; } info() { log "INFO: $*"; } warn() { log "WARN: $*" >&2; } error() { log "ERROR: $*" >&2; } die() { error "$@"; exit 1; } # Cleanup cleanup() { # Add cleanup tasks here : } trap cleanup EXIT # Usage usage() { cat <<EOF Usage: $SCRIPT_NAME [options] <argument> Description: What this script does in more detail. Options: -v, --verbose Enable verbose output -n, --dry-run Show what would be done -h, --help Show this help message Examples: $SCRIPT_NAME -v input.txt $SCRIPT_NAME --dry-run config.yml EOF exit "${1:-0}" } # Parse arguments parse_args() { while [[ $# -gt 0 ]]; do case $1 in -v|--verbose) VERBOSE=true; shift ;; -n|--dry-run) DRY_RUN=true; shift ;; -h|--help) usage 0 ;; --) shift; break ;; -*) die "Unknown option: $1" ;; *) break ;; esac done [[ $# -ge 1 ]] || die "Missing required argument" ARGUMENT=$1 } # Main logic main() { parse_args "$@" info "Starting with argument: $ARGUMENT" if $VERBOSE; then info "Verbose mode enabled" fi if $DRY_RUN; then info "Dry run - no changes will be made" fi # Your script logic here info "Done" } main "$@" Bash scripts don’t need to be fragile. set -euo pipefail catches most accidents. Proper argument parsing makes scripts usable. Traps ensure cleanup happens. These patterns transform one-off hacks into reliable automation. Use the template, adapt as needed, and stop being afraid of your own scripts. ...

February 26, 2026 Â· 7 min Â· 1491 words Â· Rob Washington

AWS CLI Power User: Queries, Filters, and Automation

The AWS Console is fine for exploration. For real work—auditing, automation, bulk operations—the CLI is essential. Here’s how to use it effectively. Output Formats 1 2 3 4 5 6 7 8 9 10 11 # JSON (default, best for scripting) aws ec2 describe-instances --output json # Table (human readable) aws ec2 describe-instances --output table # Text (tab-separated, grep-friendly) aws ec2 describe-instances --output text # YAML aws ec2 describe-instances --output yaml Set default in ~/.aws/config: ...

February 26, 2026 Â· 6 min Â· 1170 words Â· Rob Washington

GitHub Actions Patterns for Real-World CI/CD

GitHub Actions tutorials show you on: push with a simple build. Real projects need caching, matrix builds, environment protection, secrets management, and reusable workflows. Here’s what actually works. Workflow Structure 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # .github/workflows/ci.yml name: CI on: push: branches: [main] pull_request: branches: [main] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run tests run: npm test Caching Dependencies Without caching, every run downloads the internet: ...

February 26, 2026 Â· 7 min Â· 1419 words Â· Rob Washington