BLOG // [DEVOPS] Bash and Docker in DevOps: Managing Containers Efficiently
Bash and Docker in DevOps: Managing Containers Efficiently

Bash and Docker in DevOps: Managing Containers Efficiently

Comprehensive guide to combining Bash and Docker in DevOps: wrapper scripts, container management, image cleanup, healthchecks, deploying stacks with docker compose, and auto-restarting crashed containers.

DEVOPS BASH

Bash and Docker in DevOps

In the previous post, we explored automating SSH to manage multiple servers in parallel. Now, we step into the container world—where Bash serves as a powerful bridge between you and the Docker daemon, enabling container management more flexibly than any UI.

The Docker CLI is already powerful, but when you need to run batches of containers, check health status periodically, clean up old images, or deploy a complex stack—manually typing commands one by one is no longer practical. Bash lets you write wrapper scripts to automate the entire container lifecycle: from build and deploy, to healthcheck, restart on crash, and resource cleanup.

This article covers techniques for combining Bash and Docker, from basic commands to a complete container management script.


Core Docker Commands in Bash

Listing and Filtering Containers

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# List all running containers
docker ps

# List all containers including stopped ones
docker ps -a

# Output container IDs only (script-friendly)
docker ps -q

# Filter by name
docker ps --filter "name=nginx"

# Filter by status
docker ps --filter "status=running"
docker ps --filter "status=exited"

# Combine filters with custom format
docker ps --format "{{.ID}} {{.Names}} {{.Status}} {{.Ports}}"

Checking Container State

1
2
3
4
5
6
7
8
# Check if container is running (returns 0 if true)
docker inspect --format='{{.State.Running}}' myapp

# Get detailed status
docker inspect --format='{{.State.Status}}' myapp

# Check health status (if healthcheck is configured)
docker inspect --format='{{.State.Health.Status}}' myapp

Managing Containers

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Start a stopped container
docker start myapp

# Stop a container
docker stop myapp

# Restart a container
docker restart myapp

# Remove a stopped container
docker rm myapp

# Force remove (even if running)
docker rm -f myapp

Docker Compose

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Start a stack
docker compose up -d

# Stop a stack
docker compose down

# View logs
docker compose logs -f

# Restart a specific service
docker compose restart nginx

Bash Wrapper Scripts for Docker

Utility Functions

Before writing complex scripts, create reusable utility functions:

 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
#!/usr/bin/env bash

# Check if Docker daemon is running
docker_ready() {
    docker info >/dev/null 2>&1
}

# Get container ID by name (empty if not found)
container_id() {
    docker ps -q --filter "name=$1"
}

# Check if container is running
container_running() {
    [[ -n "$(container_id "$1")" ]]
}

# Check if container exists (even if stopped)
container_exists() {
    docker ps -a -q --filter "name=$1" | grep -q .
}

# Get container logs (last N lines)
container_logs() {
    local name="${1:?Container name required}"
    local lines="${2:-50}"
    docker logs --tail "$lines" "$name" 2>&1
}

Cleaning Up Docker Resources

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Remove stopped containers
cleanup_containers() {
    local count
    count=$(docker ps -a -q --filter "status=exited" | wc -l)
    if [[ "$count" -gt 0 ]]; then
        echo "Cleaning up $count stopped containers..."
        docker ps -a -q --filter "status=exited" | xargs docker rm -f
    fi
}

# Remove dangling images (images not attached to any container)
cleanup_images() {
    local count
    count=$(docker images -f "dangling=true" -q | wc -l)
    if [[ "$count" -gt 0 ]]; then
        echo "Cleaning up $count dangling images..."
        docker image prune -f
    fi
}

# Remove all unused resources (use with caution!)
cleanup_all() {
    docker system prune -f --volumes
}

Container Healthcheck with Bash

When containers lack built-in Docker healthchecks, you can write your own validation script in Bash:

 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
#!/usr/bin/env bash
# HTTP endpoint healthcheck for containers

