Skip to main content
Bash intermediate Lesson 16 of 22

Bash Networking

Use curl, wget, nc, ssh, scp, rsync, and port checks in Bash scripts for automation and DevOps tasks.

curl — HTTP Requests

curl is the standard tool for making HTTP requests in Bash scripts. It is available on every Linux server and macOS, supports all common protocols, and has flags for every scenario you will encounter in automation — authentication, custom headers, timeouts, retries, and response code checking. Always use -s (silent) in scripts to suppress the progress bar.

# Basic GET request
curl https://api.example.com/users

# Silent mode — no progress bar (essential in scripts)
curl -s https://api.example.com/users

# Save response to a file
curl -so output.json https://api.example.com/data

# Follow redirects automatically
curl -sL https://example.com

# With an Authorization header
curl -sH "Authorization: Bearer $TOKEN" https://api.example.com/me

# POST with a JSON body
curl -s -X POST \
  -H "Content-Type: application/json" \
  -d '{"name":"Alice","role":"admin"}' \
  https://api.example.com/users

# POST with a JSON body built safely from variables (use jq — no manual escaping)
payload=$(jq -n --arg name "$USERNAME" --arg role "admin" \
  '{name: $name, role: $role}')
curl -s -X POST -H "Content-Type: application/json" \
  -d "$payload" https://api.example.com/users

