Shell ScriptingReal-World ProjectAdvancedProvisioningDevOpsMay 2026

Shell Scripting Real-World Projects: System Provisioning Script

Provision a fresh Ubuntu server to a configured application host — package installation, user creation, directory setup, UFW firewall, Nginx configuration, SSL certificate, sudoers, and cron scheduling.

A provisioning script configures a fresh server from bare Ubuntu to a fully configured application host — installing packages, creating users, setting up firewall rules, deploying configs, and registering services. It's Infrastructure as Code without the framework overhead.

BASH
#!/usr/bin/env bash
# provision.sh — Provision a fresh Ubuntu server for myapp

set -euo pipefail

[[ "$(id -u)" -eq 0 ]] || { echo "Must run as root"; exit 1; }

LOG="/var/log/provision.log"
APP_USER="deploy"
APP_DIR="/opt/myapp"
DOMAIN="${1:?Usage: provision.sh DOMAIN}"
SLACK_URL="${SLACK_WEBHOOK_URL:-}"

log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "${LOG}"; }
step(){ log ""; log "▶ $*"; }

log "=== Provisioning: $(hostname) — $(date) ==="
log "Domain: ${DOMAIN}"

# ── 1. System update ──────────────────────────────────────
step "System update"
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get upgrade -y -qq
log "  System updated"

# ── 2. Required packages ──────────────────────────────────
step "Installing packages"
apt-get install -y -qq \
  nginx php8.3-fpm php8.3-mysql php8.3-redis php8.3-gd php8.3-mbstring \
  mysql-client redis-tools \
  git curl wget rsync htop iotop \
  certbot python3-certbot-nginx \
  fail2ban ufw \
  jq bc netcat-openbsd
log "  Packages installed"

# ── 3. Create application user ────────────────────────────
step "Creating application user: ${APP_USER}"
if ! id "${APP_USER}" &>/dev/null; then
  useradd -m -s /bin/bash -G www-data "${APP_USER}"
  install -d -m 700 "/home/${APP_USER}/.ssh"
  # Copy authorised keys from root
  cp /root/.ssh/authorized_keys "/home/${APP_USER}/.ssh/" 2>/dev/null || true
  chown -R "${APP_USER}:${APP_USER}" "/home/${APP_USER}/.ssh"
fi
log "  User ${APP_USER} ready"

# ── 4. Directory structure ────────────────────────────────
step "Setting up directories"
install -d -o "${APP_USER}" -g "${APP_USER}" -m 755 \
  "${APP_DIR}"/{releases,shared/{logs,uploads,config}} \
  /backups /var/log/myapp
log "  Directory structure ready"

# ── 5. Firewall ───────────────────────────────────────────
step "Configuring firewall"
ufw --force reset
ufw default deny incoming
ufw default allow outgoing
ufw allow ssh
ufw allow 'Nginx Full'
ufw --force enable
log "  Firewall: SSH + HTTP/HTTPS only"

# ── 6. Nginx configuration ────────────────────────────────
step "Configuring Nginx"
cat > "/etc/nginx/sites-available/${DOMAIN}" << NGINXCONF
server {
    listen 80;
    server_name ${DOMAIN};
    root ${APP_DIR}/current/public;
    index index.php;

    access_log /var/log/nginx/${DOMAIN}_access.log;
    error_log  /var/log/nginx/${DOMAIN}_error.log;

    location / { try_files \$uri \$uri/ /index.php?\$query_string; }

    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
    }
    location ~ /\.ht { deny all; }
}
NGINXCONF

ln -sfn "/etc/nginx/sites-available/${DOMAIN}" /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
log "  Nginx configured for ${DOMAIN}"

# ── 7. SSL certificate ────────────────────────────────────
step "Obtaining SSL certificate"
certbot --nginx -d "${DOMAIN}" --non-interactive --agree-tos \
  --email "ops@example.com" --redirect
log "  SSL certificate obtained"

# ── 8. Configure sudoers for deploy user ─────────────────
step "Configuring sudoers"
cat > /etc/sudoers.d/deploy << SUDOCONF
${APP_USER} ALL=(ALL) NOPASSWD: /bin/systemctl reload nginx
${APP_USER} ALL=(ALL) NOPASSWD: /bin/systemctl reload php8.3-fpm
${APP_USER} ALL=(ALL) NOPASSWD: /bin/systemctl restart myapp-worker
SUDOCONF
chmod 440 /etc/sudoers.d/deploy
log "  Sudoers configured"

# ── 9. Schedule jobs ──────────────────────────────────────
step "Installing cron jobs"
cat > /etc/cron.d/myapp << CRONTAB
# Backups: 2am daily
0 2 * * * ${APP_USER} /opt/scripts/backup_orchestrator.sh >> /var/log/backup.log 2>&1
# Cert renewal: 3am daily
0 3 * * * root certbot renew --quiet --deploy-hook 'nginx -s reload'
# Health monitor: every 5 minutes
*/5 * * * * ${APP_USER} /opt/scripts/monitor.sh 2>&1 | logger -t monitor
CRONTAB
log "  Cron jobs installed"

log ""
log "=== Provisioning complete: $(date) ==="
log "Next steps:"
log "  1. Copy .env to ${APP_DIR}/shared/.env"
log "  2. Run: sudo -u ${APP_USER} ./deploy.sh main"
[[ -n "${SLACK_URL}" ]] && curl -s -X POST "${SLACK_URL}" \
  -d "{\"text\":\"✅ Server provisioned: $(hostname) for ${DOMAIN}\"}" &>/dev/null
✔ Provisioning rules — Always run as root with set -euo pipefail so the first error aborts and doesn't leave the server in a half-provisioned state. Log every step to a file — you'll need it for debugging. Set the firewall before anything external-facing is installed. Use DEBIAN_FRONTEND=noninteractive to prevent apt prompts from blocking the script. Test provisioning by running on a fresh cloud instance before running on production.