check_http() {
    local container_name="${1:?Container name required}"
    local port="${2:-80}"
    local path="${3:-/}"
    local expected_status="${4:-200}"

    local ip
    ip=$(docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$container_name" 2>/dev/null)

    if [[ -z "$ip" ]]; then
        echo "[ERROR] Cannot get IP for container $container_name"
        return 1
    fi

    local status
    status=$(curl -s -o /dev/null -w "%{http_code}" "http://${ip}:${port}${path}" 2>/dev/null || echo "000")

    if [[ "$status" == "$expected_status" ]]; then
        echo "[OK] $container_name HTTP check passed (status: $status)"
        return 0
    else
        echo "[WARN] $container_name HTTP check failed (status: $status, expected: $expected_status)"
        return 1
    fi
}

# Check multiple containers
check_http "nginx" 80 "/" "200"
check_http "api" 8080 "/health" "200"

Deploying Stacks with Bash

Deploying a Docker Compose stack typically requires: pulling new images, stopping old services, restarting, and checking status. Bash helps you orchestrate the entire process:

 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
#!/usr/bin/env bash
# deploy-stack.sh — Deploy Docker Compose stack with healthcheck

set -euo pipefail

readonly COMPOSE_FILE="${1:-docker-compose.yml}"
readonly PROJECT_NAME="${2:-myapp}"
readonly HEALTH_TIMEOUT="${HEALTH_TIMEOUT:-60}"
readonly LOG_FILE="/var/log/docker-deploy.log"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}

# Pull latest images
pull_images() {
    log "Pulling latest images..."
    docker compose -f "$COMPOSE_FILE" -p "$PROJECT_NAME" pull
}

# Deploy the stack
deploy_stack() {
    log "Deploying stack..."
    docker compose -f "$COMPOSE_FILE" -p "$PROJECT_NAME" up -d --remove-orphans
}

# Wait for container to become healthy
wait_healthy() {
    local service="$1"
    local elapsed=0

    log "Waiting for $service to become healthy..."
    while [[ $elapsed -lt $HEALTH_TIMEOUT ]]; do
        local status
        status=$(docker compose -f "$COMPOSE_FILE" -p "$PROJECT_NAME" ps --format json "$service" 2>/dev/null | grep -o '"Health":"[^"]*"' | cut -d'"' -f4)

        if [[ "$status" == "healthy" ]]; then
            log "$service is healthy"
            return 0
        fi

        sleep 2
        elapsed=$((elapsed + 2))
    done

    log "[WARN] $service did not become healthy within ${HEALTH_TIMEOUT}s"
    return 1
}

# Rollback on deployment failure
rollback() {
    log "[ERROR] Deployment failed — rolling back..."
    docker compose -f "$COMPOSE_FILE" -p "$PROJECT_NAME" down
}

trap rollback ERR

main() {
    log "=== Deployment started ==="

    pull_images
    deploy_stack

    # Wait for all services to be healthy
    local services
    services=$(docker compose -f "$COMPOSE_FILE" -p "$PROJECT_NAME" ps --services)
    for svc in $services; do
        wait_healthy "$svc"
    done

    log "=== Deployment completed successfully ==="
}

main "$@"

Practical Example: Auto-Restart Watchdog Script

This complete script combines all the techniques above, designed to run via cron and keep containers running at all times:

  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
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#!/usr/bin/env bash
# ==============================================================================
# Script : docker-watchdog.sh
# Purpose: Monitor containers and auto-restart on crash
# Schedule via cron: */2 * * * * /opt/scripts/docker-watchdog.sh
# ==============================================================================

set -euo pipefail
IFS=$'\n\t'

readonly WATCHED_CONTAINERS="${WATCHED_CONTAINERS:-nginx,api,worker}"
readonly MAX_RESTART="${MAX_RESTART:-3}"
readonly RESTART_WINDOW="${RESTART_WINDOW:-300}"
readonly LOG_FILE="/var/log/docker-watchdog.log"
readonly STATE_DIR="/var/run/docker-watchdog"

mkdir -p "$STATE_DIR"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE" 2>/dev/null
}

