[{"data":1,"prerenderedAt":1326},["ShallowReactive",2],{"page-\u002Fscaling-and-operating-production-python-apis\u002Fcontainerizing-python-apis-with-docker\u002Foptimizing-python-docker-image-size\u002F":3,"faq-schema-\u002Fscaling-and-operating-production-python-apis\u002Fcontainerizing-python-apis-with-docker\u002Foptimizing-python-docker-image-size\u002F":1304},{"id":4,"title":5,"body":6,"description":1294,"extension":1295,"meta":1296,"navigation":519,"path":1300,"seo":1301,"stem":1302,"__hash__":1303},"content\u002Fscaling-and-operating-production-python-apis\u002Fcontainerizing-python-apis-with-docker\u002Foptimizing-python-docker-image-size\u002Findex.md","Optimizing Python Docker Image Size for Faster, Cheaper Deploys",{"type":7,"value":8,"toc":1282},"minimark",[9,13,28,36,41,48,139,142,243,247,262,293,391,395,398,493,500,685,696,779,809,847,851,865,945,958,962,971,982,996,1000,1003,1052,1055,1059,1071,1123,1141,1145,1157,1161,1171,1177,1192,1215,1221,1225,1230,1254,1259,1278],[10,11,5],"h1",{"id":12},"optimizing-python-docker-image-size-for-faster-cheaper-deploys",[14,15,16,17,22,23,27],"p",{},"A Python API image that weighs 1.2GB isn't just untidy — it pulls slowly on every cold start, inflates your registry bill, and stretches every CI run. The fix is rarely one trick; it's a stack of decisions about base image, build strategy, and layer ordering, and each one is independently shippable. This page is part of the ",[18,19,21],"a",{"href":20},"\u002Fscaling-and-operating-production-python-apis\u002Fcontainerizing-python-apis-with-docker\u002F","Containerizing Python APIs with Docker"," guide, which sits inside the wider work of ",[18,24,26],{"href":25},"\u002Fscaling-and-operating-production-python-apis\u002F","scaling and operating production Python APIs",". It focuses on one question: how do you get a FastAPI image as small as it can reasonably be without breaking compatibility or wasting a weekend fighting musl libc?",[14,29,30,31,35],{},"The honest answer for most commercial APIs is short — ",[32,33,34],"code",{},"python:3.x-slim"," plus a multi-stage build gets you to roughly 180MB — but the reasoning behind that answer is where the money is. Get the base image or the layer ordering wrong and you pay for it on every deploy, forever.",[37,38,40],"h2",{"id":39},"the-base-image-decision","The base image decision",[14,42,43,44,47],{},"Most of your image weight is decided by the very first ",[32,45,46],{},"FROM"," line. Everything downstream — build speed, wheel compatibility, how easily you can debug a broken container at 2am — flows from that one choice. Here's how the common Python bases compare for a FastAPI service.",[49,50,51,70],"table",{},[52,53,54],"thead",{},[55,56,57,61,64,67],"tr",{},[58,59,60],"th",{},"Base image",[58,62,63],{},"Approx. size",[58,65,66],{},"Build speed",[58,68,69],{},"Compatibility gotchas",[71,72,73,90,105,124],"tbody",{},[55,74,75,81,84,87],{},[76,77,78],"td",{},[32,79,80],{},"python:3.11",[76,82,83],{},"~1 GB",[76,85,86],{},"Fast",[76,88,89],{},"Huge; ships compilers and headers you never run",[55,91,92,97,100,102],{},[76,93,94],{},[32,95,96],{},"python:3.11-slim",[76,98,99],{},"~180 MB",[76,101,86],{},[76,103,104],{},"Debian glibc; add build deps only in a builder stage",[55,106,107,112,115,121],{},[76,108,109],{},[32,110,111],{},"python:3.11-alpine",[76,113,114],{},"~120 MB",[76,116,117],{},[118,119,120],"strong",{},"Slow",[76,122,123],{},"musl libc: many wheels missing, so packages compile",[55,125,126,131,134,136],{},[76,127,128],{},[32,129,130],{},"gcr.io\u002Fdistroless\u002Fpython3",[76,132,133],{},"~90 MB",[76,135,86],{},[76,137,138],{},"No shell or package manager; harder to debug",[14,140,141],{},"The headline number is tempting, but build speed and compatibility are where alpine quietly costs you. The chart below shows the compressed image size for a typical FastAPI app with a database driver and Pydantic — the gap between full and slim is enormous, while the gap between slim, alpine, and distroless is small enough that other factors should decide it.",[143,144,152,153,152,157,152,161,152,168,152,176,152,179,152,187,152,194,152,200,152,204,152,208,152,212,152,216,152,220,152,224,152,228,152,233,152,237],"svg",{"viewBox":145,"role":146,"ariaLabelledBy":147,"xmlns":150,"style":151},"0 0 720 260","img",[148,149],"dockimg-size-t","dockimg-size-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;height:auto;margin:1.5rem 0;font-family:var(--font-sans);","\n  ",[154,155,156],"title",{"id":148},"Python Docker image size by base and strategy",[158,159,160],"desc",{"id":149},"Bar comparison: full python base around 1 gigabyte, slim multi-stage around 180 megabytes, alpine around 120 megabytes, distroless around 90 megabytes.",[162,163],"rect",{"x":164,"y":164,"width":165,"height":166,"fill":167},"0","720","260","var(--c-surface)",[169,170],"line",{"x1":171,"y1":172,"x2":171,"y2":173,"stroke":174,"style":175},"150","30","210","var(--c-border)","stroke-width:1;",[169,177],{"x1":171,"y1":173,"x2":178,"y2":173,"stroke":174,"style":175},"700",[180,181,186],"text",{"x":182,"y":183,"fill":184,"style":185},"140","56","var(--c-text)","text-anchor:end;font-size:12;font-family:var(--font-sans);","python (full)",[162,188],{"x":171,"y":189,"width":190,"height":191,"rx":192,"fill":193},"44","520","22","3","var(--c-coral)",[180,195,83],{"x":196,"y":197,"fill":198,"style":199},"678","60","var(--c-text-muted)","font-size:11;font-family:var(--font-sans);",[180,201,203],{"x":182,"y":202,"fill":184,"style":185},"100","slim (multi-stage)",[162,205],{"x":171,"y":206,"width":171,"height":191,"rx":192,"fill":207},"88","var(--c-blue)",[180,209,99],{"x":210,"y":211,"fill":198,"style":199},"308","104",[180,213,215],{"x":182,"y":214,"fill":184,"style":185},"144","alpine",[162,217],{"x":171,"y":218,"width":202,"height":191,"rx":192,"fill":219},"132","var(--c-yellow)",[180,221,114],{"x":222,"y":223,"fill":198,"style":199},"258","148",[180,225,227],{"x":182,"y":226,"fill":184,"style":185},"188","distroless",[162,229],{"x":171,"y":230,"width":231,"height":191,"rx":192,"fill":232},"176","78","var(--c-teal)",[180,234,133],{"x":235,"y":236,"fill":198,"style":199},"236","192",[180,238,242],{"x":239,"y":240,"fill":198,"style":241},"425","234","text-anchor:middle;font-size:11;font-family:var(--font-sans);","compressed image size (approx., FastAPI + deps)",[37,244,246],{"id":245},"the-alpine-trap-musl-and-the-wheel-problem","The alpine trap: musl and the wheel problem",[14,248,249,250,253,254,257,258,261],{},"Alpine's small base is real, but it hides a compatibility cost that bites the moment your dependency tree includes a C extension — which, for a real API, it always does. PyPI ships prebuilt ",[32,251,252],{},"manylinux"," wheels that install in seconds on glibc systems like slim and the full base. Alpine uses musl libc, and most projects don't publish musl wheels, so pip falls back to compiling from source. That pulls in ",[32,255,256],{},"gcc",", ",[32,259,260],{},"musl-dev",", and a pile of headers, turning a 20-second install into a multi-minute one and partly erasing the size win you came for.",[14,263,264,265,269,270,274,275,257,278,257,281,284,285,288,289,292],{},"This is not a rare edge case. The ",[18,266,268],{"href":267},"\u002Fgetting-started-with-python-apis-for-builders\u002Fparsing-json-responses\u002Fvalidating-json-with-pydantic-v2\u002F","Pydantic v2 core"," is a compiled Rust extension, ",[18,271,273],{"href":272},"\u002Fscaling-and-operating-production-python-apis\u002Fasync-database-access-with-sqlalchemy\u002Fasyncpg-vs-psycopg3-for-fastapi\u002F","asyncpg"," is compiled C, and ",[32,276,277],{},"cryptography",[32,279,280],{},"numpy",[32,282,283],{},"orjson",", and ",[32,286,287],{},"uvloop"," all ship native code. Any one of them forces a source build on musl. The diagram below traces the same ",[32,290,291],{},"pip install asyncpg"," down both paths.",[143,294,152,299,152,302,152,305,152,308,152,313,152,319,152,324,152,327,152,333,152,335,152,339,152,342,152,346,152,350,152,354,152,357,152,360,152,363,152,366,152,369,152,371,152,374,152,377,152,379,152,381,152,384,152,387],{"viewBox":295,"role":146,"ariaLabelledBy":296,"xmlns":150,"style":151},"0 0 720 290",[297,298],"dockimg-wheel-t","dockimg-wheel-d",[154,300,301],{"id":297},"How pip resolves a C-extension package on glibc versus musl",[158,303,304],{"id":298},"On glibc slim a prebuilt manylinux wheel installs in about twenty seconds; on musl alpine no wheel exists so pip compiles from source in about five minutes and adds a toolchain.",[162,306],{"x":164,"y":164,"width":165,"height":307,"fill":167},"290",[180,309,291],{"x":310,"y":311,"fill":184,"style":312},"20","34","font-size:13;font-family:var(--font-sans);",[162,314],{"x":310,"y":197,"width":315,"height":316,"rx":317,"fill":318,"stroke":174,"style":175},"200","54","6","var(--c-surface-alt)",[180,320,96],{"x":321,"y":322,"fill":184,"style":323},"120","82","text-anchor:middle;font-size:12;font-family:var(--font-sans);",[180,325,326],{"x":321,"y":202,"fill":198,"style":241},"glibc",[169,328],{"x1":329,"y1":330,"x2":331,"y2":330,"stroke":232,"style":332},"220","87","250","stroke-width:2;",[162,334],{"x":331,"y":197,"width":315,"height":316,"rx":317,"fill":318,"stroke":174,"style":175},[180,336,338],{"x":337,"y":322,"fill":184,"style":323},"350","manylinux wheel",[180,340,341],{"x":337,"y":202,"fill":198,"style":241},"found on PyPI",[169,343],{"x1":344,"y1":330,"x2":345,"y2":330,"stroke":232,"style":332},"450","480",[162,347],{"x":345,"y":197,"width":173,"height":316,"rx":317,"fill":232,"fillOpacity":348,"stroke":232,"style":349},0.22,"stroke-width:1.5;",[180,351,353],{"x":352,"y":322,"fill":184,"style":323},"585","installed, ~20 s",[180,355,356],{"x":352,"y":202,"fill":184,"style":241},"no compiler needed",[162,358],{"x":310,"y":359,"width":315,"height":316,"rx":317,"fill":318,"stroke":174,"style":175},"180",[180,361,111],{"x":321,"y":362,"fill":184,"style":323},"202",[180,364,365],{"x":321,"y":329,"fill":198,"style":241},"musl",[169,367],{"x1":329,"y1":368,"x2":331,"y2":368,"stroke":193,"style":332},"207",[162,370],{"x":331,"y":359,"width":315,"height":316,"rx":317,"fill":318,"stroke":174,"style":175},[180,372,373],{"x":337,"y":362,"fill":184,"style":323},"no musl wheel",[180,375,376],{"x":337,"y":329,"fill":198,"style":241},"compile from source",[169,378],{"x1":344,"y1":368,"x2":345,"y2":368,"stroke":193,"style":332},[162,380],{"x":345,"y":359,"width":173,"height":316,"rx":317,"fill":193,"fillOpacity":348,"stroke":193,"style":349},[180,382,383],{"x":352,"y":362,"fill":184,"style":323},"+gcc toolchain, ~5 min",[180,385,386],{"x":352,"y":329,"fill":184,"style":241},"size win eroded",[180,388,390],{"x":310,"y":389,"fill":198,"style":199},"268","One compiled dependency is enough to send the whole build down the slow path.",[37,392,394],{"id":393},"before-and-after-a-runnable-multi-stage-build","Before and after: a runnable multi-stage build",[14,396,397],{},"Start with the naive Dockerfile most people write first:",[399,400,405],"pre",{"className":401,"code":402,"language":403,"meta":404,"style":404},"language-dockerfile shiki shiki-themes github-light github-dark","# BEFORE — ~1.1 GB\nFROM python:3.11\nWORKDIR \u002Fapp\nCOPY . .\nRUN pip install -r requirements.txt\nCMD [\"gunicorn\", \"app.main:app\", \"-k\", \"uvicorn.workers.UvicornWorker\", \"-b\", \"0.0.0.0:8000\"]\n","dockerfile","",[32,406,407,415,425,434,443,452],{"__ignoreMap":404},[408,409,411],"span",{"class":169,"line":410},1,[408,412,414],{"class":413},"sJ8bj","# BEFORE — ~1.1 GB\n",[408,416,418,421],{"class":169,"line":417},2,[408,419,46],{"class":420},"szBVR",[408,422,424],{"class":423},"sVt8B"," python:3.11\n",[408,426,428,431],{"class":169,"line":427},3,[408,429,430],{"class":420},"WORKDIR",[408,432,433],{"class":423}," \u002Fapp\n",[408,435,437,440],{"class":169,"line":436},4,[408,438,439],{"class":420},"COPY",[408,441,442],{"class":423}," . .\n",[408,444,446,449],{"class":169,"line":445},5,[408,447,448],{"class":420},"RUN",[408,450,451],{"class":423}," pip install -r requirements.txt\n",[408,453,455,458,461,465,467,470,472,475,477,480,482,485,487,490],{"class":169,"line":454},6,[408,456,457],{"class":420},"CMD",[408,459,460],{"class":423}," [",[408,462,464],{"class":463},"sZZnC","\"gunicorn\"",[408,466,257],{"class":423},[408,468,469],{"class":463},"\"app.main:app\"",[408,471,257],{"class":423},[408,473,474],{"class":463},"\"-k\"",[408,476,257],{"class":423},[408,478,479],{"class":463},"\"uvicorn.workers.UvicornWorker\"",[408,481,257],{"class":423},[408,483,484],{"class":463},"\"-b\"",[408,486,257],{"class":423},[408,488,489],{"class":463},"\"0.0.0.0:8000\"",[408,491,492],{"class":423},"]\n",[14,494,495,496,499],{},"It works, but it ships the full Debian toolchain, pip's download cache, the entire build context including ",[32,497,498],{},".git",", and uncompiled source. Now the optimized version, which is the one you should actually ship:",[399,501,503],{"className":401,"code":502,"language":403,"meta":404,"style":404},"# AFTER — ~180 MB\n# syntax=docker\u002Fdockerfile:1\n\nFROM python:3.11-slim AS builder\nENV PIP_NO_CACHE_DIR=1 PIP_DISABLE_PIP_VERSION_CHECK=1\nWORKDIR \u002Fbuild\nCOPY requirements.txt .\n# Install into an isolated prefix we can copy out cleanly.\nRUN --mount=type=cache,target=\u002Froot\u002F.cache\u002Fpip \\\n    pip install --prefix=\u002Finstall -r requirements.txt\n\nFROM python:3.11-slim AS runtime\nRUN useradd --system --create-home app\nCOPY --from=builder \u002Finstall \u002Fusr\u002Flocal\nWORKDIR \u002Fapp\nCOPY --chown=app:app .\u002Fapp .\u002Fapp\nUSER app\nENV PYTHONUNBUFFERED=1\nEXPOSE 8000\nCMD [\"sh\", \"-c\", \"exec gunicorn app.main:app \\\n    -k uvicorn.workers.UvicornWorker \\\n    -w ${WEB_CONCURRENCY:-4} -b 0.0.0.0:${PORT:-8000}\"]\n",[32,504,505,510,515,521,534,542,549,557,563,571,577,582,594,602,610,617,625,634,642,651,671,677],{"__ignoreMap":404},[408,506,507],{"class":169,"line":410},[408,508,509],{"class":413},"# AFTER — ~180 MB\n",[408,511,512],{"class":169,"line":417},[408,513,514],{"class":413},"# syntax=docker\u002Fdockerfile:1\n",[408,516,517],{"class":169,"line":427},[408,518,520],{"emptyLinePlaceholder":519},true,"\n",[408,522,523,525,528,531],{"class":169,"line":436},[408,524,46],{"class":420},[408,526,527],{"class":423}," python:3.11-slim ",[408,529,530],{"class":420},"AS",[408,532,533],{"class":423}," builder\n",[408,535,536,539],{"class":169,"line":445},[408,537,538],{"class":420},"ENV",[408,540,541],{"class":423}," PIP_NO_CACHE_DIR=1 PIP_DISABLE_PIP_VERSION_CHECK=1\n",[408,543,544,546],{"class":169,"line":454},[408,545,430],{"class":420},[408,547,548],{"class":423}," \u002Fbuild\n",[408,550,552,554],{"class":169,"line":551},7,[408,553,439],{"class":420},[408,555,556],{"class":423}," requirements.txt .\n",[408,558,560],{"class":169,"line":559},8,[408,561,562],{"class":413},"# Install into an isolated prefix we can copy out cleanly.\n",[408,564,566,568],{"class":169,"line":565},9,[408,567,448],{"class":420},[408,569,570],{"class":423}," --mount=type=cache,target=\u002Froot\u002F.cache\u002Fpip \\\n",[408,572,574],{"class":169,"line":573},10,[408,575,576],{"class":423},"    pip install --prefix=\u002Finstall -r requirements.txt\n",[408,578,580],{"class":169,"line":579},11,[408,581,520],{"emptyLinePlaceholder":519},[408,583,585,587,589,591],{"class":169,"line":584},12,[408,586,46],{"class":420},[408,588,527],{"class":423},[408,590,530],{"class":420},[408,592,593],{"class":423}," runtime\n",[408,595,597,599],{"class":169,"line":596},13,[408,598,448],{"class":420},[408,600,601],{"class":423}," useradd --system --create-home app\n",[408,603,605,607],{"class":169,"line":604},14,[408,606,439],{"class":420},[408,608,609],{"class":423}," --from=builder \u002Finstall \u002Fusr\u002Flocal\n",[408,611,613,615],{"class":169,"line":612},15,[408,614,430],{"class":420},[408,616,433],{"class":423},[408,618,620,622],{"class":169,"line":619},16,[408,621,439],{"class":420},[408,623,624],{"class":423}," --chown=app:app .\u002Fapp .\u002Fapp\n",[408,626,628,631],{"class":169,"line":627},17,[408,629,630],{"class":420},"USER",[408,632,633],{"class":423}," app\n",[408,635,637,639],{"class":169,"line":636},18,[408,638,538],{"class":420},[408,640,641],{"class":423}," PYTHONUNBUFFERED=1\n",[408,643,645,648],{"class":169,"line":644},19,[408,646,647],{"class":420},"EXPOSE",[408,649,650],{"class":423}," 8000\n",[408,652,654,656,658,661,663,666,668],{"class":169,"line":653},20,[408,655,457],{"class":420},[408,657,460],{"class":423},[408,659,660],{"class":463},"\"sh\"",[408,662,257],{"class":423},[408,664,665],{"class":463},"\"-c\"",[408,667,257],{"class":423},[408,669,670],{"class":463},"\"exec gunicorn app.main:app \\\n",[408,672,674],{"class":169,"line":673},21,[408,675,676],{"class":463},"    -k uvicorn.workers.UvicornWorker \\\n",[408,678,680,683],{"class":169,"line":679},22,[408,681,682],{"class":463},"    -w ${WEB_CONCURRENCY:-4} -b 0.0.0.0:${PORT:-8000}\"",[408,684,492],{"class":423},[14,686,687,688,691,692,695],{},"The trick that moves the number most is the two-stage split. The builder stage does all the heavy work — pulling wheels, running any source compiles — and the runtime stage copies only the finished ",[32,689,690],{},"\u002Finstall"," prefix. No compiler, no ",[32,693,694],{},"build-essential",", no pip cache ever reaches the final image. The diagram shows what crosses the boundary and what stays behind.",[143,697,152,702,152,705,152,708,152,711,152,716,152,721,152,726,152,730,152,734,152,738,152,741,152,744,152,748,152,751,152,754,152,757,152,760,152,763,152,767,152,771,152,775],{"viewBox":698,"role":146,"ariaLabelledBy":699,"xmlns":150,"style":151},"0 0 720 300",[700,701],"dockimg-stage-t","dockimg-stage-d",[154,703,704],{"id":700},"Multi-stage build data flow",[158,706,707],{"id":701},"The builder stage installs packages with compilers and a cache; only the installed prefix is copied into the runtime stage, so the toolchain never ships.",[162,709],{"x":164,"y":164,"width":165,"height":710,"fill":167},"300",[162,712],{"x":172,"y":713,"width":307,"height":714,"rx":715,"fill":318,"stroke":174,"style":175},"55","185","8",[180,717,720],{"x":718,"y":322,"fill":207,"style":719},"45","font-size:13;font-family:var(--font-sans);font-weight:600;","builder stage",[180,722,725],{"x":718,"y":723,"fill":184,"style":724},"112","font-size:12;font-family:var(--font-sans);","COPY requirements.txt",[180,727,729],{"x":718,"y":728,"fill":184,"style":724},"138","pip install --prefix=\u002Finstall",[180,731,733],{"x":718,"y":732,"fill":198,"style":724},"164","compilers + headers (heavy)",[180,735,737],{"x":718,"y":736,"fill":198,"style":724},"190","BuildKit wheel cache",[180,739,740],{"x":718,"y":329,"fill":193,"style":199},"stays in this stage — discarded",[162,742],{"x":743,"y":713,"width":307,"height":714,"rx":715,"fill":318,"stroke":232,"style":332},"400",[180,745,747],{"x":746,"y":322,"fill":232,"style":719},"415","runtime stage (final image)",[180,749,750],{"x":746,"y":723,"fill":184,"style":724},"COPY --from=builder \u002Finstall",[180,752,753],{"x":746,"y":728,"fill":184,"style":724},"app source only",[180,755,756],{"x":746,"y":732,"fill":184,"style":724},"runs as non-root user",[180,758,759],{"x":746,"y":736,"fill":184,"style":724},"no compilers",[180,761,762],{"x":746,"y":329,"fill":232,"style":199},"ships at ~180 MB",[169,764],{"x1":765,"y1":766,"x2":743,"y2":766,"stroke":232,"style":332},"320","147",[768,769],"path",{"d":770,"fill":232},"M 393 142 L 400 147 L 393 152 Z",[180,772,690],{"x":773,"y":728,"fill":198,"style":774},"360","text-anchor:middle;font-size:10;font-family:var(--font-sans);",[180,776,778],{"x":172,"y":777,"fill":198,"style":199},"278","The toolchain never reaches the shipped image — only the built packages cross the line.",[14,780,781,782,785,786,257,788,791,792,795,796,799,800,803,804,808],{},"A tight ",[32,783,784],{},".dockerignore"," keeps ",[32,787,498],{},[32,789,790],{},".venv",", and test fixtures out of the build context, and the ",[32,793,794],{},"--mount=type=cache"," cache mount reuses downloaded wheels across builds so repeat CI runs stay fast. The ",[32,797,798],{},"WEB_CONCURRENCY"," and ",[32,801,802],{},"PORT"," values read from the environment rather than being baked in — matching the worker count to your vCPUs is its own decision, covered in ",[18,805,807],{"href":806},"\u002Fgetting-started-with-python-apis-for-builders\u002Fsetting-up-fastapi\u002Fuvicorn-vs-gunicorn-worker-configuration\u002F","Uvicorn vs Gunicorn worker configuration",". Measure the result:",[399,810,814],{"className":811,"code":812,"language":813,"meta":404,"style":404},"language-bash shiki shiki-themes github-light github-dark","docker build -t api:optimized .\ndocker images api:optimized   # confirm it lands near ~180MB\n","bash",[32,815,816,835],{"__ignoreMap":404},[408,817,818,822,825,829,832],{"class":169,"line":410},[408,819,821],{"class":820},"sScJk","docker",[408,823,824],{"class":463}," build",[408,826,828],{"class":827},"sj4cs"," -t",[408,830,831],{"class":463}," api:optimized",[408,833,834],{"class":463}," .\n",[408,836,837,839,842,844],{"class":169,"line":417},[408,838,821],{"class":820},[408,840,841],{"class":463}," images",[408,843,831],{"class":463},[408,845,846],{"class":413},"   # confirm it lands near ~180MB\n",[37,848,850],{"id":849},"where-the-megabytes-actually-go","Where the megabytes actually go",[14,852,853,854,857,858,861,862,864],{},"To optimize with intent rather than superstition, you need to know what's inside that 1.1GB. Run ",[32,855,856],{},"docker history --no-trunc your-image:tag"," or a tool like ",[32,859,860],{},"dive"," and the same four culprits show up almost every time: the fat base OS, the build toolchain, pip's download cache, and the source context you accidentally copied in. The optimized image keeps only the base and the installed packages. The comparison below is drawn from a real FastAPI service with ",[32,863,273],{}," and Pydantic.",[143,866,152,871,152,874,152,877,152,880,152,884,152,888,152,891,152,895,152,898,152,901,152,904,152,907,152,911,152,913,152,916,152,921,152,923,152,926,152,929,152,933,152,936,152,940,152,942],{"viewBox":867,"role":146,"ariaLabelledBy":868,"xmlns":150,"style":151},"0 0 720 270",[869,870],"dockimg-layers-t","dockimg-layers-d",[154,872,873],{"id":869},"Composition of a fat image versus an optimized image",[158,875,876],{"id":870},"The 1.1 gigabyte image is dominated by the base OS, build toolchain, pip cache and copied source; the 180 megabyte image is just the slim base plus site-packages and app code.",[162,878],{"x":164,"y":164,"width":165,"height":879,"fill":167},"270",[180,881,883],{"x":310,"y":882,"fill":184,"style":724},"58","1.1 GB before",[162,885],{"x":171,"y":886,"width":887,"height":311,"fill":207},"46","167",[162,889],{"x":890,"y":886,"width":766,"height":311,"fill":193},"317",[162,892],{"x":893,"y":886,"width":894,"height":311,"fill":219},"464","59",[162,896],{"x":897,"y":886,"width":206,"height":311,"fill":232},"523",[162,899],{"x":900,"y":886,"width":231,"height":311,"fill":198},"611",[180,902,903],{"x":310,"y":728,"fill":184,"style":724},"180 MB after",[162,905],{"x":171,"y":906,"width":894,"height":311,"fill":207},"126",[162,908],{"x":909,"y":906,"width":910,"height":311,"fill":232},"209","27",[162,912],{"x":235,"y":906,"width":192,"height":311,"fill":198},[162,914],{"x":310,"y":315,"width":915,"height":915,"fill":207},"14",[180,917,920],{"x":918,"y":919,"fill":184,"style":199},"42","212","base OS + Python",[162,922],{"x":359,"y":315,"width":915,"height":915,"fill":193},[180,924,925],{"x":362,"y":919,"fill":184,"style":199},"build toolchain",[162,927],{"x":928,"y":315,"width":915,"height":915,"fill":219},"340",[180,930,932],{"x":931,"y":919,"fill":184,"style":199},"362","pip cache",[162,934],{"x":310,"y":935,"width":915,"height":915,"fill":232},"228",[180,937,939],{"x":918,"y":938,"fill":184,"style":199},"240","site-packages",[162,941],{"x":359,"y":935,"width":915,"height":915,"fill":198},[180,943,944],{"x":362,"y":938,"fill":184,"style":199},"copied source + .git",[14,946,947,948,951,952,954,955,957],{},"The lesson is that two segments — the toolchain and the pip cache — are pure waste in production, and multi-stage plus ",[32,949,950],{},"--no-cache-dir"," deletes both. The base shrinks from full to slim, and the source segment collapses once ",[32,953,784],{}," stops you shipping ",[32,956,498],{},". What's left is the irreducible core: the interpreter and your actual dependencies.",[37,959,961],{"id":960},"when-to-choose-each-base","When to choose each base",[14,963,964,970],{},[118,965,966,967,969],{},"Use ",[32,968,96],{}," (the default)."," For almost every FastAPI API, slim plus multi-stage is the right answer. You get glibc wheel compatibility, fast builds, easy debugging with a real shell, and a final image small enough that the next 100MB of savings isn't worth an engineer's afternoon.",[14,972,973,976,977,981],{},[118,974,975],{},"Reach for alpine only when"," image size is a hard constraint ",[978,979,980],"em",{},"and"," your dependency tree is pure-Python or has musl wheels available. Verify every compiled dependency before committing — otherwise you trade size for slow, fragile builds. Many teams that start on alpine migrate back to slim once a new dependency forces a source compile.",[14,983,984,987,988,991,992,995],{},[118,985,986],{},"Use distroless when"," you want the smallest attack surface for a hardened deploy and you've already nailed your build. Distroless has no shell and no package manager, so you debug with a ",[32,989,990],{},":debug"," variant or ",[32,993,994],{},"docker cp",". Pair it with a slim builder that produces the prefix, then copy that into the distroless runtime. It's an excellent end state, not a starting point.",[37,997,999],{"id":998},"migration-path-from-a-fat-image","Migration path from a fat image",[14,1001,1002],{},"You don't have to do this all at once. A safe order that keeps every step independently shippable and reversible:",[1004,1005,1006,1015,1027,1033,1039],"ol",{},[1007,1008,1009,1014],"li",{},[118,1010,1011,1012],{},"Add ",[32,1013,784],{}," — instant context shrink, zero risk.",[1007,1016,1017,1023,1024,1026],{},[118,1018,1019,1020],{},"Switch the base to ",[32,1021,1022],{},"-slim",", adding ",[32,1025,694],{}," only in a builder stage if anything compiles.",[1007,1028,1029,1032],{},[118,1030,1031],{},"Split into multi-stage",", copying the installed prefix into a clean runtime.",[1007,1034,1035,1038],{},[118,1036,1037],{},"Add a BuildKit cache mount"," to claw back build speed on repeat builds.",[1007,1040,1041,1044,1045,799,1048,1051],{},[118,1042,1043],{},"Measure"," with ",[32,1046,1047],{},"docker images",[32,1049,1050],{},"docker history",", then consider alpine or distroless only if you still need more.",[14,1053,1054],{},"Because each step stands alone, you can stop the moment the size is good enough — and for most builders that's after step three.",[37,1056,1058],{"id":1057},"what-image-size-costs-you-in-production","What image size costs you in production",[14,1060,1061,1062,1065,1066,1070],{},"The runtime CPU and memory of your container don't change with image size, but the ",[978,1063,1064],{},"pull"," time does, and on autoscaling or serverless platforms the image is pulled and extracted before the first request can be served. A 1.1GB image can add ten seconds of cold-start latency that your customer feels as a timeout; a 180MB image cuts that to a couple of seconds. If you run on a platform where cold starts are frequent — see ",[18,1067,1069],{"href":1068},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdeploying-apis-to-render-or-vercel\u002Frender-vs-railway-vs-fly-io\u002F","Render vs Railway vs Fly.io"," for how each behaves — a lean image is the difference between graceful autoscale and a queue of stalled requests.",[143,1072,152,1077,152,1080,152,1083,152,1085,152,1089,152,1094,152,1099,152,1102,152,1104,152,1108,152,1111,152,1115,152,1120],{"viewBox":1073,"role":146,"ariaLabelledBy":1074,"xmlns":150,"style":151},"0 0 720 210",[1075,1076],"dockimg-cold-t","dockimg-cold-d",[154,1078,1079],{"id":1075},"Cold-start image pull time by image size",[158,1081,1082],{"id":1076},"Approximate pull and extract time on a cold node at about 100 megabytes per second: 1.1 gigabytes takes about 11 seconds, 180 megabytes about 2 seconds, 90 megabytes about 1 second.",[162,1084],{"x":164,"y":164,"width":165,"height":173,"fill":167},[180,1086,1088],{"x":182,"y":1087,"fill":184,"style":185},"52","1.1 GB",[162,1090],{"x":171,"y":1091,"width":1092,"height":1093,"rx":192,"fill":193},"40","500","24",[180,1095,1098],{"x":1096,"y":1097,"fill":198,"style":199},"658","57","~11 s",[180,1100,1101],{"x":182,"y":202,"fill":184,"style":185},"180 MB",[162,1103],{"x":171,"y":206,"width":202,"height":1093,"rx":192,"fill":207},[180,1105,1107],{"x":222,"y":1106,"fill":198,"style":199},"105","~2 s",[180,1109,1110],{"x":182,"y":223,"fill":184,"style":185},"90 MB",[162,1112],{"x":171,"y":1113,"width":1114,"height":1093,"rx":192,"fill":232},"136","50",[180,1116,1119],{"x":1117,"y":1118,"fill":198,"style":199},"208","153","~1 s",[180,1121,1122],{"x":171,"y":226,"fill":198,"style":199},"approx pull + extract on a cold node (~100 MB\u002Fs)",[14,1124,1125,1126,1130,1131,1135,1136,1140],{},"There's a registry cost too. Storing and transferring gigabyte images across dozens of deploys a day adds up on managed registries, and every CI pipeline pushes and pulls the full thing. Fold that into your unit economics the same way you would any fixed overhead — the method is in ",[18,1127,1129],{"href":1128},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdesigning-api-pricing-tiers\u002Fcalculating-cost-per-api-request\u002F","calculating cost per API request",", and the margin it protects is what the ",[18,1132,1134],{"href":1133},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdesigning-api-pricing-tiers\u002F","API pricing tiers"," are built on. A lean image also makes ",[18,1137,1139],{"href":1138},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdeploying-apis-to-render-or-vercel\u002Fzero-downtime-deploys-for-python-apis\u002F","zero-downtime deploys"," faster, because new replicas come up before the old ones drain.",[37,1142,1144],{"id":1143},"builder-verdict","Builder verdict",[14,1146,1147,1148,1156],{},"For the overwhelming majority of commercial Python APIs, the answer is ",[118,1149,1150,1152,1153,1155],{},[32,1151,34],{}," with a multi-stage build, a tight ",[32,1154,784],{},", and a BuildKit cache mount."," It gets you to roughly 150–200MB — small enough that cold starts and registry costs stop mattering — while keeping builds fast and debugging sane. Alpine's smaller number is a trap the moment a compiled dependency forces a source build, and distroless is worth it only after you've optimized and want to harden. Optimize for total operating cost — build minutes, cold-start latency, and the engineering hours you'd otherwise burn on musl edge cases — not for the smallest possible megabyte count.",[37,1158,1160],{"id":1159},"faq","FAQ",[14,1162,1163,1166,1167,1170],{},[118,1164,1165],{},"Is alpine always smaller than slim?","\nThe base layer is smaller, but the final image often isn't. If pip has to compile C extensions from source on musl, it pulls in ",[32,1168,1169],{},"build-base"," and friends, and the toolchain plus compiled artifacts can erase the saving. For a FastAPI app with Pydantic and a database driver, a slim multi-stage image is frequently competitive with — and far faster to build than — alpine.",[14,1172,1173,1176],{},[118,1174,1175],{},"How much does a fat image actually cost me a month?","\nIt's mostly indirect: longer cold starts that hurt conversion, slower CI so you ship less often, and registry storage plus egress on every deploy. At a few deploys a day a gigabyte image can push tens of dollars of transfer and minutes of pipeline time per week. Dropping to 180MB roughly sixes that overhead and shortens cold starts from ten seconds to two — a direct win for both margin and customer experience.",[14,1178,1179,1182,1183,1185,1186,1188,1189,1191],{},[118,1180,1181],{},"How do I find what's taking up space in my image?","\nRun ",[32,1184,856],{}," to see per-layer sizes, or use ",[32,1187,860],{},". The usual culprits are the base image choice, leftover build dependencies, and pip's cache. Each has a clean fix: slim base, multi-stage copy, and ",[32,1190,950],{}," or a cache mount.",[14,1193,1194,1200,1201,1203,1204,1207,1208,1210,1211,1214],{},[118,1195,1196,1197,1199],{},"Should I use ",[32,1198,950],{}," or a BuildKit cache mount?","\nBoth, for different reasons. ",[32,1202,950],{}," (or copying out of an isolated prefix) keeps pip's download cache ",[978,1205,1206],{},"out of the final image"," so it stays small. A ",[32,1209,794],{}," keeps that cache ",[978,1212,1213],{},"on the build host"," between builds so rebuilds are fast. They're complementary, not alternatives.",[14,1216,1217,1220],{},[118,1218,1219],{},"Is it worth migrating an existing service to distroless?","\nOnly if you've already done multi-stage on slim and still want a smaller attack surface for compliance or hardening. The migration risk is real — no shell means your existing debug and health-check tooling may break — so treat it as a deliberate hardening step, not a routine size optimization. Most teams get 90% of the benefit from slim and never need it.",[37,1222,1224],{"id":1223},"related","Related",[14,1226,1227],{},[118,1228,1229],{},"Same track:",[1231,1232,1233,1241,1248],"ul",{},[1007,1234,1235,1237,1238,1240],{},[18,1236,21],{"href":20}," — the parent guide covering ",[32,1239,784],{},", healthchecks, and the full build.",[1007,1242,1243,1247],{},[18,1244,1246],{"href":1245},"\u002Fscaling-and-operating-production-python-apis\u002Fcaching-python-api-responses-with-redis\u002F","Caching Python API responses with Redis"," — the next lever once your image and cold starts are lean.",[1007,1249,1250,1253],{},[18,1251,1252],{"href":272},"asyncpg vs psycopg3 for FastAPI"," — a compiled driver that decides whether alpine is viable for you.",[14,1255,1256],{},[118,1257,1258],{},"Other tracks:",[1231,1260,1261,1266,1272],{},[1007,1262,1263,1265],{},[18,1264,1069],{"href":1068}," — how cold-start behavior differs by host, and why image size matters there.",[1007,1267,1268,1271],{},[18,1269,1270],{"href":1138},"Zero-downtime deploys for Python APIs"," — a lean image makes rolling replicas come up faster.",[1007,1273,1274,1277],{},[18,1275,1276],{"href":1128},"Calculating cost per API request"," — fold registry and build overhead into your real unit economics.",[1279,1280,1281],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":404,"searchDepth":417,"depth":417,"links":1283},[1284,1285,1286,1287,1288,1289,1290,1291,1292,1293],{"id":39,"depth":417,"text":40},{"id":245,"depth":417,"text":246},{"id":393,"depth":417,"text":394},{"id":849,"depth":417,"text":850},{"id":960,"depth":417,"text":961},{"id":998,"depth":417,"text":999},{"id":1057,"depth":417,"text":1058},{"id":1143,"depth":417,"text":1144},{"id":1159,"depth":417,"text":1160},{"id":1223,"depth":417,"text":1224},"Shrink Python Docker images: slim vs alpine vs distroless, multi-stage builds, layer caching and cold-start cost, with runnable FastAPI Dockerfiles.","md",{"pageTitle":1297,"datePublished":1298,"dateModified":1299},"Optimizing Python Docker Image Size for Cheaper Deploys","2026-06-18","2026-07-23","\u002Fscaling-and-operating-production-python-apis\u002Fcontainerizing-python-apis-with-docker\u002Foptimizing-python-docker-image-size",{"title":5,"description":1294},"scaling-and-operating-production-python-apis\u002Fcontainerizing-python-apis-with-docker\u002Foptimizing-python-docker-image-size\u002Findex","z68NvKM-hsMZ4aiM47ewuohHwIz3YPcng94cnqa4Y6k",{"@context":1305,"@type":1306,"mainEntity":1307},"https:\u002F\u002Fschema.org","FAQPage",[1308,1313,1316,1319,1323],{"@type":1309,"name":1165,"acceptedAnswer":1310},"Question",{"@type":1311,"text":1312},"Answer","The base layer is smaller, but the final image often isn't. If pip has to compile C extensions from source on musl, it pulls in build-base and friends, and the toolchain plus compiled artifacts can erase the saving. For a FastAPI app with Pydantic and a database driver, a slim multi-stage image is frequently competitive with — and far faster to build than — alpine.",{"@type":1309,"name":1175,"acceptedAnswer":1314},{"@type":1311,"text":1315},"It's mostly indirect: longer cold starts that hurt conversion, slower CI so you ship less often, and registry storage plus egress on every deploy. At a few deploys a day a gigabyte image can push tens of dollars of transfer and minutes of pipeline time per week. Dropping to 180MB roughly sixes that overhead and shortens cold starts from ten seconds to two — a direct win for both margin and customer experience.",{"@type":1309,"name":1181,"acceptedAnswer":1317},{"@type":1311,"text":1318},"Run docker history --no-trunc your-image:tag to see per-layer sizes, or use dive. The usual culprits are the base image choice, leftover build dependencies, and pip's cache. Each has a clean fix: slim base, multi-stage copy, and --no-cache-dir or a cache mount.",{"@type":1309,"name":1320,"acceptedAnswer":1321},"Should I use --no-cache-dir or a BuildKit cache mount?",{"@type":1311,"text":1322},"Both, for different reasons. --no-cache-dir (or copying out of an isolated prefix) keeps pip's download cache out of the final image so it stays small. A --mount=type=cache keeps that cache on the build host between builds so rebuilds are fast. They're complementary, not alternatives.",{"@type":1309,"name":1219,"acceptedAnswer":1324},{"@type":1311,"text":1325},"Only if you've already done multi-stage on slim and still want a smaller attack surface for compliance or hardening. The migration risk is real — no shell means your existing debug and health-check tooling may break — so treat it as a deliberate hardening step, not a routine size optimization. Most teams get 90% of the benefit from slim and never need it.",1784887028586]