# Get only the HTTP status code
status=$(curl -so /dev/null -w "%{http_code}" https://example.com)
echo "Status: $status"

# Fail and return non-zero exit code on HTTP 4xx/5xx errors
curl -sf https://api.example.com/health || echo "Health check failed"

# Set connection and total timeouts
curl -s --connect-timeout 5 --max-time 30 https://api.example.com/data

# Basic auth
curl -su "user:password" https://api.example.com/protected

Robust curl wrapper for scripts

A reusable wrapper that handles errors uniformly and extracts both the response body and the status code:

api_request() {
  local method="$1"
  local url="$2"
  local data="${3:-}"

  local response
  local status

  # -w appends the HTTP status code on a new line after the body
  response=$(curl -s -w "\n%{http_code}" \
    -X "$method" \
    -H "Authorization: Bearer $API_TOKEN" \
    -H "Content-Type: application/json" \
    --connect-timeout 5 \
    --max-time 30 \
    ${data:+-d "$data"} \
    "$url")

  status=$(tail -1 <<< "$response")
  body=$(head -n -1 <<< "$response")

  if [[ "$status" -ge 400 ]]; then
    echo "ERROR: HTTP $status from $url" >&2
    echo "$body" >&2
    return 1
  fi

  echo "$body"
}

users=$(api_request GET "https://api.example.com/users")
result=$(api_request POST "https://api.example.com/users" '{"name":"Bob"}')

wget — File Downloads

wget excels at downloading files, especially when you need retry logic or recursive site mirroring. For API calls and pipeline use, prefer curl; for downloading release archives and following mirrors, wget is often simpler.

# Download a file to the current directory
wget https://example.com/file.tar.gz

# Download quietly (no progress output)
wget -q https://example.com/file.tar.gz

# Download to a specific filename
wget -qO /tmp/app.tar.gz https://example.com/app-latest.tar.gz

# Download with automatic retry on connection failure
wget -q --retry-connrefused --tries=5 https://example.com/file.tar.gz

# Check if a URL is reachable without downloading
wget -q --spider https://example.com && echo "Reachable"

nc (netcat) — Port Checks and Raw TCP

nc (netcat) is the Swiss army knife of TCP/UDP networking. In scripts, its most common use is checking whether a port is open — useful for waiting until a service is ready before proceeding with a deploy or test.

# Check if a TCP port is open (3-second timeout)
nc -zw3 localhost 5432 && echo "PostgreSQL is reachable"

# Check multiple ports on the same host
for port in 80 443 8080; do
  nc -zw2 example.com "$port" \
    && echo "Port $port: OPEN" \
    || echo "Port $port: CLOSED"
done

# Send a one-shot message to a TCP listener
echo "Hello" | nc -w1 192.168.1.10 9000

# Grab a service banner (useful for verifying SSH or SMTP)
nc -w3 example.com 22    # shows the SSH version banner

Port check without nc (using /dev/tcp)

Bash’s /dev/tcp pseudo-device lets you check TCP connectivity even when nc is not installed:

check_port() {
  local host="$1"
  local port="$2"

  # Attempt to open a TCP connection — success means the port is open
  if (echo > /dev/tcp/"$host"/"$port") 2>/dev/null; then
    return 0   # port is open
  else
    return 1   # port is closed or unreachable
  fi
}

# Wait until a service is ready — useful in deploy scripts and Docker entrypoints
wait_for_port() {
  local host="$1"
  local port="$2"
  local max_wait="${3:-60}"
  local waited=0

  echo "Waiting for $host:$port..."
  until check_port "$host" "$port"; do
    if (( waited >= max_wait )); then
      echo "Timeout waiting for $host:$port" >&2
      return 1
    fi
    sleep 2
    (( waited += 2 ))
  done
  echo "$host:$port is ready (waited ${waited}s)"
}

wait_for_port localhost 5432 60   # wait up to 60s for PostgreSQL

SSH — Remote Command Execution

SSH is the standard way to execute commands on remote servers from scripts. Using SSH keys instead of passwords is essential for automation — password-based SSH requires interactive input that scripts cannot provide.

# Run a single command on a remote server
ssh user@server "uptime"

# Run with a specific SSH key
ssh -i ~/.ssh/deploy_key user@server "systemctl restart myapp"

# Run multiple commands — use a quoted heredoc to prevent local variable expansion
ssh user@server <<'EOF'
  cd /var/www/myapp
  git pull origin main
  npm install --production
  pm2 restart myapp
EOF

# Skip host key checking in CI environments — use carefully
ssh -o StrictHostKeyChecking=no user@server "echo connected"

# SSH port forwarding — tunnel local port 8080 to remote port 80
ssh -L 8080:localhost:80 user@server -N -f

# Test SSH connectivity in a script
if ssh -o BatchMode=yes -o ConnectTimeout=5 user@server "exit" 2>/dev/null; then
  echo "SSH to server is working"
else
  echo "SSH to server failed" >&2
fi

Deploy script using SSH

deploy_to_server() {
  local server="$1"
  local app_dir="/var/www/myapp"

  echo "Deploying to $server..."
  # Note: variables from the local shell expand here; use \$ for remote variables
  ssh -o StrictHostKeyChecking=no "deploy@${server}" bash <<EOF
    set -euo pipefail
    cd $app_dir
    git fetch origin main
    git reset --hard origin/main
    npm ci --production
    systemctl restart myapp
    echo "Deploy complete on \$(hostname)"
EOF
}

scp — Secure File Copy

scp copies files between local and remote hosts over SSH. For simple one-off transfers it is convenient; for large or frequent transfers, prefer rsync which can resume interrupted transfers and skip unchanged files.

# Copy a local file to a remote host
scp app.tar.gz user@server:/tmp/

# Copy a remote file to the local machine
scp user@server:/var/log/app.log ./

# Copy a directory recursively
scp -r ./dist/ user@server:/var/www/html/

# Specify a non-standard SSH port
scp -P 2222 file.txt user@server:/tmp/

rsync — Efficient File Sync

rsync is the right tool for syncing directories, especially over slow or unreliable connections. It only transfers files that have changed (based on size and modification time), compresses data in transit, and can resume interrupted transfers. It is the standard tool for deployment and backup.

# Sync a local directory to a remote host (archive mode: preserve perms, times, symlinks)
rsync -avz ./dist/ user@server:/var/www/html/

# Dry run — see exactly what would change without making any changes
rsync -avzn ./dist/ user@server:/var/www/html/

# Delete files on the destination that no longer exist locally
rsync -avz --delete ./dist/ user@server:/var/www/html/

# Exclude files and directories
rsync -avz --exclude='*.log' --exclude='.git' ./ user@server:/app/

# Backup with a per-day archive of changed files
rsync -avz --backup --backup-dir="/backup/$(date +%Y%m%d)" \
  /var/data/ user@backup-server:/backups/current/

# Sync using a specific SSH key
rsync -avz -e "ssh -i ~/.ssh/deploy_key" ./dist/ user@server:/var/www/

DNS Lookups

DNS lookups are useful for verifying infrastructure, debugging connectivity issues, and checking that DNS records are propagating correctly after a change.

# Forward lookup — IP address for a hostname
dig +short example.com
host example.com

# Reverse lookup — hostname for an IP address
dig +short -x 93.184.216.34

# Check specific record types
dig +short example.com MX    # mail exchange records
dig +short example.com TXT   # TXT records (SPF, DMARC, verification)
dig +short example.com NS    # authoritative nameservers

# Quick lookup using a specific DNS server
nslookup example.com 8.8.8.8   # query Google's DNS directly

What’s Next

The next tutorial covers system information commands — uname, df, du, ps, lsof, netstat, and systemctl.

Frequently Asked Questions

When should I use curl vs wget?
curl is more scriptable — it supports more protocols, has better options for headers and auth, and works great in pipelines. wget is better for recursive downloads and mirroring sites. Use curl in scripts.
How do I check if a port is open in Bash without nmap?
Use nc -zw3 host port or bash's /dev/tcp pseudo-device: (echo > /dev/tcp/host/port) 2>/dev/null. The /dev/tcp trick works even when nc is not installed.
How do I run a command on a remote server over SSH in a script?
Use ssh user@host 'command'. For multiple commands, use a heredoc: ssh user@host << 'EOF' ... EOF. Use SSH keys (not passwords) for automation.