CLI Philosophy & Structure
Docker's CLI has evolved from a flat list of commands (docker ps, docker rm) into a structured hierarchy of management commands. Understanding this structure is the first step to CLI mastery.
Management Commands vs Legacy Shorthand
Docker 1.13 introduced management commands that group operations logically. The legacy shorthand still works, but the management command form is more explicit and discoverable:
| Management Command | Legacy Shorthand | Purpose |
|---|---|---|
docker container ls | docker ps | List running containers |
docker container rm | docker rm | Remove containers |
docker image ls | docker images | List images |
docker image rm | docker rmi | Remove images |
docker network ls | (same) | List networks |
docker volume ls | (same) | List volumes |
docker container inspect | docker inspect | Inspect container details |
docker container logs | docker logs | View container logs |
docker container ls) in scripts and documentation for clarity. Use shorthand (docker ps) for interactive terminal sessions where speed matters. Both produce identical results.
The Help System
Docker's built-in help is your best reference. Every command supports --help, and the management command structure makes discovery natural:
# Discover all management commands
docker --help
# Management Commands:
# builder Manage builds
# config Manage Swarm configs
# container Manage containers
# image Manage images
# network Manage networks
# system Manage Docker
# volume Manage volumes
# Discover all container sub-commands
docker container --help
# Commands:
# attach Attach to a running container
# commit Create new image from container changes
# cp Copy files between container and host
# create Create a new container
# diff Inspect filesystem changes
# exec Execute a command in running container
# inspect Display detailed container info
# kill Kill a running container
# logs Fetch container logs
# ls List containers
# pause Pause all processes in container
# port List port mappings
# prune Remove all stopped containers
# rename Rename a container
# restart Restart a container
# rm Remove containers
# run Create and run a new container
# start Start stopped containers
# stats Display resource usage statistics
# stop Stop running containers
# top Display running processes
# unpause Unpause a paused container
# update Update container configuration
# wait Block until container stops
# Get detailed help for a specific command
docker container run --help
# Shows all flags: -d, -it, --name, -p, -v, -e, --network, etc.
Container Lifecycle Commands
A Docker container transitions through well-defined states. Understanding these transitions — and the commands that trigger them — is fundamental to container management.
stateDiagram-v2
[*] --> Created: docker create
Created --> Running: docker start
[*] --> Running: docker run
Running --> Paused: docker pause
Paused --> Running: docker unpause
Running --> Stopped: docker stop (SIGTERM → SIGKILL)
Running --> Stopped: docker kill (SIGKILL)
Stopped --> Running: docker start / docker restart
Stopped --> Removed: docker rm
Running --> Stopped: container exits (process ends)
Removed --> [*]
style Created fill:#16476A,color:#ffffff
style Running fill:#3B9797,color:#ffffff
style Paused fill:#BF092F,color:#ffffff
style Stopped fill:#132440,color:#ffffff
style Removed fill:#666666,color:#ffffff
Lifecycle Commands in Detail
# CREATE: Prepare a container without starting it
# Useful when you want to configure before running
docker create --name my-nginx -p 8080:80 nginx:latest
# Returns container ID: a1b2c3d4e5f6...
# Container is in "Created" state
# START: Begin execution of a created/stopped container
docker start my-nginx
# Container moves to "Running" state
# RUN: Create + Start in one command (most common)
docker run -d --name web -p 8080:80 nginx:latest
# Equivalent to: docker create + docker start
# STOP: Graceful shutdown (SIGTERM, then SIGKILL after timeout)
docker stop my-nginx
# Sends SIGTERM to PID 1, waits 10s (default), then SIGKILL
# Change timeout: docker stop -t 30 my-nginx
# KILL: Immediate termination (SIGKILL by default)
docker kill my-nginx
# Instantly terminates — no graceful shutdown
# Send specific signal: docker kill -s SIGUSR1 my-nginx
# RESTART: Stop + Start
docker restart my-nginx
# Equivalent to: docker stop + docker start
# PAUSE: Freeze all processes (uses cgroup freezer)
docker pause my-nginx
# All processes suspended — no CPU consumed, memory retained
# UNPAUSE: Resume frozen processes
docker unpause my-nginx
# RM: Remove a stopped container
docker rm my-nginx
# Force remove a running container:
docker rm -f my-nginx # Sends SIGKILL first, then removes
# Remove all stopped containers at once
docker container prune -f
# WARNING! This will remove all stopped containers.
docker stop vs docker kill — When to Use Each
docker stop sends SIGTERM first, allowing the application to flush buffers, close connections, and save state. After the grace period (default 10 seconds), it escalates to SIGKILL. docker kill sends SIGKILL immediately — no cleanup happens.
- Use
docker stopfor production containers: databases, web servers, message queues — anything that needs graceful shutdown - Use
docker killfor stuck containers that don't respond to SIGTERM, or testing crash scenarios - Increase timeout for slow-shutdown apps:
docker stop -t 60 postgres-db
Many Docker images fail to handle signals correctly because they use shell form CMD which runs under /bin/sh (PID 1) — the actual app doesn't receive the signal. We'll address this in Part 7 (Dockerfile Fundamentals).
Running Containers: docker run Deep Dive
docker run is the most complex and most-used Docker command. It combines image pull, container creation, and container start — with dozens of flags controlling every aspect of execution.
| Flag | Purpose | Example |
|---|---|---|
-d | Detached mode (run in background) | docker run -d nginx |
-it | Interactive + TTY (for shell access) | docker run -it ubuntu bash |
--name | Assign a name to the container | docker run --name web nginx |
-p | Port mapping (host:container) | docker run -p 8080:80 nginx |
-v | Volume mount (host:container or named) | docker run -v data:/app/data nginx |
-e | Set environment variable | docker run -e NODE_ENV=production node |
--network | Connect to a specific network | docker run --network mynet nginx |
--rm | Auto-remove when container stops | docker run --rm alpine echo hi |
--restart | Restart policy | docker run --restart unless-stopped nginx |
-m | Memory limit | docker run -m 512m nginx |
--cpus | CPU limit | docker run --cpus 1.5 nginx |
--env-file | Read environment from file | docker run --env-file .env app |
-w | Set working directory | docker run -w /app node npm start |
-u | Run as specific user | docker run -u 1000:1000 app |
--read-only | Read-only root filesystem | docker run --read-only nginx |
Practical docker run Examples
# Development web server with live code reload
docker run -d --name dev-server \
-p 3000:3000 \
-v $(pwd)/src:/app/src \
-e NODE_ENV=development \
--rm \
node:20-alpine sh -c "cd /app && npm run dev"
# Production database with resource limits and persistence
docker run -d --name postgres-prod \
-p 5432:5432 \
-v pgdata:/var/lib/postgresql/data \
-e POSTGRES_USER=admin \
-e POSTGRES_PASSWORD=secretpassword \
-e POSTGRES_DB=myapp \
-m 2g --cpus 2.0 \
--restart unless-stopped \
postgres:16-alpine
# One-off utility container (auto-removes when done)
docker run --rm -v $(pwd):/workspace -w /workspace \
python:3.12-slim python -c "
import json
with open('config.json') as f:
data = json.load(f)
print(f'Found {len(data)} entries')
"
# Redis with custom configuration
docker run -d --name cache \
-p 6379:6379 \
-v redis-data:/data \
--restart always \
redis:7-alpine redis-server --appendonly yes --maxmemory 256mb
# Security-hardened container
docker run -d --name secure-app \
--read-only \
--tmpfs /tmp:size=100m \
--tmpfs /run \
-u 1000:1000 \
--cap-drop ALL \
--cap-add NET_BIND_SERVICE \
--security-opt no-new-privileges:true \
myapp:latest
--privileged in production. This flag disables all security restrictions and gives the container full access to the host — effectively equivalent to root access on the host machine. If you need specific capabilities, use --cap-add to grant only what's required.
Executing in Running Containers
docker exec is your primary debugging tool. It runs a new process inside an already-running container, inheriting the container's namespaces (but running as a separate PID).
# Open an interactive shell in a running container
docker exec -it my-nginx /bin/bash
# If bash isn't available (Alpine images):
docker exec -it my-nginx /bin/sh
# Run a single command and return output
docker exec my-nginx cat /etc/nginx/nginx.conf
# Prints the nginx configuration file
# Run as a specific user
docker exec -u root my-nginx apt-get update
# Executes as root even if container runs as non-root
# Set environment variables for the exec session
docker exec -e DEBUG=true my-app node /scripts/health-check.js
# Execute in a specific working directory
docker exec -w /var/log my-nginx ls -la
# Lists files in /var/log inside the container
# Debugging network connectivity
docker exec my-app ping -c 3 database
# PING database (172.25.0.3): 56 data bytes
# 64 bytes from 172.25.0.3: seq=0 ttl=64 time=0.089 ms
docker exec my-app nslookup database
# Server: 127.0.0.11
# Name: database
# Address 1: 172.25.0.3
# Check what processes are running
docker exec my-app ps aux
# PID USER COMMAND
# 1 node node server.js
# 45 node /bin/sh (your exec session)
# Copy a configuration file for debugging
docker exec my-app cat /app/config/production.yml > local-debug.yml
docker exec starts a new process in the container. docker attach connects your terminal to the container's PID 1 stdout/stdin. Use exec for debugging (it won't kill the container if you Ctrl+C). Use attach only when you need to interact with the main process directly.
Image Management
Images are the building blocks of containers. The Docker CLI provides complete control over the image lifecycle — from pulling and building to tagging, pushing, and cleanup.
# Pull an image from a registry
docker pull nginx:latest
# latest: Pulling from library/nginx
# a2abf6c4d29d: Pull complete ← individual layer downloads
# a9edb18cadd3: Pull complete
# Digest: sha256:abc123...
# Status: Downloaded newer image for nginx:latest
# Pull a specific platform variant
docker pull --platform linux/arm64 python:3.12-slim
# List all local images
docker images
# REPOSITORY TAG IMAGE ID CREATED SIZE
# nginx latest 605c77e624dd 3 days ago 187MB
# python 3.12-slim d4a34e5a8c23 1 week ago 130MB
# postgres 16-alpine f9b577fb1ed6 2 weeks ago 243MB
# Filter images
docker images --filter "dangling=true" # Untagged images ()
docker images --filter "reference=nginx" # Only nginx images
docker images --format "{{.Repository}}:{{.Tag}} ({{.Size}})"
# nginx:latest (187MB)
# python:3.12-slim (130MB)
# Tag an image (create a new reference to the same image)
docker tag nginx:latest myregistry.io/team/nginx:v2.1.0
docker tag nginx:latest myregistry.io/team/nginx:latest
# Push to a registry
docker push myregistry.io/team/nginx:v2.1.0
docker push myregistry.io/team/nginx:latest
# Remove images
docker rmi nginx:latest # Remove by name:tag
docker rmi 605c77e624dd # Remove by ID
docker rmi $(docker images -q -f "dangling=true") # Remove all dangling
# Image history (show layers)
docker history nginx:latest
# IMAGE CREATED CREATED BY SIZE
# 605c77e624dd 3 days ago CMD ["nginx" "-g" "daemon off;"] 0B
# 3 days ago EXPOSE map[80/tcp:{}] 0B
# 3 days ago STOPSIGNAL SIGQUIT 0B
# 3 days ago RUN /bin/sh -c set -x && apt-get update... 58MB
# Build an image from a Dockerfile
docker build -t myapp:latest .
docker build -t myapp:v1.2.3 -f Dockerfile.production .
# Prune unused images
docker image prune -f # Remove dangling images only
docker image prune -a -f # Remove ALL unused images (not used by any container)
Inspection & Debugging
When containers misbehave, the Docker CLI provides powerful inspection tools. Mastering docker logs and docker inspect (especially with Go template formatting) separates beginners from experts.
docker logs — Container Output
# View all logs from a container
docker logs my-nginx
# Follow logs in real-time (like tail -f)
docker logs -f my-nginx
# 172.17.0.1 - - [14/May/2026:10:15:30 +0000] "GET / HTTP/1.1" 200 615
# (streams new log entries as they occur)
# Ctrl+C to stop following
# Show only the last N lines
docker logs --tail 50 my-nginx
# Show logs since a specific time
docker logs --since 2026-05-14T10:00:00 my-nginx
docker logs --since 30m my-nginx # Last 30 minutes
docker logs --since 2h my-nginx # Last 2 hours
# Show timestamps with each log line
docker logs -t my-nginx
# 2026-05-14T10:15:30.123456789Z 172.17.0.1 - "GET / HTTP/1.1" 200
# Combine flags for debugging sessions
docker logs -f --tail 100 --since 5m my-nginx
# View logs from a stopped container (useful for crash analysis)
docker logs crashed-container 2>&1 | tail -50
docker inspect — Deep Container Metadata
docker inspect returns the complete JSON metadata for any Docker object. The real power comes from Go template formatting to extract exactly the fields you need:
# Full JSON output (verbose)
docker inspect my-nginx | jq .
# Get container IP address
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' my-nginx
# 172.17.0.2
# Get container state (running, paused, stopped)
docker inspect -f '{{.State.Status}}' my-nginx
# running
# Get restart count and last start time
docker inspect -f 'Restarts: {{.RestartCount}}, Started: {{.State.StartedAt}}' my-nginx
# Restarts: 0, Started: 2026-05-14T10:00:00.123456Z
# Get port bindings
docker inspect -f '{{json .NetworkSettings.Ports}}' my-nginx | jq .
# { "80/tcp": [{"HostIp": "0.0.0.0", "HostPort": "8080"}] }
# Get mounted volumes
docker inspect -f '{{range .Mounts}}{{.Type}}: {{.Source}} → {{.Destination}}{{"\n"}}{{end}}' my-app
# volume: /var/lib/docker/volumes/data/_data → /app/data
# bind: /home/user/config → /app/config
# Get environment variables
docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' my-app
# NODE_ENV=production
# DATABASE_URL=postgres://...
# PATH=/usr/local/bin:/usr/bin
# Get the full command that's running
docker inspect -f '{{.Config.Cmd}}' my-nginx
# [nginx -g daemon off;]
# Get image used by container
docker inspect -f '{{.Config.Image}}' my-nginx
# nginx:latest
# Get container health status (if HEALTHCHECK defined)
docker inspect -f '{{.State.Health.Status}}' my-app
# healthy
# Compare configs of two containers
diff <(docker inspect container-a) <(docker inspect container-b)
docker diff & docker top — Lesser-Known Gems
Two often-overlooked commands provide critical debugging information:
docker diff <container>— Shows all filesystem changes since the container started (added/modified/deleted files). Invaluable for understanding what a container modifies at runtime.docker top <container>— Shows running processes inside the container as seen from the host (with host PIDs). Unlikedocker exec ps, this doesn't requirepsto be installed in the container.
# See what files a container has changed
docker diff my-nginx
# C /var (Changed)
# C /var/cache (Changed)
# A /var/cache/nginx/client_temp (Added)
# C /run (Changed)
# A /run/nginx.pid (Added)
# See processes from the host's perspective
docker top my-nginx
# UID PID PPID CMD
# root 4567 4545 nginx: master process
# www 4589 4567 nginx: worker process
# www 4590 4567 nginx: worker process
Resource Monitoring
Understanding resource consumption is essential for capacity planning and debugging performance issues. Docker provides real-time statistics and system-wide disk usage reporting.
# Real-time resource usage (like top for containers)
docker stats
# CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O
# a1b2c3d4e5f6 web 0.15% 45.2MiB / 512MiB 8.83% 1.2kB / 648B 0B / 0B
# f6e5d4c3b2a1 postgres 1.20% 128MiB / 2GiB 6.25% 2.1kB / 1.8kB 12MB / 45MB
# 9f8e7d6c5b4a redis 0.05% 12MiB / 256MiB 4.69% 890B / 450B 0B / 2.1MB
# Stats for specific containers only
docker stats web postgres redis
# Single snapshot (non-streaming) — useful in scripts
docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}"
# NAME CPU % MEM USAGE / LIMIT
# web 0.15% 45.2MiB / 512MiB
# postgres 1.20% 128MiB / 2GiB
# System-wide disk usage
docker system df
# TYPE TOTAL ACTIVE SIZE RECLAIMABLE
# Images 15 5 4.2GB 2.8GB (66%)
# Containers 8 3 120MB 95MB (79%)
# Local Volumes 12 4 1.5GB 800MB (53%)
# Build Cache 23 0 890MB 890MB (100%)
# Detailed breakdown
docker system df -v
# Shows individual images, containers, and volumes with sizes
-m flag). If no limit is set, it shows percentage of total host memory. CPU % can exceed 100% on multi-core systems (200% = 2 full cores). NET I/O and BLOCK I/O are cumulative since container start.
Container Export/Import vs Image Save/Load
Docker provides two pairs of commands for moving containers and images between systems. They serve different purposes and produce different results:
| Command Pair | Input | Output | Preserves Layers | Use Case |
|---|---|---|---|---|
docker save / docker load |
Image (by name:tag) | tar archive with all layers + metadata | Yes (all layers, tags, history) | Transfer images to air-gapped systems |
docker export / docker import |
Container (running or stopped) | tar archive of flattened filesystem | No (single flat layer, no metadata) | Snapshot a container's current filesystem state |
# SAVE/LOAD: Transfer complete images with all layers
# Save one or more images to a tar file
docker save -o images-backup.tar nginx:latest postgres:16 redis:7
# File contains all layers, manifests, and tags
# Load images on the destination machine
docker load -i images-backup.tar
# Loaded image: nginx:latest
# Loaded image: postgres:16
# Loaded image: redis:7
# Pipe through SSH for direct transfer
docker save myapp:latest | ssh user@remote 'docker load'
# Compress for smaller transfer
docker save myapp:latest | gzip > myapp-latest.tar.gz
gunzip -c myapp-latest.tar.gz | docker load
# EXPORT/IMPORT: Snapshot a container's filesystem
# Export a container's current state to a flat tar
docker export my-container > container-snapshot.tar
# Import as a new image (loses all metadata, history, CMD, ENV, etc.)
docker import container-snapshot.tar myimage:snapshot
# Creates a single-layer image from the flat filesystem
# Import with custom CMD
cat container-snapshot.tar | docker import --change "CMD [\"nginx\", \"-g\", \"daemon off;\"]" - myimage:restored
save/load for 99% of use cases — it preserves the full image with layers, history, and metadata. Use export/import only when you specifically need a flattened filesystem snapshot (e.g., forensic analysis or creating a minimal base image from a running container).
System Maintenance
Docker accumulates unused resources over time — stopped containers, dangling images, orphaned volumes, and build cache. Regular maintenance prevents disk exhaustion.
# Nuclear option: remove ALL unused resources
docker system prune -f
# Removes: stopped containers, unused networks, dangling images, build cache
# WARNING! This will remove:
# - all stopped containers
# - all networks not used by at least one container
# - all dangling images
# - all dangling build cache
# Include unused images (not just dangling)
docker system prune -a -f
# Also removes images not referenced by any container
# This can free gigabytes of space
# Include volumes (DANGEROUS — data loss possible!)
docker system prune -a --volumes -f
# Also removes all unused volumes (named and anonymous)
# Targeted cleanup — more surgical approach
docker container prune -f # Remove all stopped containers
docker image prune -f # Remove dangling images only
docker image prune -a -f # Remove ALL unused images
docker volume prune -f # Remove unused volumes
docker network prune -f # Remove unused networks
docker builder prune -f # Clear build cache
# Remove containers older than 24 hours
docker container prune -f --filter "until=24h"
# Remove images older than 7 days
docker image prune -a -f --filter "until=168h"
# Check what would be removed before pruning
docker system df # See overall usage
docker images -f "dangling=true" # List dangling images
docker ps -a -f "status=exited" # List stopped containers
docker volume ls -f "dangling=true" # List orphaned volumes
CLI Tips & Productivity
Power users leverage aliases, format strings, and Docker's built-in filtering to work faster and more precisely.
# Useful shell aliases (add to ~/.bashrc or ~/.zshrc)
alias dps='docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"'
alias dpsa='docker ps -a --format "table {{.Names}}\t{{.Status}}\t{{.Image}}"'
alias dimg='docker images --format "table {{.Repository}}:{{.Tag}}\t{{.Size}}\t{{.CreatedSince}}"'
alias dlogs='docker logs -f --tail 100'
alias dexec='docker exec -it'
alias dprune='docker system prune -af && docker volume prune -f'
alias dstop='docker stop $(docker ps -q)' # Stop all running containers
# Filtering with --filter
docker ps -f "status=running"
docker ps -f "name=web"
docker ps -f "ancestor=nginx" # Containers from nginx image
docker ps -f "label=environment=prod"
docker images -f "before=nginx:1.24" # Images created before this one
# Custom format strings for scripting
docker ps -q # Only container IDs
docker ps --format '{{.ID}}' # Same as -q
docker ps --format '{{.Names}}: {{.Status}}'
docker inspect -f '{{.State.Pid}}' my-container # Get PID
# Batch operations using subshells
docker stop $(docker ps -q) # Stop ALL running containers
docker rm $(docker ps -aq) # Remove ALL containers
docker rmi $(docker images -q -f dangling=true) # Remove dangling images
# Docker context — manage multiple Docker hosts
docker context create staging --docker "host=ssh://user@staging.example.com"
docker context use staging
docker ps # Now shows containers on staging server
docker context use default # Switch back to local
# .docker/config.json — persistent settings
cat ~/.docker/config.json
# {
# "psFormat": "table {{.Names}}\t{{.Status}}\t{{.Ports}}",
# "imagesFormat": "table {{.Repository}}\t{{.Tag}}\t{{.Size}}",
# "detachKeys": "ctrl-q,ctrl-q"
# }
Docker cp — Transfer Files Without Volumes
docker cp copies files between host and container at any time — the container can be running or stopped. It's invaluable for quick debugging without pre-configuring volumes:
# Copy from container to host
docker cp my-app:/app/logs/error.log ./debug/
docker cp my-app:/etc/nginx/nginx.conf ./nginx-backup.conf
# Copy from host to container
docker cp ./hotfix.js my-app:/app/src/hotfix.js
docker cp ./new-config.yml my-app:/app/config/
# Copy an entire directory
docker cp my-app:/app/data/ ./backup-data/
# Works with stopped containers too!
docker cp crashed-container:/var/log/app.log ./crash-analysis/
Exercises
- Lifecycle Marathon — Create a container with
docker create, start it, pause it, unpause it, stop it, restart it, and finally remove it. At each step, verify the state withdocker inspect -f '{{.State.Status}}'. - Go Template Mastery — Using only
docker inspectwith format strings, extract: (a) the container's IP on a user-defined network, (b) all mounted volumes with their source paths, (c) the exact command running as PID 1, (d) the container's health check command. - Resource Detective — Run 5 containers (nginx, postgres, redis, node app, python app) with different memory limits. Use
docker stats --no-streamwith a custom format to generate a markdown table of container name, CPU %, memory usage, and memory limit. - Cleanup Audit — Run
docker system df -vand identify: (a) images that are candidates for removal, (b) containers that have been stopped for more than 24 hours, (c) volumes not attached to any container. Write a cleanup script that removes only safe-to-remove resources. - Air-Gap Transfer — Save a multi-layer image to a tar file, check the file size, compress it, transfer to a different Docker host (or simulate with
docker rmi+docker load), and verify all tags and layers were preserved.
Conclusion & Next Steps
The Docker CLI is far more than docker run and docker stop. Mastering the full command set — lifecycle management, inspection with Go templates, resource monitoring, and system maintenance — gives you complete operational control over your container infrastructure.
Key takeaways from this article:
- Management commands (
docker container,docker image) provide discoverability; legacy shorthand provides speed - Container lifecycle follows a clear state machine: Created → Running → Paused/Stopped → Removed
- docker run is the Swiss Army knife — understand every flag for both development and production scenarios
- docker exec is your debugging entry point;
docker logsanddocker inspectare your diagnostic tools - Go template formatting transforms
docker inspectfrom a JSON dump into a precision extraction tool - Regular maintenance with
docker system pruneprevents disk exhaustion in development and CI environments - save/load preserves image layers for transport; export/import flattens container filesystems
Now that you can drive Docker fluently from the command line, Part 7 takes you behind the scenes of image creation — the Dockerfile. You'll learn how every instruction translates to a layer, how build caching works, and how to write Dockerfiles that produce minimal, secure, and cache-efficient images.
Next in the Series
In Part 7: Dockerfile Fundamentals, we will master every Dockerfile instruction — FROM, RUN, COPY, CMD, ENTRYPOINT — and understand how the layer-by-layer build model affects image size, security, and build performance.