[{"data":1,"prerenderedAt":497},["ShallowReactive",2],{"mdc-muurh4-key":3,"mdc-9ojohf-key":38,"mdc--qsnyrg-key":92,"mdc--azlka1-key":122,"mdc--e6pg04-key":158,"mdc--69s0na-key":182,"mdc--g2c535-key":195,"mdc--bq9yyy-key":207,"mdc--2eyzu3-key":310,"mdc-9ui0kt-key":320,"mdc-mdrylf-key":448,"mdc-dtyxsf-key":480},{"data":4,"body":5},{},{"type":6,"children":7},"root",[8,25],{"type":9,"tag":10,"props":11,"children":12},"element","p",{},[13,16,23],{"type":14,"value":15},"text","A base image is eight megabytes. Run fifty containers from it and you do not want fifty copies — and you certainly do not want one container's ",{"type":9,"tag":17,"props":18,"children":20},"code",{"className":19},[],[21],{"type":14,"value":22},"rm -rf /",{"type":14,"value":24}," to damage the image the other forty-nine are using.",{"type":9,"tag":10,"props":26,"children":27},{},[28,30,36],{"type":14,"value":29},"The answer is ",{"type":9,"tag":31,"props":32,"children":33},"strong",{},[34],{"type":14,"value":35},"copy-on-write",{"type":14,"value":37},": every container gets what looks like its own private copy, but blocks are only duplicated at the moment something writes to them. Creating one is instant and costs nothing until it is used.",{"data":39,"body":40},{},{"type":6,"children":41},[42,49,54,66],{"type":9,"tag":43,"props":44,"children":46},"h2",{"id":45},"set-up-a-btrfs-filesystem",[47],{"type":14,"value":48},"Set up a btrfs filesystem",{"type":9,"tag":10,"props":50,"children":51},{},[52],{"type":14,"value":53},"btrfs has snapshots built in. You don't need a spare disk — a file will do:",{"type":9,"tag":55,"props":56,"children":60},"pre",{"className":57,"code":59,"language":14},[58],"language-text","sudo apt install -y btrfs-progs\n\n# A sparse 5G file, formatted as a btrfs filesystem\ntruncate -s 5G ./btrfs-disk.img\nmkfs.btrfs -f ./btrfs-disk.img\n\nmkdir -p ./btrfs-mount\nsudo mount -o loop ./btrfs-disk.img ./btrfs-mount\n",[61],{"type":9,"tag":17,"props":62,"children":64},{"__ignoreMap":63},"",[65],{"type":14,"value":59},{"type":9,"tag":10,"props":67,"children":68},{},[69,75,77,82,84,90],{"type":9,"tag":17,"props":70,"children":72},{"className":71},[],[73],{"type":14,"value":74},"truncate -s 5G",{"type":14,"value":76}," creates a ",{"type":9,"tag":31,"props":78,"children":79},{},[80],{"type":14,"value":81},"sparse",{"type":14,"value":83}," file: it reports as 5 GB but occupies almost no disk until written to. ",{"type":9,"tag":17,"props":85,"children":87},{"className":86},[],[88],{"type":14,"value":89},"mount -o loop",{"type":14,"value":91}," presents that file to the kernel as a block device, which is what lets a filesystem live inside a regular file.",{"data":93,"body":94},{},{"type":6,"children":95},[96,101],{"type":9,"tag":97,"props":98,"children":100},"terminal-teaser",{":lines":99},"[{\"cmd\":\"ls -lh btrfs-disk.img\",\"out\":\"-rw-r--r-- 1 you you 5.0G Aug 28 09:14 btrfs-disk.img\"},{\"cmd\":\"du -h btrfs-disk.img\",\"out\":\"3.8M\\tbtrfs-disk.img\"},{\"cmd\":\"df -h ./btrfs-mount\",\"out\":\"Filesystem      Size  Used Avail Use% Mounted on\\n/dev/loop0      5.0G  3.8M  4.3G   1% /home/you/btrfs-mount\"}]",[],{"type":9,"tag":10,"props":102,"children":103},{},[104,106,112,114,120],{"type":14,"value":105},"Five gigabytes according to ",{"type":9,"tag":17,"props":107,"children":109},{"className":108},[],[110],{"type":14,"value":111},"ls",{"type":14,"value":113},", under four megabytes on disk according to ",{"type":9,"tag":17,"props":115,"children":117},{"className":116},[],[118],{"type":14,"value":119},"du",{"type":14,"value":121},". That gap is the sparse file.",{"data":123,"body":124},{},{"type":6,"children":125},[126,132,144,153],{"type":9,"tag":43,"props":127,"children":129},{"id":128},"a-subvolume-for-the-base-image",[130],{"type":14,"value":131},"A subvolume for the base image",{"type":9,"tag":10,"props":133,"children":134},{},[135,137,142],{"type":14,"value":136},"A btrfs ",{"type":9,"tag":31,"props":138,"children":139},{},[140],{"type":14,"value":141},"subvolume",{"type":14,"value":143}," is an independently snapshottable tree inside the filesystem. Make one and unpack Alpine into it:",{"type":9,"tag":55,"props":145,"children":148},{"className":146,"code":147,"language":14},[58],"sudo btrfs subvolume create ./btrfs-mount/base-image\n\ncurl -o alpine.tar.gz \\\n  https://dl-cdn.alpinelinux.org/alpine/v3.19/releases/x86_64/alpine-minirootfs-3.19.1-x86_64.tar.gz\n\nsudo tar -xf alpine.tar.gz -C ./btrfs-mount/base-image\n",[149],{"type":9,"tag":17,"props":150,"children":151},{"__ignoreMap":63},[152],{"type":14,"value":147},{"type":9,"tag":10,"props":154,"children":155},{},[156],{"type":14,"value":157},"This is the immutable thing. Nothing will ever write to it again.",{"data":159,"body":160},{},{"type":6,"children":161},[162,168,177],{"type":9,"tag":43,"props":163,"children":165},{"id":164},"snapshot-it-per-container",[166],{"type":14,"value":167},"Snapshot it per container",{"type":9,"tag":55,"props":169,"children":172},{"className":170,"code":171,"language":14},[58],"CONTAINER_ID=\"my-container\"\n\nsudo btrfs subvolume snapshot \\\n  ./btrfs-mount/base-image \\\n  ./btrfs-mount/$CONTAINER_ID\n",[173],{"type":9,"tag":17,"props":174,"children":175},{"__ignoreMap":63},[176],{"type":14,"value":171},{"type":9,"tag":10,"props":178,"children":179},{},[180],{"type":14,"value":181},"That completes instantly and consumes essentially no space. The new subvolume references exactly the same blocks as the base image; only when the container modifies a file does btrfs allocate a fresh block for the changed data and repoint that one file. The base image is never touched.",{"data":183,"body":184},{},{"type":6,"children":185},[186,190],{"type":9,"tag":97,"props":187,"children":189},{":lines":188},"[{\"cmd\":\"sudo btrfs subvolume snapshot ./btrfs-mount/base-image ./btrfs-mount/c1\",\"out\":\"Create a snapshot of './btrfs-mount/base-image' in './btrfs-mount/c1'\"},{\"cmd\":\"sudo btrfs subvolume list ./btrfs-mount\",\"out\":\"ID 256 gen 9 top level 5 path base-image\\nID 257 gen 9 top level 5 path c1\"},{\"cmd\":\"sudo sh -c \\\"echo hello > ./btrfs-mount/c1/etc/marker\\\"\",\"out\":\"\"},{\"cmd\":\"ls ./btrfs-mount/base-image/etc/marker\",\"out\":\"ls: cannot access ... No such file or directory\"}]",[],{"type":9,"tag":10,"props":191,"children":192},{},[193],{"type":14,"value":194},"Written in the snapshot, absent from the base. That is the whole guarantee, and it is what makes it safe to hand fifty containers the \"same\" filesystem.",{"data":196,"body":197},{},{"type":6,"children":198},[199],{"type":9,"tag":200,"props":201,"children":206},"quiz",{":answer":202,":options":203,"explanation":204,"question":205},"0","[\"Essentially nothing — the snapshots share every block with the base\",\"20 GB, one full copy per snapshot\",\"2 GB, since only the first snapshot needs storing\"]","A snapshot records references, not data. Space is consumed only as blocks diverge, which is why starting a container is instant regardless of image size. It is also why \"disk full\" on a container host is usually about accumulated *writes*, not images.","You snapshot a 2 GB base image ten times. How much disk does that consume before any container writes anything?",[],{"data":208,"body":209},{},{"type":6,"children":210},[211,217,230,235],{"type":9,"tag":43,"props":212,"children":214},{"id":213},"this-is-what-an-image-layer-is",[215],{"type":14,"value":216},"This is what an image layer is",{"type":9,"tag":10,"props":218,"children":219},{},[220,222,228],{"type":14,"value":221},"Docker's layered images are the same idea, generalised. Each instruction in a Dockerfile produces a layer holding only what changed; a running container adds one final writable layer on top and every write lands there. ",{"type":9,"tag":17,"props":223,"children":225},{"className":224},[],[226],{"type":14,"value":227},"docker commit",{"type":14,"value":229}," freezes that writable layer into a new read-only one.",{"type":9,"tag":10,"props":231,"children":232},{},[233],{"type":14,"value":234},"Which explains several behaviours that otherwise look arbitrary:",{"type":9,"tag":236,"props":237,"children":238},"ul",{},[239,266,300],{"type":9,"tag":240,"props":241,"children":242},"li",{},[243,248,250,256,258,264],{"type":9,"tag":31,"props":244,"children":245},{},[246],{"type":14,"value":247},"Deleting a file doesn't shrink the image.",{"type":14,"value":249}," The upper layer records a deletion marker; the original bytes are still in the layer below. ",{"type":9,"tag":17,"props":251,"children":253},{"className":252},[],[254],{"type":14,"value":255},"RUN apt-get install ... && rm -rf /var/lib/apt/lists/*",{"type":14,"value":257}," in ",{"type":9,"tag":259,"props":260,"children":261},"em",{},[262],{"type":14,"value":263},"one",{"type":14,"value":265}," instruction works precisely because the removal happens before the layer is sealed.",{"type":9,"tag":240,"props":267,"children":268},{},[269,274,276,282,284,290,292,298],{"type":9,"tag":31,"props":270,"children":271},{},[272],{"type":14,"value":273},"Reordering a Dockerfile changes build time enormously.",{"type":14,"value":275}," A layer is cached until something above it changes, so ",{"type":9,"tag":17,"props":277,"children":279},{"className":278},[],[280],{"type":14,"value":281},"COPY package.json",{"type":14,"value":283}," before ",{"type":9,"tag":17,"props":285,"children":287},{"className":286},[],[288],{"type":14,"value":289},"COPY . .",{"type":14,"value":291}," keeps the ",{"type":9,"tag":17,"props":293,"children":295},{"className":294},[],[296],{"type":14,"value":297},"npm install",{"type":14,"value":299}," layer valid across source edits.",{"type":9,"tag":240,"props":301,"children":302},{},[303,308],{"type":9,"tag":31,"props":304,"children":305},{},[306],{"type":14,"value":307},"Container writes vanish on removal.",{"type":14,"value":309}," The writable layer is deleted with the container. Volumes exist to sit outside this stack entirely.",{"data":311,"body":312},{},{"type":6,"children":313},[314],{"type":9,"tag":200,"props":315,"children":319},{":answer":202,":options":316,"explanation":317,"question":318},"[\"It grows by roughly 200 MB — the files remain in the earlier layer\",\"It shrinks back, since the files are gone from the final filesystem\",\"It stays the same; deletions are applied retroactively\"]","Layers are immutable once written. The later layer records that the files are absent, but the bytes are still shipped and still pulled. Doing both in a single `RUN` is the fix, and multi-stage builds are the general answer.","A Dockerfile installs a 200 MB toolchain in one `RUN`, then deletes it in a later `RUN`. What happens to image size?",[],{"data":321,"body":322},{},{"type":6,"children":323},[324],{"type":9,"tag":325,"props":326,"children":328},"deep-dive",{"title":327},"overlayfs, which is what Docker actually uses",[329,341,362,371,418,437],{"type":9,"tag":10,"props":330,"children":331},{},[332,334,339],{"type":14,"value":333},"btrfs is one of several storage drivers Docker supports, and it is used here because snapshots are a single obvious command. The default on nearly every modern installation is ",{"type":9,"tag":31,"props":335,"children":336},{},[337],{"type":14,"value":338},"overlayfs",{"type":14,"value":340},", which reaches the same result differently.",{"type":9,"tag":10,"props":342,"children":343},{},[344,346,352,354,360],{"type":14,"value":345},"overlayfs stacks directories. Given a read-only ",{"type":9,"tag":17,"props":347,"children":349},{"className":348},[],[350],{"type":14,"value":351},"lower",{"type":14,"value":353}," and a writable ",{"type":9,"tag":17,"props":355,"children":357},{"className":356},[],[358],{"type":14,"value":359},"upper",{"type":14,"value":361},", it presents a merged view:",{"type":9,"tag":55,"props":363,"children":366},{"className":364,"code":365,"language":14},[58],"mount -t overlay overlay \\\n  -o lowerdir=./base,upperdir=./upper,workdir=./work \\\n  ./merged\n",[367],{"type":9,"tag":17,"props":368,"children":369},{"__ignoreMap":63},[370],{"type":14,"value":365},{"type":9,"tag":10,"props":372,"children":373},{},[374,376,381,383,388,390,395,397,402,404,409,411,416],{"type":14,"value":375},"Reads come from ",{"type":9,"tag":17,"props":377,"children":379},{"className":378},[],[380],{"type":14,"value":359},{"type":14,"value":382}," if the file is there and ",{"type":9,"tag":17,"props":384,"children":386},{"className":385},[],[387],{"type":14,"value":351},{"type":14,"value":389}," otherwise. Writes always go to ",{"type":9,"tag":17,"props":391,"children":393},{"className":392},[],[394],{"type":14,"value":359},{"type":14,"value":396}," — and writing to a file that exists only in ",{"type":9,"tag":17,"props":398,"children":400},{"className":399},[],[401],{"type":14,"value":351},{"type":14,"value":403}," triggers a ",{"type":9,"tag":31,"props":405,"children":406},{},[407],{"type":14,"value":408},"copy-up",{"type":14,"value":410},": the whole file is copied into ",{"type":9,"tag":17,"props":412,"children":414},{"className":413},[],[415],{"type":14,"value":359},{"type":14,"value":417}," first, then modified.",{"type":9,"tag":10,"props":419,"children":420},{},[421,423,428,430,435],{"type":14,"value":422},"Two consequences that show up in production. The first write to a large file in a lower layer is slow, because it copies the entire file regardless of how many bytes you changed — which is why database data directories belong on volumes, not in the container filesystem. And deleting a lower-layer file creates a ",{"type":9,"tag":259,"props":424,"children":425},{},[426],{"type":14,"value":427},"whiteout",{"type":14,"value":429},", a character device with major/minor 0/0 in ",{"type":9,"tag":17,"props":431,"children":433},{"className":432},[],[434],{"type":14,"value":359},{"type":14,"value":436},", which is the mechanism behind deletions not reclaiming space.",{"type":9,"tag":10,"props":438,"children":439},{},[440,446],{"type":9,"tag":17,"props":441,"children":443},{"className":442},[],[444],{"type":14,"value":445},"lowerdir",{"type":14,"value":447}," accepts a colon-separated list, and that list is the image's layers.",{"data":449,"body":450},{},{"type":6,"children":451},[452,458,471],{"type":9,"tag":43,"props":453,"children":455},{"id":454},"tearing-it-down",[456],{"type":14,"value":457},"Tearing it down",{"type":9,"tag":10,"props":459,"children":460},{},[461,463,469],{"type":14,"value":462},"Snapshots are subvolumes, so they are removed with a btrfs command rather than ",{"type":9,"tag":17,"props":464,"children":466},{"className":465},[],[467],{"type":14,"value":468},"rm",{"type":14,"value":470},":",{"type":9,"tag":55,"props":472,"children":475},{"className":473,"code":474,"language":14},[58],"sudo btrfs subvolume delete ./btrfs-mount/$CONTAINER_ID\n",[476],{"type":9,"tag":17,"props":477,"children":478},{"__ignoreMap":63},[479],{"type":14,"value":474},{"data":481,"body":482},{},{"type":6,"children":483},[484,492],{"type":9,"tag":485,"props":486,"children":491},"fill-blank",{":answer":487,"hint":488,"placeholder":489,"prompt":490},"[\"sudo btrfs subvolume snapshot ./btrfs-mount/base-image ./btrfs-mount/c2\",\"btrfs subvolume snapshot ./btrfs-mount/base-image ./btrfs-mount/c2\"]","Three words of btrfs subcommand, then source and destination.","sudo btrfs ...","Take a copy-on-write snapshot of `./btrfs-mount/base-image` into `./btrfs-mount/c2`.",[],{"type":9,"tag":10,"props":493,"children":494},{},[495],{"type":14,"value":496},"Next up: networking — giving the container its own interface, its own IP, and a wire back to the host.",1787908868365]