5. Docker integration โ
๐ฏ What you'll learn in this chapter:
- Docker basics โ Image vs Container, the layer model
docker run,exec,logsโ everyday commands- Port forwarding and volume mount
- Writing a Dockerfile โ with multi-stage build
docker composeโ multi-container apps- Managing Docker from Bash scripts (sandbox pattern)
- Cleanup and resource management
- A real example โ PostgreSQL test sandbox
โฑ Time: ~35 minutes ๐งช Exercises:
bashlings watchโ 7 interactive exercises ready (exercises/15_docker/)
5.1. What is Docker? โ
A container wraps an application together with all its dependencies and runs it in an isolated environment.
Difference from a VM:
- VM โ full operating system emulation (heavy, slow boot)
- Container โ shares one kernel, but is isolated at the process/network/filesystem level (lightweight, starts in seconds)
Core idea
It solves the "works on my machine" problem. A container is the environment on your machine + your application โ as a single package.
Image vs Container โ the critical difference โ
| Image | Container |
|---|---|
| Static template (read-only) | A running instance of the image |
| Made up of layers | Image + writable layer + runtime state |
Shown by docker images | Shown by docker ps |
Composition: Dockerfile | Created with: docker run <image> |
| Size: MB-GB | Image size + changes |
Analogy: image โ class, container โ instance of the class.
5.2. Installation and first connection โ
# macOS โ Docker Desktop or OrbStack
brew install --cask docker # Docker Desktop
brew install orbstack # alternative (lighter)
# Ubuntu / Debian
curl -fsSL https://get.docker.com | sh
# Check the version
docker --version # Docker version 27.x...
docker info # whether the daemon is connected or notFirst test:
docker run --rm hello-world
# Hello from Docker! ...--rm โ automatically remove the container after it finishes.
5.3. Core commands โ
Starting a container โ
# Simple
docker run nginx # in the foreground (stops with Ctrl+C)
docker run -d nginx # detached (background)
docker run -d --name web nginx # with a name
# Port forwarding
docker run -d -p 8080:80 nginx
# โ โ
# host container
curl http://localhost:8080 # connect to nginx
# Volume mount
docker run -d -v $(pwd)/html:/usr/share/nginx/html:ro nginx
# Environment variable
docker run -d -e POSTGRES_PASSWORD=secret postgres:16
# All together
docker run -d \
--name web \
-p 8080:80 \
-v $(pwd)/html:/usr/share/nginx/html:ro \
-e NGINX_HOST=example.com \
nginxClassic run flags โ
| Flag | Meaning |
|---|---|
-d (--detach) | To the background |
-it | Interactive + TTY (for a shell session) |
--rm | Remove after it finishes |
--name <name> | Container name (default โ random) |
-p HOST:CONT | Port mapping |
-v HOST:CONT[:ro] | Volume mount (ro = read-only) |
-e KEY=value | Environment variable |
--env-file .env | Load from an env file |
--network <net> | Custom network |
--restart unless-stopped | Auto-restart |
--memory 512m | Memory limit |
--cpus 1.5 | CPU limit |
Viewing containers โ
docker ps # running containers
docker ps -a # **ALL** (including stopped ones)
docker ps -aq # IDs only
docker ps --filter status=exitedManaging โ
docker stop web # send SIGTERM (waits 10s)
docker kill web # SIGKILL (immediately)
docker restart web
docker rm web # remove (must be stopped first)
docker rm -f web # stop and remove togetherImages โ
docker images # local images
docker pull nginx:1.25 # download from the server
docker rmi nginx:1.25 # remove
docker image prune # clean up unused images5.4. Working interactively โ
"Getting inside" a container โ
# A bash session in a new container
docker run -it --rm ubuntu:24.04 bash
# root@a1b2c3:/# ls
# root@a1b2c3:/# exit
# Inside a running container
docker exec -it web bash
docker exec -it web sh # nginx, alpine โ `sh` is the default-it:
-iโ STDIN open (can receive input)-tโ allocate a TTY (appears as a terminal)
Viewing logs โ
docker logs web # everything
docker logs -f web # follow (real-time)
docker logs --tail 50 web # last 50 lines
docker logs --since 10m web # last 10 minutes
docker logs --timestamps web # with timestampsdocker logs -f = tail -f
Most container images write logs to stdout/stderr. docker logs captures them. In production โ centralized log aggregation (Loki, ELK) is preferred.
Container statistics โ
docker stats # real-time CPU/memory/network
docker stats --no-stream # a single snapshot
docker inspect web # full JSON metadata
docker inspect web | jq '.[0].NetworkSettings.IPAddress'5.5. Volume mount and port mapping โ
Bind mount (host โ container) โ
# Mount an existing directory
docker run -d -v $(pwd):/app -w /app node:20 npm start
# Read-only
docker run -d -v /etc/myapp:/config:ro myapp
# Macros โ $(pwd) gives an absolute path
docker run -v $(pwd):/data busybox ls /dataNamed volume (Docker-managed) โ
# Create a volume
docker volume create pgdata
# Use it
docker run -d -v pgdata:/var/lib/postgresql/data postgres:16
# All of them
docker volume ls
docker volume inspect pgdata
docker volume rm pgdataBind mount permissions
The user inside the container (usually root or UID 1000) must access the files on the host with the correct permissions. On macOS it usually works; on Linux a chown may be needed.
Port mapping nuances โ
docker run -p 8080:80 nginx
# Host 0.0.0.0:8080 โ container 80
docker run -p 127.0.0.1:8080:80 nginx
# Accessible only from localhost (secure)
docker run -p 8080:80/udp myudp
# UDP port
docker run -P nginx
# ALL EXPOSED ports on RANDOM host ports
docker ps # see which port it got5.6. Environment variables โ
# Several -e
docker run -d \
-e POSTGRES_USER=admin \
-e POSTGRES_PASSWORD=secret \
-e POSTGRES_DB=myapp \
postgres:16
# From an env file
cat > .env <<'EOF'
POSTGRES_USER=admin
POSTGRES_PASSWORD=secret
POSTGRES_DB=myapp
EOF
docker run --env-file .env -d postgres:16Secrets and env vars
docker inspect <container> โ shows all env vars in JSON. Passing secrets via env vars is OK for a lab, but dangerous in production. Use:
- Docker secrets (Swarm)
- Kubernetes Secrets
- Vault, AWS Secrets Manager
- Or at minimum:
--env-file+ the file with 600 permissions
5.7. Dockerfile โ building an image โ
Simple example โ
# Dockerfile
FROM alpine:3.19
# Dependency packages
RUN apk add --no-cache curl jq
# Copy the script
COPY healthcheck.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/healthcheck.sh
# Working directory
WORKDIR /app
# Default command
CMD ["/usr/local/bin/healthcheck.sh"]Build and run:
docker build -t myhealthcheck:1.0 .
docker run --rm myhealthcheck:1.0Classic directives โ
| Directive | Meaning |
|---|---|
FROM | Base image (at the start of every Dockerfile) |
RUN | Run a command during build |
COPY src dst | Copy from local into the image |
ADD src dst | COPY + URL/archive support (less recommended) |
WORKDIR /x | Set the current directory |
ENV K=V | Environment variable |
EXPOSE 8080 | Documentation โ port exposure (no automatic mapping) |
CMD ["..."] | Default startup command (can be overridden) |
ENTRYPOINT [...] | Main command (CMD provides arguments) |
USER ali | User (important for security) |
HEALTHCHECK | Health check command |
LABEL k=v | Metadata |
.dockerignore โ
# .dockerignore โ what to SKIP during `COPY .`
node_modules
.git
*.log
.env*
dist
targetIt reduces build time and improves security (keeps secrets out of the image).
Multi-stage build โ smaller image size โ
# Stage 1 โ build environment (large, only for building)
FROM rust:1.78 AS builder
WORKDIR /app
COPY . .
RUN cargo build --release
# Stage 2 โ runtime (small, only the binary)
FROM debian:12-slim
COPY --from=builder /app/target/release/myapp /usr/local/bin/
CMD ["myapp"]| Approach | Final image size |
|---|---|
FROM rust:1.78 | ~1.5 GB |
Multi-stage debian:12-slim | ~80 MB |
Multi-stage scratch | ~10 MB |
Layer caching
Each RUN, COPY is a new layer. Layers are cached. Put frequently changing things later:
COPY package.json package-lock.json ./
RUN npm ci # โ cached, won't rerun unless dependencies change
COPY . . # โ every time, fresh5.8. docker compose โ multi-container app โ
A single docker-compose.yml โ the whole stack (web + db + cache + ...).
docker-compose.yml example โ
services:
db:
image: postgres:16
environment:
POSTGRES_DB: myapp
POSTGRES_USER: admin
POSTGRES_PASSWORD: secret
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U admin"]
interval: 5s
retries: 5
cache:
image: redis:7-alpine
ports:
- "127.0.0.1:6379:6379"
web:
build: . # from the current Dockerfile
ports:
- "8080:80"
environment:
DATABASE_URL: postgres://admin:secret@db:5432/myapp
REDIS_URL: redis://cache:6379
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
volumes:
pgdata:Managing โ
docker compose up -d # start in the background
docker compose ps # view status
docker compose logs -f web # log for a single service
docker compose exec db psql -U admin myapp # interactive
docker compose restart web
docker compose down # stop + remove containers
docker compose down -v # + also the volumesService names = DNS โ
Services within Compose find each other by name:
# From inside the web container
curl http://db:5432 # connects to the `db` service
redis-cli -h cache # to the `cache` serviceThis is Compose's most magical feature. No network configuration needed.
5.9. Managing Docker from Bash scripts โ
Parsing container status โ
# Status โ as a string
status=$(docker inspect -f '{{.State.Status}}' web)
echo "$status" # running / exited / paused / ...
# More powerful with JSON + jq
docker ps --format json | jq -r 'select(.Names == "web") | .Status'Health check loop โ
#!/usr/bin/env bash
# wait-for-healthy.sh โ wait for a container to become healthy
NAME="${1:?Container name required}"
MAX_WAIT=60
for ((i=0; i<MAX_WAIT; i++)); do
state=$(docker inspect -f '{{.State.Health.Status}}' "$NAME" 2>/dev/null || echo "missing")
case "$state" in
healthy) echo "โ $NAME healthy"; exit 0 ;;
starting) echo " Starting up... ($i/$MAX_WAIT)" ;;
unhealthy) echo "โ $NAME unhealthy"; exit 1 ;;
missing) echo "Container not found"; exit 2 ;;
esac
sleep 1
done
echo "Timeout"
exit 1Cleanup pattern (with trap) โ
#!/usr/bin/env bash
set -euo pipefail
CONTAINER="test-pg-$$" # $$ โ current PID
cleanup() {
docker rm -f "$CONTAINER" 2>/dev/null || true
}
trap cleanup EXIT
# Start it
docker run -d --name "$CONTAINER" \
-e POSTGRES_PASSWORD=test \
-p 5432:5432 \
postgres:16
# Wait
sleep 5
# Test work
psql -h localhost -U postgres -c 'SELECT version();'
# trap cleanup is called automatically5.10. A real example โ Test database sandbox โ
A production-grade pattern: spin up a clean PostgreSQL for each test.
#!/usr/bin/env bash
#
# with-pg.sh โ run each command in a clean PostgreSQL sandbox
#
# Usage:
# ./with-pg.sh psql -c 'SELECT 1;'
# ./with-pg.sh ./integration-tests.sh
#
set -euo pipefail
readonly CONTAINER="pg-sandbox-$$"
readonly DB_PORT=15432
readonly DB_PASSWORD="${DB_PASSWORD:-test_$(date +%s)}"
cleanup() {
local rc=$?
echo "๐งน Cleaning up the sandbox..."
docker rm -f "$CONTAINER" > /dev/null 2>&1 || true
exit "$rc"
}
trap cleanup EXIT INT TERM
echo "๐ฆ Starting the PostgreSQL sandbox..."
docker run -d \
--name "$CONTAINER" \
-e POSTGRES_PASSWORD="$DB_PASSWORD" \
-e POSTGRES_USER=test \
-e POSTGRES_DB=testdb \
-p "$DB_PORT:5432" \
postgres:16 > /dev/null
# Wait until it's ready
echo "โณ Waiting for the database to be ready..."
for i in {1..30}; do
if docker exec "$CONTAINER" pg_isready -U test > /dev/null 2>&1; then
echo "โ Ready (${i}s)"
break
fi
sleep 1
[[ $i -eq 30 ]] && { echo "โ Timeout"; exit 1; }
done
# Export the connection details
export PGHOST=localhost
export PGPORT="$DB_PORT"
export PGUSER=test
export PGPASSWORD="$DB_PASSWORD"
export PGDATABASE=testdb
# Run the user's command
echo "๐งช Running: $*"
"$@"Usage:
chmod +x with-pg.sh
# Simple test
./with-pg.sh psql -c 'SELECT version();'
# Integration tests
./with-pg.sh ./tests/run-integration.sh
# Interactive shell
./with-pg.sh bash
# PGHOST, PGPORT, ... are exported automaticallyThis script:
- Creates a clean sandbox each time it runs
- Patiently waits for PostgreSQL to become ready
- Exposes the connection parameters as env vars
- In every case (success/fail/Ctrl+C) โ the sandbox is cleaned up
- Cross-platform, nothing needs to be installed (other than Docker)
5.11. Cleanup and resource management โ
Docker's biggest worry is the disk filling up. Unused images, volumes, and networks accumulate.
Classic cleanup commands โ
# Clean up things nothing is using
docker container prune # stopped containers
docker image prune # dangling images
docker image prune -a # ALL unused
docker volume prune # unused volumes
docker network prune # unused networks
# All together
docker system prune # stopped + dangling
docker system prune -a # + unused images
docker system prune -a --volumes # + volumes (CAUTION โ data is lost)Viewing disk usage โ
docker system df # general report
# TYPE TOTAL ACTIVE SIZE
# Images 42 12 15.2GB
# Containers 18 4 312MB
# Local Volumes 7 3 2.1GB
docker system df -v # detailedAutomated cleanup (cron) โ
# Every Sunday night
0 3 * * 0 docker system prune -af --volumes --filter "until=168h" >> /var/log/docker-cleanup.log 2>&1--filter "until=168h" โ those older than 168 hours (7 days).
5.12. Security practices โ
# 1. Non-root user
FROM node:20-alpine
RUN addgroup -S app && adduser -S app -G app
USER app
WORKDIR /app
COPY --chown=app:app . .
CMD ["node", "server.js"]
# 2. Read-only filesystem
# docker run --read-only --tmpfs /tmp myapp
# 3. Restrict capabilities
# docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE myapp
# 4. Specific tag, not :latest
FROM node:20.11.0-alpine # โ specific
# FROM node:latest # โ moving target
# 5. Image scanning
# docker scout cves myapp:1.0 (Docker Desktop)
# trivy image myapp:1.0 (Trivy)Best practice checklist โ
- [ ] Don't use
:latestโ a precise version tag for every image - [ ] Run inside as non-root (
USER) - [ ]
.dockerignoreโ skip.env,.git, secrets files - [ ] Multi-stage โ small final image
- [ ] Image scanning โ
docker scoutortrivy - [ ] Resource limits โ
--memory,--cpus - [ ] Read-only filesystem when possible
- [ ] Secrets โ
--env-file(file with 600), Docker secrets, or Vault
5.13. Common mistakes โ
Classic pitfalls
localhostinside a container โ the host's localhost. To connect from inside a container to the host:- Mac/Windows:
host.docker.internal - Linux:
--add-host=host.docker.internal:host-gateway
- Mac/Windows:
Volume mount UID mismatch. uid 1000 on the host, uid 0 in the container โ permission errors.
--user $(id -u):$(id -g)helps.The
:latesttag โ dangerous in production. "It worked a week ago" โ becauselatestchanged. Always use a precise tag.docker rminstead ofdocker rmi.rmโ containerrmiโ image
docker logsis stdout, not a file. If you do> /var/log/...inside the container โ Docker won't see it.Build context is too large.
COPY . .โ the entire current directory is sent as the context..dockerignoreis mandatory.Confusing
CMDvsENTRYPOINT.CMD ["node", "x.js"]โdocker run myimageโ it starts.docker run myimage echo hiโ overridesCMD.ENTRYPOINT ["node"]+CMD ["x.js"]โdocker run myimage y.jsโnode y.js.
Mixing up named vs bind volumes.
-v /host/path:/in/containerโ bind (host path).-v myvolume:/in/containerโ named volume.Forgetting network isolation. By default containers are not on the same network โ use
docker network create mynetand--network mynet, or use Compose.Slow file writes on macOS. Bind mounts on macOS are 10-100ร slower than on Linux. If you need performance โ a named volume or OrbStack.
5.14. Exercises โ
๐งช Bashlings โ interactive exercises
This chapter's 7 exercises come with auto-checking via the bashlings CLI. None of them require a Docker daemon โ they work with syntax and configuration:
bashlings watch # start from the first pending exercise
bashlings run docker1 # check a single exercise
bashlings hint docker1 # step-by-step hintSource: exercises/15_docker/
Try the following real-world exercises wherever Docker is installed:
Quick Nginx โ start nginx via
docker run(on port 8080), confirmcurl localhost:8080works, and clean up withdocker rm -f.Custom Dockerfile โ build an alpine-based image with
curlandjqinstalled (-t myhealthcheck). Run it and see the output ofcurl --version.Compose โ write a
docker-compose.yml: nginx + postgres + redis. Runup -dand check that all 3 arerunning.Health wait script โ run
wait-for-healthy.sh(above) on your machine and write a script that waits for the postgres container to become healthy.Cleanup automation โ automate
docker system prune -afevery Sunday at 03:00 via cron (with logging).
5.15. Summary โ
| Concept | Key point |
|---|---|
| Image | Static template |
| Container | A running instance of the image |
docker run | Start a new container |
docker ps | Running containers |
docker ps -a | + stopped ones |
-d | Detached |
-it | Interactive + TTY |
--rm | Auto-cleanup |
-p HOST:CONT | Port mapping |
-v PATH:PATH | Volume mount |
-e KEY=val | Env variable |
docker exec -it X bash | "Get inside" a container |
docker logs -f X | Real-time logs |
Dockerfile | Recipe for building an image |
| Multi-stage | Small final image (10ร reduction) |
docker compose up | Multi-container stack |
docker inspect -f | Metadata for scripts |
docker system prune -af | Clean up |
5 key ideas โ
- Image โ Container โ class vs instance.
- The
:latesttag is the enemy โ always use a precise version in production. - Multi-stage build โ reduces the final image size by 10-100ร.
- Docker Compose โ the standard for every local dev environment.
docker system pruneregularly โ otherwise the disk fills up quickly.
๐ You now have the skill to manage containers and integrate Bash scripts with Docker. In the next and final chapter โ automating everything in CI/CD via GitHub Actions.
Next page: 6. CI/CD โ GitHub Actions โ