# Count restarts within the time window
restart_count() {
    local container="$1"
    local state_file="${STATE_DIR}/${container}.restarts"
    local count=0

    if [[ -f "$state_file" ]]; then
        while IFS= read -r timestamp; do
            if [[ $(date +%s) -lt $((timestamp + RESTART_WINDOW)) ]]; then
                count=$((count + 1))
            fi
        done < "$state_file"

        # Clean old timestamps
        awk -v cutoff=$(($(date +%s) - RESTART_WINDOW)) '$1 >= cutoff' "$state_file" > "${state_file}.tmp"
        mv "${state_file}.tmp" "$state_file"
    fi

    echo "$count"
}

record_restart() {
    local container="$1"
    echo "$(date +%s)" >> "${STATE_DIR}/${container}.restarts"
}

# Check if container is running
is_running() {
    docker ps --format '{{.Names}}' | grep -q "^$1$"
}

# Check if container exists (even if stopped)
container_exists() {
    docker ps -a --format '{{.Names}}' | grep -q "^$1$"
}

restart_container() {
    local container="$1"
    local count
    count=$(restart_count "$container")

    if [[ "$count" -ge "$MAX_RESTART" ]]; then
        log "[CRIT] $container restarted $count times in ${RESTART_WINDOW}s — manual intervention required"
        return 1
    fi

    log "[WARN] $container is not running — restarting..."
    docker restart "$container" 2>/dev/null
    record_restart "$container"

    sleep 5
    if is_running "$container"; then
        log "[INFO] $container restarted successfully"
    else
        log "[ERROR] $container failed to restart"
        return 1
    fi
}

check_container() {
    local container="$1"

    if ! container_exists "$container"; then
        log "[WARN] $container does not exist — skipping"
        return 0
    fi

    if is_running "$container"; then
        # Check health status if available
        local health
        health=$(docker inspect --format='{{if .State.Health}}{{.State.Health.Status}}{{else}}no-healthcheck{{end}}' "$container" 2>/dev/null)

        if [[ "$health" == "unhealthy" ]]; then
            log "[WARN] $container is unhealthy"
            restart_container "$container"
        fi
    else
        restart_container "$container"
    fi
}

main() {
    IFS=',' read -ra CONTAINERS <<< "$WATCHED_CONTAINERS"

    for container in "${CONTAINERS[@]}"; do
        container=$(echo "$container" | xargs)
        check_container "$container"
    done
}

main "$@"

Cron Configuration

1
2
3
# Run every 2 minutes
crontab -e
*/2 * * * * /opt/scripts/docker-watchdog.sh >> /var/log/docker-watchdog-cron.log 2>&1

Sample Output

1
2
3
4
[2026-08-31 10:00:01] [INFO] nginx is healthy
[2026-08-31 10:00:01] [WARN] api is not running — restarting...
[2026-08-31 10:00:06] [INFO] api restarted successfully
[2026-08-31 10:00:06] [INFO] worker is healthy

Deployment Notes & Best Practices

  • Docker socket permission: Bash scripts need access to the Docker daemon via the /var/run/docker.sock socket. Ensure the user running the script belongs to the docker group or has sudo privileges.
  • BatchMode SSH: When running Docker scripts via SSH (from the previous post), use ssh -o BatchMode=yes to avoid host key confirmation prompts.
  • Container naming convention: Always use --name when starting containers for easier management. Avoid using generated IDs as they are hard to remember and unstable.
  • Resource cleanup: The watchdog script only restarts containers—it does not auto-delete. Combine with periodic docker system prune runs to free up disk space.
  • Logging aggregation: In production environments, container logs should be shipped to a centralized logging system (ELK, Loki) rather than only writing to local files.

Conclusion

Combining Bash and Docker creates a powerful, flexible, and lightweight container management tool that surpasses any complex orchestration framework. From listing containers, checking health status, cleaning up resources, to writing a watchdog script that auto-restarts on crash—all within your grasp with just Bash and the Docker CLI.

In the next post, we will explore monitoring with Bash: collecting system metrics, sending reports via email/webhook, and building daily report dashboards.

RESPONSES & DISCUSSION