24 July, 2026

1. Foundations

The standard hierarchy of an application environment, from top to bottom:

App Code
Third-Party Libraries
Runtime
Userland
Kernel
Hardware

Containerization vs. Virtualization

ContainerizationVirtualization
MechanismPackages an app as an isolated process sharing the host’s kernelUses a Hypervisor to abstract hardware and run a full, independent guest OS
OverheadLightweightHeavier

Kernel-Level Isolation Primitives

Operating systems expose native features for isolating and managing resources:

  • Windows: Job objects and Object namespaces
  • Linux: Cgroups (Control Groups) and Namespaces

A userland library sits on top of these kernel primitives so higher-level tools don’t have to call the kernel directly.

2. History: LxC, dotCloud & Docker

LxC (Linux Containers)

  • One of the first major container runtimes.
  • Provided the functions needed to drive the kernel isolation primitives above.
  • Aimed squarely at C programmers — a low-level, developer-facing library.
  • Higher-level toolkits eventually emerged to make LxC-style isolation easier to use.

dotCloud → Docker

  • dotCloud was a cloud vendor offering a simpler alternative to full virtualization.
  • Historically limited to Linux applications. (Today Docker also supports native Windows containers, and uses lightweight utility VMs to run on macOS and Windows.)
  • dotCloud built an internal toolkit for packaging apps into isolated environments.
  • That toolkit was open-sourced as Docker.
  • dotCloud (the company) eventually shut down and rebranded entirely as Docker, Inc.

Runtime Lineage

  • Docker originally built on LxC.
  • Docker later moved off LxC to build its own Go-based tool, libcontainer, for tighter integration.
  • libcontainer evolved into runc.
  • LxC is still actively developed today (e.g., LXD/Incus).

3. Images vs. Containers

Create vs. Start: Creating a container just defines it — a container is created from an image; starting it launches the primary process.

What’s in an Image?

ComponentDescription
MetadataInstructions for the container runtime: isolation rules, memory limits, the primary process (CMD), etc.
DataThe actual file system contents present in every container built from the image

The Primary Process (CMD) starts and stops everything in the container. The lifetime of the primary process is the lifetime of the container.

Images are built with tooling and published to a registry.

4. Standards: OCI & CNCF

BodyRole
OCI (Open Container Initiative)Owned by the Linux Foundation. Defines the standard for what a container is, what an image is, and how isolation works. Maintains runc.
CNCF (Cloud Native Computing Foundation)Hosts tools and platforms built on top of OCI standards (e.g., Kubernetes, containerd).

Key components:

  • runc — the reference implementation of the OCI runtime spec; the actual engine that sandboxes and runs containers.
  • containerd — a graduated CNCF project (donated by Docker). Downloads images from a registry, stores them locally, and invokes runc to execute them.

Note on the call chain: Docker doesn’t call runc directly — it calls containerd, which in turn calls runc. (“Docker → containerd → runc.”)

5. Namespaces & Isolation

Containers isolate the following Linux namespaces:

  • Process (PID)
  • File System (Mount)
  • Network (Net)
  • Hostname (UTS)
  • Inter-Process Communication (IPC)
  • User — isolates UIDs/GIDs, letting a process be root inside the container while mapping to an unprivileged user on the host.

Process Isolation in Practice

  • Starting a container creates a new host process wrapped in a namespace.
  • Ten containers = ten fully isolated environments; each only sees processes within its own namespace, and PIDs are aliased starting from 1 inside each container.
  • A container is kept alive solely by its PID 1. If PID 1 dies, everything else in the container stops too.
  • kill -9 1 fails inside a container. Per Linux’s PID-namespace semantics, a namespace’s PID 1 ignores unhandled signals — including SIGKILLwhen the signal originates from within the same PID namespace. (A SIGKILL sent from outside the namespace, e.g. docker kill, still works normally.)

6. Image Storage: The Union File System

  • An image is a stack of layers (data + metadata) combined in order.
  • A private file system is created per-container by unioning these layers together.
  • Images and layers are each uniquely identified by a SHA256 hash and verified via checksum.
  • Deduplication: each unique layer is stored once on disk; images sharing base layers don’t re-download or re-store them.

Two Golden Rules

