
In the world of AI, speed and scale are everything. But as we push the boundaries of what’s possible, we’ve shifted from monolithic apps to distributed architectures. These setups, powered by technologies like Docker and Kubernetes, are the backbone of modern AI deployments, enabling the agility and resilience needed to train models, process massive data streams, and serve users globally.
Yet, this power comes with a price: complexity. When things go wrong, finding the root cause can feel like searching for a needle in a digital haystack. The logs you need might be scattered across dozens of containers, pods, and nodes, making traditional troubleshooting a nightmare.
This guide is for every DevOps engineer, SRE, and developer who’s ever stared at a blank terminal, wondering where to begin. Let’s get to the core of Docker and Kubernetes troubleshooting, so you can resolve issues faster and keep your AI workloads running smoothly.
🐳 Docker: The Building Blocks
Think of Docker as the standardized packaging for your applications. It takes your app, its dependencies, and its configuration and bundles them into a lightweight, portable container. This ensures your code runs consistently, whether it’s on your laptop or a cloud server.
Why Docker is a Game-Changer
- Consistent Environments: “It works on my machine” is no longer a valid excuse. A Docker container guarantees your app runs the same everywhere.
- Faster Pipelines: Docker streamlines Continuous Integration/Continuous Deployment (CI/CD) pipelines, making it easier to build, test, and deploy applications.
- Microservices Foundation: It’s the ideal runtime for building and scaling microservices-based systems.
☸️ Kubernetes: The Conductor
If Docker provides the instruments, Kubernetes is the conductor, managing the entire orchestra. Kubernetes orchestrates how your Docker containers are deployed, scaled, and networked across a cluster of machines. It’s the engine that ensures your applications are always available, healthy, and performing optimally.
Key Features of Kubernetes
- Automatic Scaling and Failover: Kubernetes can automatically scale your application up or down based on demand and can restart failed containers or pods.
- Self-Healing: If a container or node crashes, Kubernetes reschedules the workload to a healthy node, ensuring continuous operation.
- Service Discovery: It handles networking and communication between different parts of your application, so you don’t have to manually configure everything.
🔍 The Challenge of Distributed Troubleshooting
In a monolithic application, you typically have one set of logs in one place. In a distributed system, a single user request might span multiple containers, pods, and nodes. This creates a distributed puzzle where the pieces of information you need are all over the place.
Consider these common scenarios:
- A user reports an error. Which of the 50 containers handled that request?
- A pod keeps restarting. Is the container crashing? Is it a dependency issue on the node?
- A sudden scaling event adds new pods. Does this introduce new issues?
🛠️ Docker & Kubernetes Troubleshooting Cheatsheet
Here’s your go-to guide for finding the information you need, fast.
Docker Logs
| Activity | Command |
| List running containers: | |
| Pull logs from a container: | |
| Stream logs in real-time: | docker logs -f <container_id_or_name> |
| Limit log output: | logs --tail 100 <container_id_or_name> |
Kubernetes Logs
| Activity | Command |
| List pods in a namespace: | kubectl get pods -n <namespace> |
| Get logs from a pod: | kubectl logs <pod_name> -n <namespace> |
| Get logs from a specific container in a pod: | kubectl logs <pod_name> -c <container_name> -n <namespace> |
| Stream logs: | kubectl logs -f <pod_name> -n <namespace> |
| View logs from the previous container instance (if it crashed) | kubectl logs <pod_name> -n <namespace> --previous |
Advanced Log Retrieval
- Label-based logs:
kubectl logs -l app=myapp -n <namespace>This is incredibly useful for finding logs from all pods related to a specific application or service. - Tools to make life easier: Consider using tools like stern and kubetail to stream logs from multiple pods at once.
⚡ The Ultimate Solution: Automated Log Collection Script
For large or complex clusters, manually running commands for every container is simply not practical. An automated script can save you valuable time and stress. The following script pulls logs from all Docker containers and Kubernetes pods, saving them into a structured, timestamped archive.
Create a file named collect-logs.sh with the following content:
Bash
#!/usr/bin/env bash
# collect-logs.sh — Pull Docker + Kubernetes logs into timestamped files
# This script automates log collection for troubleshooting
# It is designed to be self-contained and easy to use.
# It can be modified to suit specific needs.
# --- Start Script content ---
#!/usr/bin/env bash
# collect-logs.sh — Pull Docker + Kubernetes logs into timestamped files
# Usage examples:
# ./collect-logs.sh
# ./collect-logs.sh -o ./mylogs --tail 500 --since 2h
# ./collect-logs.sh -n prod -l app=myapp --previous
# ./collect-logs.sh --kube-context my-ctx --gzip
set -u
set -o pipefail
# ---------- Defaults ----------
OUTPUT_ROOT="logs"
TAIL_LINES=""
SINCE_DURATION=""
NAMESPACE=""
LABEL_SELECTOR=""
KUBE_CONTEXT=""
PREVIOUS=false
GZIP_AT_END=false
# ---------- Helpers ----------
usage() {
cat <<EOF
Usage: $0 [options]
Options:
-o, --output DIR Output root directory (default: ${OUTPUT_ROOT})
--tail N Limit to last N lines (docker & kubectl)
--since DURATION Only logs newer than duration (e.g. 30m, 2h, 1d)
-n, --namespace NAME Kubernetes namespace (default: all namespaces)
-l, --label SELECTOR Kubernetes label selector (e.g. app=myapp)
--kube-context NAME Use a specific kubectl context
--previous Include logs from previous container instance (K8s)
--gzip Gzip the collected folder at the end
-h, --help Show this help
Examples:
$0
$0 -o ./out --tail 500 --since 2h
$0 -n prod -l app=myapp --previous
$0 --kube-context my-ctx --gzip
EOF
}
# ---------- Arg parsing ----------
while (( "$#" )); do
case "$1" in
-o|--output) OUTPUT_ROOT="${2:-}"; shift 2;;
--tail) TAIL_LINES="${2:-}"; shift 2;;
--since) SINCE_DURATION="${2:-}"; shift 2;;
-n|--namespace) NAMESPACE="${2:-}"; shift 2;;
-l|--label) LABEL_SELECTOR="${2:-}"; shift 2;;
--kube-context) KUBE_CONTEXT="${2:-}"; shift 2;;
--previous) PREVIOUS=true; shift 1;;
--gzip) GZIP_AT_END=true; shift 1;;
-h|--help) usage; exit 0;;
*) echo "Unknown option: $1" >&2; usage; exit 1;;
esac
done
TS_UTC="$(date -u +%Y%m%dT%H%M%SZ)"
OUT_DIR="${OUTPUT_ROOT}/collect_${TS_UTC}"
K8S_DIR="${OUT_DIR}/k8s"
DOCKER_DIR="${OUT_DIR}/docker"
META_FILE="${OUT_DIR}/INDEX.txt"
mkdir -p "${K8S_DIR}" "${DOCKER_DIR}"
echo "# Log collection started: ${TS_UTC} (UTC)" | tee -a "${META_FILE}"
echo "# Output path: ${OUT_DIR}" | tee -a "${META_FILE}"
# Build common option arrays
DOCKER_OPTS=()
KUBECTL_TAIL_OPTS=()
if [[ -n "${TAIL_LINES}" ]]; then
DOCKER_OPTS+=(--tail "${TAIL_LINES}")
KUBECTL_TAIL_OPTS+=(--tail="${TAIL_LINES}")
fi
if [[ -n "${SINCE_DURATION}" ]]; then
DOCKER_OPTS+=(--since "${SINCE_DURATION}")
KUBECTL_TAIL_OPTS+=(--since="${SINCE_DURATION}")
fi
# ---------- Collect Docker logs ----------
if command -v docker >/dev/null 2>&1; then
echo "# Docker detected — collecting logs..." | tee -a "${META_FILE}"
# Get all containers (running + exited), so you keep crash logs too
while IFS= read -r line; do
cid="$(awk '{print $1}' <<<"$line")"
cname="$(awk '{print $2}' <<<"$line")"
safe_name="${cname//\//_}"
logfile="${DOCKER_DIR}/${safe_name}_${cid}_${TS_UTC}.log"
(
echo "### docker logs ${DOCKER_OPTS[*]} ${cid}" > "${logfile}"
docker logs "${DOCKER_OPTS[@]}" "${cid}" >> "${logfile}" 2>&1 || true
)
echo "Docker: ${cname} (${cid}) -> ${logfile}" | tee -a "${META_FILE}"
done < <(docker ps -a --format '{{"{{"}}.ID{{"}}"}} {{"{{"}}.Names{{"}}"}}')
else
echo "# Docker not found — skipping Docker logs." | tee -a "${META_FILE}"
fi
# ---------- Collect Kubernetes logs ----------
if command -v kubectl >/dev/null 2>&1; then
echo "# kubectl detected — collecting K8s logs..." | tee -a "${META_FILE}"
# Context selection
if [[ -n "${KUBE_CONTEXT}" ]]; then
export KUBECONFIG="${KUBECONFIG:-}"
kubectl config use-context "${KUBE_CONTEXT}" >/dev/null 2>&1 || {
echo "WARN: Unable to switch to context '${KUBE_CONTEXT}', continuing with current context." | tee -a "${META_FILE}"
}
fi
# Namespace list
if [[ -n "${NAMESPACE}" ]]; then
NS_LIST=("${NAMESPACE}")
else
mapfile -t NS_LIST < <(kubectl get ns -o jsonpath='{.items[*].metadata.name}' 2>/dev/null)
# Fallback if we can’t list (e.g., limited permissions): use 'default'
[[ ${#NS_LIST[@]} -eq 0 ]] && NS_LIST=("default")
fi
for ns in "${NS_LIST[@]}"; do
echo "## Namespace: ${ns}" | tee -a "${META_FILE}"
# Pod list (respect label selector if provided)
if [[ -n "${LABEL_SELECTOR}" ]]; then
mapfile -t PODS < <(kubectl get pods -n "${ns}" -l "${LABEL_SELECTOR}" -o jsonpath='{.items[*].metadata.name}' 2>/dev/null)
else
mapfile -t PODS < <(kubectl get pods -n "${ns}" -o jsonpath='{.items[*].metadata.name}' 2>/dev/null)
fi
[[ ${#PODS[@]} -eq 0 ]] && { echo " (no pods)" | tee -a "${META_FILE}"; continue; }
for pod in "${PODS[@]}"; do
# Collect container names (app + init)
containers="$(kubectl get pod "${pod}" -n "${ns}" -o jsonpath='{.spec.containers[*].name} {.spec.initContainers[*].name}' 2>/dev/null || true)"
if [[ -z "${containers// /}" ]]; then
echo " Pod ${pod}: (no containers?)" | tee -a "${META_FILE}"
continue
fi
for c in ${containers}; do
base="${K8S_DIR}/${ns}__${pod}__${c}__${TS_UTC}"
log_current="${base}.log"
log_prev="${base}__previous.log"
# Current logs
(
echo "### kubectl logs -n ${ns} ${pod} -c ${c} ${KUBECTL_TAIL_OPTS[*]}" > "${log_current}"
kubectl logs -n "${ns}" "${pod}" -c "${c}" "${KUBECTL_TAIL_OPTS[@]}" >> "${log_current}" 2>&1 || true
)
echo "K8s: ${ns}/${pod}/${c} -> ${log_current}" | tee -a "${META_FILE}"
# Previous logs (if requested)
if [[ "${PREVIOUS}" == true ]]; then
(
echo "### kubectl logs -n ${ns} ${pod} -c ${c} --previous ${KUBECTL_TAIL_OPTS[*]}" > "${log_prev}"
kubectl logs -n "${ns}" "${pod}" -c "${c}" --previous "${KUBECTL_TAIL_OPTS[@]}" >> "${log_prev}" 2>&1 || true
)
echo "K8s (prev): ${ns}/${pod}/${c} -> ${log_prev}" | tee -a "${META_FILE}"
fi
done
done
done
else
echo "# kubectl not found — skipping Kubernetes logs." | tee -a "${META_FILE}"
fi
# ---------- Finish ----------
if [[ "${GZIP_AT_END}" == true ]]; then
tarball="${OUT_DIR}.tar.gz"
(cd "$(dirname "${OUT_DIR}")" && tar -czf "$(basename "${tarball}")" "$(basename "${OUT_DIR}")")
echo "# Archive created: ${tarball}" | tee -a "${META_FILE}"
fi
echo "# Done. Logs saved under: ${OUT_DIR}"
# --- End Script content ---
Make the script executable and run it:
Bash
chmod +x collect-logs.sh
./collect-logs.sh --tail 500 --since 2h --gzip
This will generate a structured directory of logs, making it easy to navigate and analyze the data later. The script is downloadable from GitHub.
Final Thoughts
Distributed architectures are the foundation of a new era of AI, but mastering them requires a new set of skills. By understanding the core troubleshooting tools and workflows for Docker and Kubernetes, you can turn a crisis into a manageable challenge. With a solid cheatsheet and an automated script in your arsenal, you’ll be able to identify and resolve issues faster, ensuring your AI workloads remain resilient, scalable, and ready for whatever comes next.