1. Images are immutable.

  • Once a layer is part of an image, it never changes.
  • Container writes go to a new, container-specific Container Storage Layer — the only writable layer.
  • Copy-on-Write (CoW): editing a file that exists in an image layer copies the whole file up to the Container Storage Layer first, then edits it there.
  • The Union File System itself lives in memory — running N containers from the same image costs zero extra disk space beyond each container’s own storage layer.
  • Container storage is created with the container and destroyed with it.

2. Containers are disposable.

  • Only ephemeral data belongs in a container’s storage layer — never store permanent data there.

7. Registry & Naming Conventions

registry_name/account_name/repo:tag
SegmentDefault if omitted
registry_namedocker.io
account_namelibrary
repo(mandatory — no default)
taglatest

8. Docker CLI Syntax

  • Format: docker <context> <command> — context is always singular (image, not images).
  • Shortcut: docker <command> works when the command name is unique across contexts.
  • Options: single-hyphen for single letters (-p, -i, -t), double-hyphen for full words (--publish, --interactive). Single-letter flags can be chained (-it).
  • Values: separate an option from its value with a space or =.
  • Parameters: no hyphens, passed directly at the end of the command.
  • Order: all options come before parameters.

9. Command Reference

System & Image Info

docker version
docker image ls          # or: docker images
docker system df         # disk usage summary

Example docker system df output:

TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          39        7         2.931GB   853.3MB (29%)
Containers      7         0         3.523MB   3.523MB (100%)
Local Volumes   8         4         72.4MB    28.67kB (0%)
Build Cache     309       0         14.48GB   12.8GB

Managing Images

docker image pull alpine:latest
docker image history alpine:latest
docker image pull nginx:alpine
docker image rm nginx:alpine
docker image rm alpine:latest
docker pull alpine:3.23.5
docker pull nginx:alpine
 
# Inspect an image
docker image inspect alpine:3.24     # or: docker inspect alpine:3.24

Example docker image history output:

IMAGE          CREATED       CREATED BY                                      SIZE      COMMENT
28bd5fe8b56d   5 weeks ago   CMD ["/bin/sh"]                                 0B        buildkit.dockerfile.v0
<missing>      5 weeks ago   ADD alpine-minirootfs-3.24.1-x86_64.tar.gz /…   9.07MB    buildkit.dockerfile.v0

Example inspect CMD snippet:

"Cmd": [
    "nginx",
    "-g",
    "daemon off;"
]

Container Lifecycle

docker container create <image_name>
docker container ls
docker container ls -a           # or: docker container ls --all
docker container start c1

Checking host-level processes:

ps -aef | grep nginx
# Output shows host PIDs corresponding to containerized nginx processes

Executing Commands Inside a Container

Useful for debugging — commands can run interactively (with I/O) or non-interactively.

docker container exec c1 hostname
docker container exec -it c1 /bin/sh   # or: --interactive --tty

Inside the container shell:

/ # ps -aef
PID   USER     TIME  COMMAND
    1 root      0:00 nginx: master process nginx -g daemon off;
   30 nginx     0:00 nginx: worker process

Stopping & Inspecting Containers

docker container stop <container_name>

Stop protocol: Docker sends SIGTERM first; if the process doesn’t shut down gracefully, it follows up with SIGKILL.

docker container diff <container_name>

Shows what changed in the container’s storage layer:

  • A = Added
  • D = Deleted
  • C = Changed
C /var/cache
A /var/cache/nginx/client_temp
C /etc/nginx/conf.d/default.conf

A large diff output is a red flag — it usually means data is being written to the container’s temporary storage layer instead of a permanent mount.

Storage Persistence Notes

  • Container storage survives a stop, but is permanently wiped on delete.
  • Logs (STDOUT/STDERR from PID 1) are stored separately from container storage and mounted storage, and persist across stops but not deletes.

10. Networking

  • Containers are isolated via network interfaces, each assigned its own IP.
  • Each container gets a dedicated interface mapped to its own network namespace — it can’t see other interfaces.
  • Containers on the same Docker network can talk to each other directly.
  • Port forwarding exposes a container port to the outside world, set at creation time: `shdocker container create —name c3 —publish 8080:80 nginx:alpine
  • DNS inside a container can be configured explicitly at creation, or defaults to whatever DNS server the host engine provides.

25 July, 2026

1. Persistent Storage

For durable storage, mount external storage into the container.

  • Shadowing: mounting onto a directory that already has data hides (shadows) the existing container data.
  • OCI standard: mounts can target either a directory or a single file.
  • Immutability of mounts: all mounts for a container are defined at creation time (docker create / docker run) and cannot be added or removed while the container is running.
  • Bypassing the UFS: writes to a mount path go straight to the external storage, bypassing the container’s writable layer entirely.
  • Tracking bloat: docker container diff <name> only shows changes to the container’s own UFS layer — a large diff signals data is landing in temporary storage instead of a mount.

2. Bind Mounts

Maps a specific host directory or file to a directory or file inside the container.

  • Always use absolute paths for the host side.
  • Windows note: Docker Desktop mounts the Windows file system into its hidden Linux VM first, then binds from the VM into the container. This automatic translation does not happen on native Linux/server hosts.

Syntax

Preferred (--mount):

docker container create --name v1 \
  --mount type=bind,source=/home/tanuv/projects/docker-training/my-dir-1,target=/data \
  nginx:alpine

Legacy (-v):

docker container create --name v1 \
  -v /home/tanuv/projects/docker-training/my-dir-1:/data \
  nginx:alpine

Inspecting a Bind Mount

"Mounts": [
  {
    "Type": "bind",
    "Source": "/home/tanuv/projects/docker-training/my-dir-1",
    "Target": "/data"
  }
]

Aside: exec -it flags

docker container exec -it v1 /bin/sh
  • -t allocates a pseudo-TTY.
  • -i keeps stdin open (OpenStdin: true).
  • Alpine is a common debugging image because its userland is guaranteed to include sh, even when the target image doesn’t.

3. Volume Mounts

A volume is an isolated unit of external storage fully managed by Docker — could be a local disk directory, NFS, or cloud storage depending on the volume driver in use.

  • Default driver: local (built in).
  • Protection: Docker won’t let you delete a volume that’s still referenced by a container.
  • Concurrency: one volume can be mounted into multiple containers at once — but Docker does not manage file locking. Preventing corruption is the application’s responsibility.

Shadowing Rule for Volumes

  • Mount volumes onto empty directories where possible.
  • If you mount an empty volume onto a directory that already has files, Docker (by default) copies the existing container files into the volume on creation. Don’t rely on this — other engines (e.g., Podman) may behave differently.

Commands

docker volume ls

Create automatically at container creation (read-only example):

docker container create -v <volume_name>:<container_directory>:ro image:tag

Or via --mount:

docker container create \
  --mount type=volume,source=<volume_name>,target=<container_directory>,readonly \
  image:tag

Remove a volume:

docker volume rm volume-1

Removing a volume still in use returns: Error response from daemon: remove volume-1: volume is in use

4. TMPFS (Temporary File System)

Mounts data directly into host RAM rather than disk. Not persistent.

  • Use cases: image processing scratch space, temporary caches, sensitive secrets/keys that should never touch a physical disk.
  • Performance: very fast, since it’s memory-backed.
  • The -v flag cannot be used for tmpfs — you must use --mount (or the tmpfs shorthand).

Syntax

docker container create --mount type=tmpfs,target=<container_directory> image:tag

Shorthand: --tmpfs <container_directory>

A read-only tmpfs mount (--mount tmpfs-options=ro) is technically possible but practically useless — since tmpfs always starts empty, a read-only mount means the app can never write to it or meaningfully read from it.

5. Storage Best Practices

  • The application/Dockerfile author should decide the target mount directory — not the end user.
  • Images should advertise their expected persistent-data location via the Dockerfile’s VOLUME instruction.
  • Discover advertised mount points via: docker image inspect <image_name>:<tag>

6. Docker Hub & Image Tagging

  • Official Images are maintained by the software’s original creators, in collaboration with Docker, with quality guarantees from Docker.
  • Docker Hardened Images are a separate, security-focused offering.
  • Registry: hub.docker.com

Partial Version Tags

Multiple tags can point to the same image; partial versions point to the latest matching full version (e.g., 4.3 currently resolves to 4.3.4, but could later move to 4.3.5):

4.3.4, 4.3, 4, latest
4.2.9, 4.2
4.2.9-management, 4.2-management
4.2.9-alpine, 4.2-alpine
  • Images can also be pulled by their content hash for full reproducibility.
  • Userland evolution: many official images started on Ubuntu (feature-rich but bloated) → moved to Debian (lighter) → moved to Alpine (minimal) as the default/optional flavor. If a tag doesn’t specify a flavor, it’s usually Debian by default.

7. Container Creation Workflow (CSCC)

General workflow and concepts for spinning up any container — not specific to Docker Hub, but this is where the notes originally introduced them.

run vs. create + start

docker run -d --hostname my-rabbit --name some-rabbit rabbitmq:3
  • docker run = create + start + attach.
  • -d runs detached.
  • Reattach later with: docker container attach <container_name>

The “CSCC” Mental Checklist

When setting up a new container, decide in this order:

  1. Command — which image/version, and any command override
  2. Storage — where persistent data lives
  3. Communication — port forwarding
  4. Configuration — environment variables, etc.

Environment Variables

  • Managed by the OS, per-process — each process can have its own set.
  • Set at process start, or modified while running.
  • A child process can optionally inherit its parent’s environment variables.
  • Set at container creation with -e <variable>=<value> or --env <variable>=<value>.

8. RabbitMQ

(Notes pending — revisit.)

9. PostgreSQL

  • bookworm and trixie are Debian release codenames used in Postgres image tags.

Creating a Container

Basic one-shot run:

docker run --name some-postgres -e POSTGRES_PASSWORD=mysecretpassword -d postgres

Basic creation (no config, mainly for inspection):

docker container create postgres

Full example with a named volume:

docker container create --name pg1 \
  --mount type=volume,source=pg1vol,target=/var/lib/postgresql \
  -e POSTGRES_PASSWORD=something \
  -e POSTGRES_USER=admin \
  postgres:latest
  • To find where data is stored, check the image’s advertised -v/--mount target.

Overriding CMD

A container’s CMD can be overridden by appending a command after the image name — useful for connecting a throwaway psql client to a running server on the same network:

docker run -it --rm --network some-network postgres psql -h some-postgres -U postgres

Exit Codes & Logs

  • Exit codes: 0 = clean exit; anything non-zero = crash (127 = killed).
  • Logs: docker container logs <container_name>
    • PID 1 is expected to write logs to STDOUT/STDERR; logs persist across stops but are deleted with the container. Log storage is distinct from container storage and mounted storage — managed separately by the container engine. (Open question: how are logs handled with multiple concurrent processes inside one container?)

Inspecting the Container’s IP

docker container inspect pg1:

"Networks": {
    "bridge": {
        "IPAMConfig": null,
        "Links": null,
        "Aliases": null,
        "DriverOpts": null,
        "GwPriority": 0,
        "NetworkID": "efe83191686201734ebf3a632c26279c678b63a0d058c9f039900dfd716f994b",
        "EndpointID": "0ada15977653aa09eddfa111985d490e7ae85fece48cb8932914250969ae36f6",
        "Gateway": "172.17.0.1",
        "IPAddress": "172.17.0.2",
        "MacAddress": "72:44:c2:1a:69:9e",
        "IPPrefixLen": 16,
        "IPv6Gateway": "",
        "GlobalIPv6Address": "",
        "GlobalIPv6PrefixLen": 0,
        "DNSNames": null
    }
}

10. MySQL

docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:tag
  • Data directory inside the container: /var/lib/mysql
  • Default port: 3306 (verify against current docs for the specific tag)

11. Adminer

docker container create --name ad1 --publish 18080:8080 adminer
  • Adminer connects to Postgres/MySQL internally over the Docker network.
  • Port forwarding is only needed for external → container access; container-to-container traffic stays on the internal Docker network.

12. Docker Networks

Docker networks provide a software-defined way to connect containers and simulate a network experience, making them act as separate machines on a shared network.

To list all available networks, use:

docker network ls

Networking is handled by Network Drivers, which are built-in software components that use various techniques (like tunnel tapping) to ensure connectivity.

Network Drivers

There are three default networks — bridge, host, and none — plus the MACVLAN/IPVLAN drivers for custom networks:

  1. Bridge Network (Default)

    • Behavior: When a container is created without specifying a network, it automatically connects to the default bridge network (docker0).
    • Accessibility: Containers on the same bridge network can communicate internally. However, they cannot be accessed from the outside host directly; you must use port forwarding for external access.
    • Verification: You can verify a container’s network details by running docker container inspect <container_name>.
  2. Host Network

    • host means do not isolate this container’s network — it can access all networks the host has.
    • Example: docker container create --name h1 --network host nginx:alpine
  3. None Network

    • none means no networking at all is attached to the container.
  4. MAC VLAN & IP VLAN Methods

    • These drivers allow containers to appear as physical devices on your network, meaning they can be accessed directly from outside the host without standard port forwarding.

13. IP Address Management (IPAM)

Network properties include an IPAM section, which defines the range of IP addresses allocated to the network.

  • IPv4 Addresses: Composed of 32 bits (4 bytes), divided into four 8-bit sections.
  • Reserved Addresses: Generally, 0 is reserved for the network address, and 255 is used for broadcast.
  • CIDR & Subnets: IP addresses are accompanied by a subnet mask using CIDR notation (e.g., /16 or /24), which dictates the network size.

14. Custom Networks and DNS

Best Practice: Always create your own custom network.

When you create a custom network, Docker automatically runs an internal DNS server. This DNS server maps container names to their internal IP addresses, allowing containers to communicate using their names instead of IPs.

  • Isolating Environments: You can place frontend containers on one network and backend containers on another. If they need to communicate, you can create a shared connecting network for both.
  • Dynamic Connectivity: Unlike some settings, network metadata and connections can be changed dynamically while containers are running.

Common Network Commands

Create a Custom Network:

docker network create --driver=bridge --subnet 192.168.125.0/24 pnet1

Create a Container on a Custom Network:

docker container create --name pn1 --network pnet1 nginx:alpine

Connect a Running Container to a Network:

docker network connect <network_name> <container_name>

Network Aliases (Load Balancing)

docker container create --name pn4 --network pnet1 --network-alias webserver nginx:alpine
  • --network-alias gives the container another name on the network.
  • If multiple containers share the same network alias, and something hits that alias, Docker automatically load-balances requests across them.
  • Use case: one way of achieving load balancing and redundancy is giving the same network alias to multiple containers.

15. Inspecting a Network (Example)

Running docker network inspect bridge provides detailed information about the network, including its IPAM configuration and connected containers.

[
    {
        "Name": "bridge",
        "Id": "9025e0d8...",
        "Scope": "local",
        "Driver": "bridge",
        "IPAM": {
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",
                    "Gateway": "172.17.0.1"
                }
            ]
        },
        "Containers": {
            "10a939...": {
                "Name": "ad1",
                "IPv4Address": "172.17.0.3/16"
            },
            "729d19...": {
                "Name": "n2",
                "IPv4Address": "172.17.0.5/16"
            },
            "8d6264...": {
                "Name": "n1",
                "IPv4Address": "172.17.0.4/16"
            },
            "dbcb8c...": {
                "Name": "pg1",
                "IPv4Address": "172.17.0.2/16"
            }
        }
    }
]

16. Docker Compose

Evolution: Previously, docker-compose was a separate, standalone tool. It has now been integrated directly into Docker as docker compose.

How it Works:

  • Docker Compose reads manifest files (typically docker-compose.yaml).
  • Based on the configuration in these files, it orchestrates the creation of multiple containers, volumes, and networks simultaneously.
  • It provides a standardized solution for defining and managing multi-container applications, a feature commonly offered by all modern container engines.
  • Docker Compose does not put any services on the default bridge network — it creates a new one for the project.

YAML Format

  • In YAML, what we are defining are documents or objects.
  • We can define one document/object or multiple documents/objects.
  • A YAML document consists of properties/fields and their values. Their syntax is field_name: value. Assume no space before field_name and no space after field_name. Must begin with a letter and end with a letter. In between we can have alphanumeric, dashes, etc.
  • Three kinds of values:
    • scalar: A scalar value is written by writing one space after the colon, and then writing the value. E.g. field_name: value. No decorations needed for value. YAML will figure it out. Either single quotes, or double quotes. Single quotes takes the entire string as it is. Double quotes can allow special cases like escape tabs, etc. yes and no are valid boolean.

    • lists: Each value is called an element. All indentations should be spaces. Cannot handle tabs. And values should be aligned with the field. They can be written like:

      array_field_1:
      - value1
      - value2
      - value3
    • map: A field contains objects with properties. The sub properties are indented two spaces. Each level is two spaces of indentation.

      map_field_1:
        sub_property_1: value1
        sub_property_2: value2
        sub_property_3:
          sub_sub_property_1: value11
          sub_sub_property_2: value22
    • Nested array:

      array_field_1:
        - prop1: value1
          prop2: value2
          prop3: value3
        - prop1: value1
          prop2: value2
          prop3: value3

Docker Compose File

A compose file has 3 top-level properties: networks, volumes, and services (containers).

How the volumes property works (on a service): lists named-volume:mount-path pairs, mounting each named volume declared in the top-level volumes: block into the container:

volumes:
  - dbvol:/var/lib/postgresql

How the networks property works (on a service): lists which top-level networks, declared in the networks: block, the service should attach to:

networks:
  - db-net

Sample compose file:

networks:
  # DB Network
  db-net:
 
volumes:
  # DB Volume
  db-vol:
 
services:
  # Postgres Database Server
  db-server:
    image: postgres:18.4-alpine3.24
    volumes:
      - db-vol:/var/lib/postgresql
    networks:
      - db-net
    environment:
      - POSTGRES_USER=admin
      - POSTGRES_PASSWORD=something    
  
  # Frontend Server Using Adminer
  fe-server:
    image: adminer:5.5.0
    networks:
      - db-net
    ports:
      - 8080
    environment:
      - ADMINER_DEFAULT_SERVER=db-server
      - ADMINER_DESIGN=lucas-sandery
  • Service discovery: refer to other services by their service name (e.g. db-server) instead of container name or IP — Compose automatically creates a network alias matching the service name.

Compose Commands

  • docker compose config
    • -f or --file: specify the manifest file name
    • -p: specify the project name
  • docker compose ps — lists the project’s containers.
  • docker compose up -d — checks the desired state vs. the current state; creates what’s missing, removes what shouldn’t exist. If something in the compose file changed and you re-run it, only the changed resources (and their dependencies) are updated.
    • --remove-orphans — removes containers not defined in the compose file.
    • Open question: if the hardcoded host ports value is removed/updated in the file, what happens on the next up -d? (Needs verification.)
  • docker compose -p db-app-2 up -d — run under a custom project name (same as the -p flag above).
  • docker compose ls — shows how many projects are deployed.
  • docker compose down — brings down containers and networks, but does not delete volumes by default. Use -v / --volumes to also remove volumes.
  • docker compose up / docker compose start / docker compose stop
  • docker compose exec — no need to add -it; use the service name instead of the container name.
  • prune command — (noted, not yet detailed).

17. Docker Images

Indicators of Bad Images

  • Should be a one-shot setup.
  • The image size should not be big.
  • Every app should be a separate container — single responsibility. Layer planning should be done, but avoid too many layers, as it causes load on the union file system.

18. How to Plan and Create Your Own Image?

  • Step 1: Identify your PID 1
  • Step 2: List all dependencies of Step 1
  • Step 3: Segregate dependencies found in Step 2 into:
    • a) Things provided by you
      • .env
      • package.json
      • Source code
    • b) Things NOT provided by you
      • node
      • npm
  • Step 4: Find a good base image based on Step 3b
  • Step 5: Copy all of Step 3a into a directory

fitnesse.org Example

  • Step 1: Identify your PID 1 -> java -jar fitnesse-standalone.jar
  • Step 2: List all dependencies of Step 1 -> Java, JAR file
  • Step 3: Segregate dependencies found in Step 2 into:
    • a) Things provided by you
      • fitnesse-standalone.jar
    • b) Things NOT provided by you
      • Java
  • Step 4: Find a good base image based on Step 3b -> eclipse-temurin:8-jre-alpine
  • Step 5: Copy all of Step 3a into a directory