[{"data":1,"prerenderedAt":4185},["ShallowReactive",2],{"content:\u002F07-infrastructure\u002F02-dockerfile-compose":3},{"title":4,"description":5,"path":6,"body":7},"Dockerfile и Docker Compose","В предыдущей теме мы разобрали, что такое образы, контейнеры, слои и volumes. Теперь перейдём к практике: как описать сборку образа (Dockerfile), как оптимизировать его для Go (multi-stage build) и как поднять несколько сервисов одной командой (Docker Compose).","\u002F07-infrastructure\u002F02-dockerfile-compose",{"type":8,"value":9,"toc":4130},"minimark",[10,14,17,20,23,28,31,36,230,234,240,327,330,341,345,350,385,398,402,414,454,463,467,475,524,534,561,565,568,581,600,607,629,635,683,686,691,727,738,742,847,850,852,856,859,863,870,878,881,885,1051,1055,1060,1087,1092,1109,1112,1116,1127,1137,1150,1182,1192,1196,1211,1215,1274,1276,1280,1284,1287,1365,1368,1372,1383,1389,1399,1403,1406,1474,1478,1481,1485,1501,1505,1537,1541,1544,1715,1718,1738,1740,1744,1748,1754,1757,1767,1773,1783,1787,2072,2076,2079,2095,2099,2112,2115,2126,2239,2245,2248,2251,2366,2369,2371,2374,2467,2474,2478,2497,2500,2554,2556,2560,2564,2711,2734,2738,2822,2826,2863,2867,2924,2941,2945,2985,2987,2991,2994,3089,3190,3237,3239,3243,3247,3258,3262,3319,3323,3376,3380,3389,3393,3405,3407,3411,3416,3422,3427,3435,3440,3459,3464,3474,3479,3500,3505,3514,3519,3541,3543,3547,3551,3554,3585,3592,3596,3602,3625,3635,3639,3642,3694,3697,3699,3703,3755,3943,4126],[11,12,4],"h1",{"id":13},"dockerfile-и-docker-compose",[15,16,5],"p",{},[15,18,19],{},"Это те навыки, которые вы будете применять ежедневно: написание Dockerfile — часть разработки любого Go-сервиса, а Docker Compose — стандарт для локального окружения.",[21,22],"hr",{},[24,25,27],"h2",{"id":26},"dockerfile-рецепт-сборки-образа","Dockerfile — рецепт сборки образа",[15,29,30],{},"Dockerfile — это текстовый файл с последовательным набором инструкций, по которым Docker автоматически собирает образ. Вместо того чтобы вручную настраивать окружение, разработчик описывает шаги декларативно.",[32,33,35],"h3",{"id":34},"основные-инструкции","Основные инструкции",[37,38,39,56],"table",{},[40,41,42],"thead",{},[43,44,45,50,53],"tr",{},[46,47,49],"th",{"align":48},"left","Инструкция",[46,51,52],{"align":48},"Назначение",[46,54,55],{"align":48},"Пример",[57,58,59,76,91,106,121,136,151,166,181,200,215],"tbody",{},[43,60,61,68,71],{},[62,63,64],"td",{"align":48},[65,66,67],"code",{},"FROM",[62,69,70],{"align":48},"Базовый образ (фундамент)",[62,72,73],{"align":48},[65,74,75],{},"FROM golang:1.26-alpine3.23",[43,77,78,83,86],{},[62,79,80],{"align":48},[65,81,82],{},"WORKDIR",[62,84,85],{"align":48},"Рабочая директория внутри образа",[62,87,88],{"align":48},[65,89,90],{},"WORKDIR \u002Fapp",[43,92,93,98,101],{},[62,94,95],{"align":48},[65,96,97],{},"COPY",[62,99,100],{"align":48},"Копирование файлов с хоста в образ",[62,102,103],{"align":48},[65,104,105],{},"COPY . .",[43,107,108,113,116],{},[62,109,110],{"align":48},[65,111,112],{},"ADD",[62,114,115],{"align":48},"То же, что COPY + распаковка архивов и URL",[62,117,118],{"align":48},[65,119,120],{},"ADD app.tar.gz \u002Fapp",[43,122,123,128,131],{},[62,124,125],{"align":48},[65,126,127],{},"RUN",[62,129,130],{"align":48},"Выполнение команды при сборке",[62,132,133],{"align":48},[65,134,135],{},"RUN go build -o server",[43,137,138,143,146],{},[62,139,140],{"align":48},[65,141,142],{},"ENV",[62,144,145],{"align":48},"Установка переменных окружения",[62,147,148],{"align":48},[65,149,150],{},"ENV GIN_MODE=release",[43,152,153,158,161],{},[62,154,155],{"align":48},[65,156,157],{},"EXPOSE",[62,159,160],{"align":48},"Документирование порта (не открывает его!)",[62,162,163],{"align":48},[65,164,165],{},"EXPOSE 8080",[43,167,168,173,176],{},[62,169,170],{"align":48},[65,171,172],{},"ENTRYPOINT",[62,174,175],{"align":48},"Основная команда контейнера (не перезаписывается)",[62,177,178],{"align":48},[65,179,180],{},"ENTRYPOINT [\"\u002Fserver\"]",[43,182,183,188,195],{},[62,184,185],{"align":48},[65,186,187],{},"CMD",[62,189,190,191,194],{"align":48},"Аргументы по умолчанию (перезаписывается при ",[65,192,193],{},"docker run",")",[62,196,197],{"align":48},[65,198,199],{},"CMD [\"--port\", \"8080\"]",[43,201,202,207,210],{},[62,203,204],{"align":48},[65,205,206],{},"ARG",[62,208,209],{"align":48},"Аргументы сборки (доступны только при build)",[62,211,212],{"align":48},[65,213,214],{},"ARG VERSION=1.0",[43,216,217,222,225],{},[62,218,219],{"align":48},[65,220,221],{},"LABEL",[62,223,224],{"align":48},"Метаданные образа",[62,226,227],{"align":48},[65,228,229],{},"LABEL maintainer=\"team@corp.com\"",[32,231,233],{"id":232},"from-выбор-базового-образа","FROM — выбор базового образа",[15,235,236,237,239],{},"Каждый Dockerfile начинается с ",[65,238,67],{},". Это основа, на которой строится ваш образ. Выбор базового образа влияет на размер, безопасность и совместимость:",[241,242,247],"pre",{"className":243,"code":244,"language":245,"meta":246,"style":246},"language-dockerfile shiki shiki-themes github-dark","# Полный дистрибутив Debian — много утилит, но тяжёлый (~900 МБ)\nFROM golang:1.26\n\n# Alpine — минималистичный Linux (~7 МБ), но использует musl вместо glibc\nFROM golang:1.26-alpine3.23\n\n# Distroless — только рантайм, нет shell, нет пакетного менеджера\nFROM gcr.io\u002Fdistroless\u002Fstatic-debian12\n\n# Пустой образ — 0 байт, только ваш бинарник\nFROM scratch\n","dockerfile","",[65,248,249,258,268,275,281,289,294,300,308,313,319],{"__ignoreMap":246},[250,251,254],"span",{"class":252,"line":253},"line",1,[250,255,257],{"class":256},"sAwPA","# Полный дистрибутив Debian — много утилит, но тяжёлый (~900 МБ)\n",[250,259,261,264],{"class":252,"line":260},2,[250,262,67],{"class":263},"snl16",[250,265,267],{"class":266},"s95oV"," golang:1.26\n",[250,269,271],{"class":252,"line":270},3,[250,272,274],{"emptyLinePlaceholder":273},true,"\n",[250,276,278],{"class":252,"line":277},4,[250,279,280],{"class":256},"# Alpine — минималистичный Linux (~7 МБ), но использует musl вместо glibc\n",[250,282,284,286],{"class":252,"line":283},5,[250,285,67],{"class":263},[250,287,288],{"class":266}," golang:1.26-alpine3.23\n",[250,290,292],{"class":252,"line":291},6,[250,293,274],{"emptyLinePlaceholder":273},[250,295,297],{"class":252,"line":296},7,[250,298,299],{"class":256},"# Distroless — только рантайм, нет shell, нет пакетного менеджера\n",[250,301,303,305],{"class":252,"line":302},8,[250,304,67],{"class":263},[250,306,307],{"class":266}," gcr.io\u002Fdistroless\u002Fstatic-debian12\n",[250,309,311],{"class":252,"line":310},9,[250,312,274],{"emptyLinePlaceholder":273},[250,314,316],{"class":252,"line":315},10,[250,317,318],{"class":256},"# Пустой образ — 0 байт, только ваш бинарник\n",[250,320,322,324],{"class":252,"line":321},11,[250,323,67],{"class":263},[250,325,326],{"class":266}," scratch\n",[15,328,329],{},"Фиксируйте версии, но не забывайте их обновлять: production-образ должен опираться на поддерживаемые ветки Go и Alpine, а не на давно протухший тег.",[15,331,332,333,336,337,340],{},"Для Go-приложений ",[65,334,335],{},"alpine"," — оптимальный выбор для финального образа: маленький, но с shell для отладки. ",[65,338,339],{},"scratch"," ещё меньше, но в нём нет ни shell, ни утилит — сложнее дебажить в production.",[32,342,344],{"id":343},"workdir-рабочая-директория","WORKDIR — рабочая директория",[15,346,347,349],{},[65,348,82],{}," задаёт директорию, в которой будут выполняться все последующие команды. Если директория не существует — Docker создаст её автоматически:",[241,351,353],{"className":243,"code":352,"language":245,"meta":246,"style":246},"WORKDIR \u002Fapp\n\n# Все последующие COPY, RUN и т.д. работают относительно \u002Fapp\nCOPY . .         # копирует в \u002Fapp\nRUN go build     # выполняется в \u002Fapp\n",[65,354,355,362,366,371,378],{"__ignoreMap":246},[250,356,357,359],{"class":252,"line":253},[250,358,82],{"class":263},[250,360,361],{"class":266}," \u002Fapp\n",[250,363,364],{"class":252,"line":260},[250,365,274],{"emptyLinePlaceholder":273},[250,367,368],{"class":252,"line":270},[250,369,370],{"class":256},"# Все последующие COPY, RUN и т.д. работают относительно \u002Fapp\n",[250,372,373,375],{"class":252,"line":277},[250,374,97],{"class":263},[250,376,377],{"class":266}," . .         # копирует в \u002Fapp\n",[250,379,380,382],{"class":252,"line":283},[250,381,127],{"class":263},[250,383,384],{"class":266}," go build     # выполняется в \u002Fapp\n",[15,386,387,388,390,391,394,395,397],{},"Используйте ",[65,389,82],{}," вместо ",[65,392,393],{},"RUN mkdir -p \u002Fapp && cd \u002Fapp",". Каждый ",[65,396,127],{}," начинает выполнение в WORKDIR заново (cd внутри RUN не влияет на следующий RUN).",[32,399,401],{"id":400},"copy-vs-add","COPY vs ADD",[15,403,404,406,407,409,410,413],{},[65,405,97],{}," копирует файлы из контекста сборки в образ. ",[65,408,112],{}," делает то же самое, но с двумя дополнительными возможностями: автоматически распаковывает ",[65,411,412],{},".tar"," архивы и поддерживает URL.",[241,415,417],{"className":243,"code":416,"language":245,"meta":246,"style":246},"# Предпочтительно — явно и предсказуемо\nCOPY go.mod go.sum .\u002F\nCOPY . .\n\n# ADD — только если действительно нужна распаковка\nADD configs.tar.gz \u002Fapp\u002Fconfigs\u002F\n",[65,418,419,424,431,438,442,447],{"__ignoreMap":246},[250,420,421],{"class":252,"line":253},[250,422,423],{"class":256},"# Предпочтительно — явно и предсказуемо\n",[250,425,426,428],{"class":252,"line":260},[250,427,97],{"class":263},[250,429,430],{"class":266}," go.mod go.sum .\u002F\n",[250,432,433,435],{"class":252,"line":270},[250,434,97],{"class":263},[250,436,437],{"class":266}," . .\n",[250,439,440],{"class":252,"line":277},[250,441,274],{"emptyLinePlaceholder":273},[250,443,444],{"class":252,"line":283},[250,445,446],{"class":256},"# ADD — только если действительно нужна распаковка\n",[250,448,449,451],{"class":252,"line":291},[250,450,112],{"class":263},[250,452,453],{"class":266}," configs.tar.gz \u002Fapp\u002Fconfigs\u002F\n",[15,455,456,457,459,460,462],{},"Best practice: всегда используйте ",[65,458,97],{},", если не нужна автоматическая распаковка. ",[65,461,112],{}," менее предсказуем и может привести к неожиданному поведению.",[32,464,466],{"id":465},"run-выполнение-команд-при-сборке","RUN — выполнение команд при сборке",[15,468,469,471,472,474],{},[65,470,127],{}," выполняет команду и создаёт новый слой. Всё, что установлено или сгенерировано через ",[65,473,127],{},", становится частью образа:",[241,476,478],{"className":243,"code":477,"language":245,"meta":246,"style":246},"# Установка системных зависимостей\nRUN apk add --no-cache git ca-certificates\n\n# Скачивание Go-зависимостей\nRUN go mod download\n\n# Компиляция\nRUN CGO_ENABLED=0 GOOS=linux go build -o \u002Fapp\u002Fserver .\u002Fcmd\u002Fserver\n",[65,479,480,485,492,496,501,508,512,517],{"__ignoreMap":246},[250,481,482],{"class":252,"line":253},[250,483,484],{"class":256},"# Установка системных зависимостей\n",[250,486,487,489],{"class":252,"line":260},[250,488,127],{"class":263},[250,490,491],{"class":266}," apk add --no-cache git ca-certificates\n",[250,493,494],{"class":252,"line":270},[250,495,274],{"emptyLinePlaceholder":273},[250,497,498],{"class":252,"line":277},[250,499,500],{"class":256},"# Скачивание Go-зависимостей\n",[250,502,503,505],{"class":252,"line":283},[250,504,127],{"class":263},[250,506,507],{"class":266}," go mod download\n",[250,509,510],{"class":252,"line":291},[250,511,274],{"emptyLinePlaceholder":273},[250,513,514],{"class":252,"line":296},[250,515,516],{"class":256},"# Компиляция\n",[250,518,519,521],{"class":252,"line":302},[250,520,127],{"class":263},[250,522,523],{"class":266}," CGO_ENABLED=0 GOOS=linux go build -o \u002Fapp\u002Fserver .\u002Fcmd\u002Fserver\n",[15,525,526,527,529,530,533],{},"Помните о слоях: каждый ",[65,528,127],{}," — новый слой. Связанные команды стоит объединять через ",[65,531,532],{},"&&",":",[241,535,537],{"className":243,"code":536,"language":245,"meta":246,"style":246},"# Хорошо: один слой, промежуточные файлы не остаются\nRUN apk add --no-cache git ca-certificates && \\\n    go mod download && \\\n    go build -o \u002Fapp\u002Fserver .\u002Fcmd\u002Fserver\n",[65,538,539,544,551,556],{"__ignoreMap":246},[250,540,541],{"class":252,"line":253},[250,542,543],{"class":256},"# Хорошо: один слой, промежуточные файлы не остаются\n",[250,545,546,548],{"class":252,"line":260},[250,547,127],{"class":263},[250,549,550],{"class":266}," apk add --no-cache git ca-certificates && \\\n",[250,552,553],{"class":252,"line":270},[250,554,555],{"class":266},"    go mod download && \\\n",[250,557,558],{"class":252,"line":277},[250,559,560],{"class":266},"    go build -o \u002Fapp\u002Fserver .\u002Fcmd\u002Fserver\n",[32,562,564],{"id":563},"entrypoint-vs-cmd","ENTRYPOINT vs CMD",[15,566,567],{},"Это одна из самых путающих тем в Docker. Оба задают команду запуска, но по-разному:",[15,569,570,573,574,576,577,580],{},[571,572,172],"strong",{}," — основная команда контейнера. Не перезаписывается аргументами ",[65,575,193],{}," (только через ",[65,578,579],{},"--entrypoint","):",[241,582,584],{"className":243,"code":583,"language":245,"meta":246,"style":246},"ENTRYPOINT [\"\u002Fserver\"]\n",[65,585,586],{"__ignoreMap":246},[250,587,588,590,593,597],{"class":252,"line":253},[250,589,172],{"class":263},[250,591,592],{"class":266}," [",[250,594,596],{"class":595},"sU2Wk","\"\u002Fserver\"",[250,598,599],{"class":266},"]\n",[15,601,602,604,605,533],{},[571,603,187],{}," — аргументы по умолчанию. Перезаписываются при ",[65,606,193],{},[241,608,610],{"className":243,"code":609,"language":245,"meta":246,"style":246},"CMD [\"--port\", \"8080\"]\n",[65,611,612],{"__ignoreMap":246},[250,613,614,616,618,621,624,627],{"class":252,"line":253},[250,615,187],{"class":263},[250,617,592],{"class":266},[250,619,620],{"class":595},"\"--port\"",[250,622,623],{"class":266},", ",[250,625,626],{"class":595},"\"8080\"",[250,628,599],{"class":266},[15,630,631,634],{},[571,632,633],{},"В связке"," ENTRYPOINT + CMD работают так:",[241,636,638],{"className":243,"code":637,"language":245,"meta":246,"style":246},"ENTRYPOINT [\"\u002Fserver\"]\nCMD [\"--port\", \"8080\"]\n\n# docker run my-app                  --> \u002Fserver --port 8080\n# docker run my-app --port 9090      --> \u002Fserver --port 9090\n# docker run my-app --debug          --> \u002Fserver --debug\n",[65,639,640,650,664,668,673,678],{"__ignoreMap":246},[250,641,642,644,646,648],{"class":252,"line":253},[250,643,172],{"class":263},[250,645,592],{"class":266},[250,647,596],{"class":595},[250,649,599],{"class":266},[250,651,652,654,656,658,660,662],{"class":252,"line":260},[250,653,187],{"class":263},[250,655,592],{"class":266},[250,657,620],{"class":595},[250,659,623],{"class":266},[250,661,626],{"class":595},[250,663,599],{"class":266},[250,665,666],{"class":252,"line":270},[250,667,274],{"emptyLinePlaceholder":273},[250,669,670],{"class":252,"line":277},[250,671,672],{"class":256},"# docker run my-app                  --> \u002Fserver --port 8080\n",[250,674,675],{"class":252,"line":283},[250,676,677],{"class":256},"# docker run my-app --port 9090      --> \u002Fserver --port 9090\n",[250,679,680],{"class":252,"line":291},[250,681,682],{"class":256},"# docker run my-app --debug          --> \u002Fserver --debug\n",[15,684,685],{},"CMD подставляется как аргументы к ENTRYPOINT. Если пользователь передаёт свои аргументы — CMD заменяется.",[15,687,688],{},[571,689,690],{},"Exec-форма vs Shell-форма:",[241,692,694],{"className":243,"code":693,"language":245,"meta":246,"style":246},"# Exec-форма (рекомендуется) — процесс запускается напрямую, получает PID 1\nENTRYPOINT [\"\u002Fserver\"]\n\n# Shell-форма — оборачивается в \u002Fbin\u002Fsh -c, сигналы не доходят до процесса\nENTRYPOINT \u002Fserver\n",[65,695,696,701,711,715,720],{"__ignoreMap":246},[250,697,698],{"class":252,"line":253},[250,699,700],{"class":256},"# Exec-форма (рекомендуется) — процесс запускается напрямую, получает PID 1\n",[250,702,703,705,707,709],{"class":252,"line":260},[250,704,172],{"class":263},[250,706,592],{"class":266},[250,708,596],{"class":595},[250,710,599],{"class":266},[250,712,713],{"class":252,"line":270},[250,714,274],{"emptyLinePlaceholder":273},[250,716,717],{"class":252,"line":277},[250,718,719],{"class":256},"# Shell-форма — оборачивается в \u002Fbin\u002Fsh -c, сигналы не доходят до процесса\n",[250,721,722,724],{"class":252,"line":283},[250,723,172],{"class":263},[250,725,726],{"class":266}," \u002Fserver\n",[15,728,729,730,733,734,737],{},"Всегда используйте exec-форму (с квадратными скобками). Shell-форма запускает процесс через ",[65,731,732],{},"sh -c",", и сигналы (SIGTERM при ",[65,735,736],{},"docker stop",") приходят в shell, а не в ваше приложение. Go-приложение не получит graceful shutdown.",[32,739,741],{"id":740},"полный-пример-простой-dockerfile-для-go","Полный пример: простой Dockerfile для Go",[241,743,745],{"className":243,"code":744,"language":245,"meta":246,"style":246},"FROM golang:1.26-alpine3.23\n\nWORKDIR \u002Fapp\n\n# Сначала зависимости (кэшируются отдельно)\nCOPY go.mod go.sum .\u002F\nRUN go mod download\n\n# Потом весь код\nCOPY . .\n\n# Сборка\nRUN CGO_ENABLED=0 go build -o \u002Fapp\u002Fserver .\u002Fcmd\u002Fserver\n\nEXPOSE 8080\n\nENTRYPOINT [\"\u002Fapp\u002Fserver\"]\n",[65,746,747,753,757,763,767,772,778,784,788,793,799,803,809,817,822,830,835],{"__ignoreMap":246},[250,748,749,751],{"class":252,"line":253},[250,750,67],{"class":263},[250,752,288],{"class":266},[250,754,755],{"class":252,"line":260},[250,756,274],{"emptyLinePlaceholder":273},[250,758,759,761],{"class":252,"line":270},[250,760,82],{"class":263},[250,762,361],{"class":266},[250,764,765],{"class":252,"line":277},[250,766,274],{"emptyLinePlaceholder":273},[250,768,769],{"class":252,"line":283},[250,770,771],{"class":256},"# Сначала зависимости (кэшируются отдельно)\n",[250,773,774,776],{"class":252,"line":291},[250,775,97],{"class":263},[250,777,430],{"class":266},[250,779,780,782],{"class":252,"line":296},[250,781,127],{"class":263},[250,783,507],{"class":266},[250,785,786],{"class":252,"line":302},[250,787,274],{"emptyLinePlaceholder":273},[250,789,790],{"class":252,"line":310},[250,791,792],{"class":256},"# Потом весь код\n",[250,794,795,797],{"class":252,"line":315},[250,796,97],{"class":263},[250,798,437],{"class":266},[250,800,801],{"class":252,"line":321},[250,802,274],{"emptyLinePlaceholder":273},[250,804,806],{"class":252,"line":805},12,[250,807,808],{"class":256},"# Сборка\n",[250,810,812,814],{"class":252,"line":811},13,[250,813,127],{"class":263},[250,815,816],{"class":266}," CGO_ENABLED=0 go build -o \u002Fapp\u002Fserver .\u002Fcmd\u002Fserver\n",[250,818,820],{"class":252,"line":819},14,[250,821,274],{"emptyLinePlaceholder":273},[250,823,825,827],{"class":252,"line":824},15,[250,826,157],{"class":263},[250,828,829],{"class":266}," 8080\n",[250,831,833],{"class":252,"line":832},16,[250,834,274],{"emptyLinePlaceholder":273},[250,836,838,840,842,845],{"class":252,"line":837},17,[250,839,172],{"class":263},[250,841,592],{"class":266},[250,843,844],{"class":595},"\"\u002Fapp\u002Fserver\"",[250,846,599],{"class":266},[15,848,849],{},"Этот Dockerfile рабочий, но не оптимальный — финальный образ содержит весь Go SDK (~800 МБ), исходный код и промежуточные файлы. Multi-stage build решает эту проблему.",[21,851],{},[24,853,855],{"id":854},"multi-stage-build-для-go","Multi-stage build для Go",[15,857,858],{},"Multi-stage build — это ключевая техника оптимизации Docker-образов для Go. Идея проста: используем два этапа (stage) — на первом собираем бинарник, на второй копируем только его.",[32,860,862],{"id":861},"зачем-это-нужно","Зачем это нужно",[15,864,865,866,869],{},"Go компилируется в нативный бинарник — ему не нужен рантайм, SDK или исходники для работы. Но если собирать в обычном Dockerfile на базе ",[65,867,868],{},"golang:1.26",", финальный образ будет ~800 МБ:",[241,871,876],{"className":872,"code":874,"language":875},[873],"language-text","golang:1.26       ~800 MB  (SDK, тулчейн, все утилиты)\nalpine:3.23       ~7 MB    + ваш бинарник\nscratch           ~0 MB    + ваш бинарник (нет shell, нет отладки)\n","text",[65,877,874],{"__ignoreMap":246},[15,879,880],{},"Multi-stage build позволяет собрать в «тяжёлом» образе, а запускать в «лёгком».",[32,882,884],{"id":883},"шаблон-multi-stage-build","Шаблон multi-stage build",[241,886,888],{"className":243,"code":887,"language":245,"meta":246,"style":246},"# === Стадия 1: сборка ===\nFROM golang:1.26-alpine3.23 AS builder\n\nWORKDIR \u002Fapp\n\n# Зависимости отдельно от кода (кэширование)\nCOPY go.mod go.sum .\u002F\nRUN go mod download\n\n# Код\nCOPY . .\n\n# Сборка статического бинарника\nRUN CGO_ENABLED=0 GOOS=linux go build -o \u002Fapp\u002Fserver .\u002Fcmd\u002Fserver\n\n# === Стадия 2: финальный образ ===\nFROM alpine:3.23\n\n# Сертификаты для HTTPS-запросов\nRUN apk add --no-cache ca-certificates\n\n# Копируем ТОЛЬКО бинарник из стадии builder\nCOPY --from=builder \u002Fapp\u002Fserver \u002Fserver\n\nEXPOSE 8080\n\nENTRYPOINT [\"\u002Fserver\"]\n",[65,889,890,895,908,912,918,922,927,933,939,943,948,954,958,963,969,973,978,985,990,996,1004,1009,1015,1023,1028,1035,1040],{"__ignoreMap":246},[250,891,892],{"class":252,"line":253},[250,893,894],{"class":256},"# === Стадия 1: сборка ===\n",[250,896,897,899,902,905],{"class":252,"line":260},[250,898,67],{"class":263},[250,900,901],{"class":266}," golang:1.26-alpine3.23 ",[250,903,904],{"class":263},"AS",[250,906,907],{"class":266}," builder\n",[250,909,910],{"class":252,"line":270},[250,911,274],{"emptyLinePlaceholder":273},[250,913,914,916],{"class":252,"line":277},[250,915,82],{"class":263},[250,917,361],{"class":266},[250,919,920],{"class":252,"line":283},[250,921,274],{"emptyLinePlaceholder":273},[250,923,924],{"class":252,"line":291},[250,925,926],{"class":256},"# Зависимости отдельно от кода (кэширование)\n",[250,928,929,931],{"class":252,"line":296},[250,930,97],{"class":263},[250,932,430],{"class":266},[250,934,935,937],{"class":252,"line":302},[250,936,127],{"class":263},[250,938,507],{"class":266},[250,940,941],{"class":252,"line":310},[250,942,274],{"emptyLinePlaceholder":273},[250,944,945],{"class":252,"line":315},[250,946,947],{"class":256},"# Код\n",[250,949,950,952],{"class":252,"line":321},[250,951,97],{"class":263},[250,953,437],{"class":266},[250,955,956],{"class":252,"line":805},[250,957,274],{"emptyLinePlaceholder":273},[250,959,960],{"class":252,"line":811},[250,961,962],{"class":256},"# Сборка статического бинарника\n",[250,964,965,967],{"class":252,"line":819},[250,966,127],{"class":263},[250,968,523],{"class":266},[250,970,971],{"class":252,"line":824},[250,972,274],{"emptyLinePlaceholder":273},[250,974,975],{"class":252,"line":832},[250,976,977],{"class":256},"# === Стадия 2: финальный образ ===\n",[250,979,980,982],{"class":252,"line":837},[250,981,67],{"class":263},[250,983,984],{"class":266}," alpine:3.23\n",[250,986,988],{"class":252,"line":987},18,[250,989,274],{"emptyLinePlaceholder":273},[250,991,993],{"class":252,"line":992},19,[250,994,995],{"class":256},"# Сертификаты для HTTPS-запросов\n",[250,997,999,1001],{"class":252,"line":998},20,[250,1000,127],{"class":263},[250,1002,1003],{"class":266}," apk add --no-cache ca-certificates\n",[250,1005,1007],{"class":252,"line":1006},21,[250,1008,274],{"emptyLinePlaceholder":273},[250,1010,1012],{"class":252,"line":1011},22,[250,1013,1014],{"class":256},"# Копируем ТОЛЬКО бинарник из стадии builder\n",[250,1016,1018,1020],{"class":252,"line":1017},23,[250,1019,97],{"class":263},[250,1021,1022],{"class":266}," --from=builder \u002Fapp\u002Fserver \u002Fserver\n",[250,1024,1026],{"class":252,"line":1025},24,[250,1027,274],{"emptyLinePlaceholder":273},[250,1029,1031,1033],{"class":252,"line":1030},25,[250,1032,157],{"class":263},[250,1034,829],{"class":266},[250,1036,1038],{"class":252,"line":1037},26,[250,1039,274],{"emptyLinePlaceholder":273},[250,1041,1043,1045,1047,1049],{"class":252,"line":1042},27,[250,1044,172],{"class":263},[250,1046,592],{"class":266},[250,1048,596],{"class":595},[250,1050,599],{"class":266},[32,1052,1054],{"id":1053},"разбор-по-шагам","Разбор по шагам",[15,1056,1057],{},[571,1058,1059],{},"Стадия 1 (builder):",[1061,1062,1063,1073,1084],"ul",{},[1064,1065,1066,1069,1070],"li",{},[65,1067,1068],{},"FROM golang:1.26-alpine3.23 AS builder"," — берём полный Go SDK, даём стадии имя ",[65,1071,1072],{},"builder",[1064,1074,1075,1076,1079,1080,1083],{},"Копируем ",[65,1077,1078],{},"go.mod","\u002F",[65,1081,1082],{},"go.sum"," и скачиваем зависимости — слой кэшируется отдельно",[1064,1085,1086],{},"Копируем исходный код и собираем бинарник",[15,1088,1089],{},[571,1090,1091],{},"Стадия 2 (финальная):",[1061,1093,1094,1100,1106],{},[1064,1095,1096,1099],{},[65,1097,1098],{},"FROM alpine:3.23"," — чистый Alpine, ~7 МБ",[1064,1101,1102,1105],{},[65,1103,1104],{},"COPY --from=builder \u002Fapp\u002Fserver \u002Fserver"," — копируем только бинарник из первой стадии",[1064,1107,1108],{},"Всё остальное (SDK, исходники, промежуточные файлы) отбрасывается",[15,1110,1111],{},"Результат: вместо 800+ МБ получаем ~15-25 МБ (Alpine + бинарник).",[32,1113,1115],{"id":1114},"cgo_enabled0-зачем-это-нужно","CGO_ENABLED=0 — зачем это нужно",[241,1117,1119],{"className":243,"code":1118,"language":245,"meta":246,"style":246},"RUN CGO_ENABLED=0 GOOS=linux go build -o \u002Fapp\u002Fserver .\u002Fcmd\u002Fserver\n",[65,1120,1121],{"__ignoreMap":246},[250,1122,1123,1125],{"class":252,"line":253},[250,1124,127],{"class":263},[250,1126,523],{"class":266},[15,1128,1129,1132,1133,1136],{},[65,1130,1131],{},"CGO_ENABLED=0"," отключает CGO — механизм вызова C-кода из Go. Без этого флага Go может линковаться с системными C-библиотеками (например, ",[65,1134,1135],{},"libc","), и бинарник не будет работать в другом окружении.",[15,1138,1139,1140,1142,1143,1146,1147,1149],{},"С ",[65,1141,1131],{}," Go создаёт ",[571,1144,1145],{},"полностью статический бинарник"," — он содержит всё необходимое внутри себя и не зависит от каких-либо системных библиотек. Такой бинарник можно запускать даже в ",[65,1148,339],{}," (пустом образе).",[241,1151,1153],{"className":243,"code":1152,"language":245,"meta":246,"style":246},"# Минимальный образ — только бинарник, ничего больше\nFROM scratch\nCOPY --from=builder \u002Fapp\u002Fserver \u002Fserver\nENTRYPOINT [\"\u002Fserver\"]\n",[65,1154,1155,1160,1166,1172],{"__ignoreMap":246},[250,1156,1157],{"class":252,"line":253},[250,1158,1159],{"class":256},"# Минимальный образ — только бинарник, ничего больше\n",[250,1161,1162,1164],{"class":252,"line":260},[250,1163,67],{"class":263},[250,1165,326],{"class":266},[250,1167,1168,1170],{"class":252,"line":270},[250,1169,97],{"class":263},[250,1171,1022],{"class":266},[250,1173,1174,1176,1178,1180],{"class":252,"line":277},[250,1175,172],{"class":263},[250,1177,592],{"class":266},[250,1179,596],{"class":595},[250,1181,599],{"class":266},[15,1183,1184,1185,1187,1188,1191],{},"Но у ",[65,1186,339],{}," есть минусы: нет shell (нельзя зайти в контейнер через ",[65,1189,1190],{},"exec","), нет утилит для отладки, нет CA-сертификатов (HTTPS-запросы не будут работать без явного копирования сертификатов).",[32,1193,1195],{"id":1194},"дополнительные-флаги-оптимизации","Дополнительные флаги оптимизации",[15,1197,1198,1199,1202,1203,1206,1207,1210],{},"Флаг ",[65,1200,1201],{},"-ldflags=\"-s -w\""," убирает таблицу символов (",[65,1204,1205],{},"-s",") и отладочную информацию (",[65,1208,1209],{},"-w","), уменьшая бинарник на 20-30%. Минус: stack trace при панике будет менее информативным.",[32,1212,1214],{"id":1213},"сравнение-размеров","Сравнение размеров",[37,1216,1217,1227],{},[40,1218,1219],{},[43,1220,1221,1224],{},[46,1222,1223],{"align":48},"Подход",[46,1225,1226],{"align":48},"Размер образа",[57,1228,1229,1240,1251,1263],{},[43,1230,1231,1237],{},[62,1232,1233,1236],{"align":48},[65,1234,1235],{},"FROM golang:1.26"," (без multi-stage)",[62,1238,1239],{"align":48},"~800 МБ",[43,1241,1242,1248],{},[62,1243,1244,1245],{"align":48},"Multi-stage + ",[65,1246,1247],{},"alpine:3.23",[62,1249,1250],{"align":48},"~15-25 МБ",[43,1252,1253,1260],{},[62,1254,1244,1255,1257,1258],{"align":48},[65,1256,335],{}," + ",[65,1259,1201],{},[62,1261,1262],{"align":48},"~10-20 МБ",[43,1264,1265,1271],{},[62,1266,1244,1267,1257,1269],{"align":48},[65,1268,339],{},[65,1270,1201],{},[62,1272,1273],{"align":48},"~5-15 МБ",[21,1275],{},[24,1277,1279],{"id":1278},"best-practices-по-написанию-dockerfile","Best practices по написанию Dockerfile",[32,1281,1283],{"id":1282},"_1-порядок-инструкций-от-редко-к-часто-меняющемуся","1. Порядок инструкций: от редко к часто меняющемуся",[15,1285,1286],{},"Слои кэшируются, но при изменении одного слоя все последующие пересобираются. Располагайте инструкции от самых стабильных к самым изменчивым:",[241,1288,1290],{"className":243,"code":1289,"language":245,"meta":246,"style":246},"FROM golang:1.26-alpine3.23 AS builder\nWORKDIR \u002Fapp\n\n# 1. Системные зависимости (меняются раз в месяц)\nRUN apk add --no-cache git ca-certificates\n\n# 2. Go-зависимости (меняются раз в неделю)\nCOPY go.mod go.sum .\u002F\nRUN go mod download\n\n# 3. Исходный код (меняется каждый коммит)\nCOPY . .\nRUN CGO_ENABLED=0 go build -o \u002Fapp\u002Fserver .\u002Fcmd\u002Fserver\n",[65,1291,1292,1302,1308,1312,1317,1323,1327,1332,1338,1344,1348,1353,1359],{"__ignoreMap":246},[250,1293,1294,1296,1298,1300],{"class":252,"line":253},[250,1295,67],{"class":263},[250,1297,901],{"class":266},[250,1299,904],{"class":263},[250,1301,907],{"class":266},[250,1303,1304,1306],{"class":252,"line":260},[250,1305,82],{"class":263},[250,1307,361],{"class":266},[250,1309,1310],{"class":252,"line":270},[250,1311,274],{"emptyLinePlaceholder":273},[250,1313,1314],{"class":252,"line":277},[250,1315,1316],{"class":256},"# 1. Системные зависимости (меняются раз в месяц)\n",[250,1318,1319,1321],{"class":252,"line":283},[250,1320,127],{"class":263},[250,1322,491],{"class":266},[250,1324,1325],{"class":252,"line":291},[250,1326,274],{"emptyLinePlaceholder":273},[250,1328,1329],{"class":252,"line":296},[250,1330,1331],{"class":256},"# 2. Go-зависимости (меняются раз в неделю)\n",[250,1333,1334,1336],{"class":252,"line":302},[250,1335,97],{"class":263},[250,1337,430],{"class":266},[250,1339,1340,1342],{"class":252,"line":310},[250,1341,127],{"class":263},[250,1343,507],{"class":266},[250,1345,1346],{"class":252,"line":315},[250,1347,274],{"emptyLinePlaceholder":273},[250,1349,1350],{"class":252,"line":321},[250,1351,1352],{"class":256},"# 3. Исходный код (меняется каждый коммит)\n",[250,1354,1355,1357],{"class":252,"line":805},[250,1356,97],{"class":263},[250,1358,437],{"class":266},[250,1360,1361,1363],{"class":252,"line":811},[250,1362,127],{"class":263},[250,1364,816],{"class":266},[15,1366,1367],{},"Если вы измените только Go-код, Docker возьмёт первые два блока из кэша и пересоберёт только третий.",[32,1369,1371],{"id":1370},"_2-dockerignore-исключение-лишних-файлов","2. .dockerignore — исключение лишних файлов",[15,1373,1374,1375,1378,1379,1382],{},"Файл ",[65,1376,1377],{},".dockerignore"," работает как ",[65,1380,1381],{},".gitignore"," — исключает файлы из контекста сборки. Это ускоряет сборку и предотвращает попадание чувствительных данных в образ:",[241,1384,1387],{"className":1385,"code":1386,"language":875},[873],"# .dockerignore\n.git\n.github\n.gitignore\n.env\n*.md\nREADME*\nLICENSE\ndocs\u002F\ntmp\u002F\nvendor\u002F    # если используете go mod download в Dockerfile\n**\u002F*_test.go\n",[65,1388,1386],{"__ignoreMap":246},[15,1390,1391,1392,1394,1395,1398],{},"Без ",[65,1393,1377],{}," Docker отправляет daemon'у весь каталог проекта, включая ",[65,1396,1397],{},".git"," (который может весить сотни мегабайт).",[32,1400,1402],{"id":1401},"_3-не-запускайте-процесс-от-root","3. Не запускайте процесс от root",[15,1404,1405],{},"По умолчанию процесс в контейнере работает от root. Это плохая практика — если злоумышленник пробьёт контейнер, он получит root-доступ:",[241,1407,1409],{"className":243,"code":1408,"language":245,"meta":246,"style":246},"FROM alpine:3.23\n\n# Создаём непривилегированного пользователя\nRUN addgroup -S appgroup && adduser -S appuser -G appgroup\n\nCOPY --from=builder \u002Fapp\u002Fserver \u002Fserver\n\n# Переключаемся на непривилегированного пользователя\nUSER appuser\n\nENTRYPOINT [\"\u002Fserver\"]\n",[65,1410,1411,1417,1421,1426,1433,1437,1443,1447,1452,1460,1464],{"__ignoreMap":246},[250,1412,1413,1415],{"class":252,"line":253},[250,1414,67],{"class":263},[250,1416,984],{"class":266},[250,1418,1419],{"class":252,"line":260},[250,1420,274],{"emptyLinePlaceholder":273},[250,1422,1423],{"class":252,"line":270},[250,1424,1425],{"class":256},"# Создаём непривилегированного пользователя\n",[250,1427,1428,1430],{"class":252,"line":277},[250,1429,127],{"class":263},[250,1431,1432],{"class":266}," addgroup -S appgroup && adduser -S appuser -G appgroup\n",[250,1434,1435],{"class":252,"line":283},[250,1436,274],{"emptyLinePlaceholder":273},[250,1438,1439,1441],{"class":252,"line":291},[250,1440,97],{"class":263},[250,1442,1022],{"class":266},[250,1444,1445],{"class":252,"line":296},[250,1446,274],{"emptyLinePlaceholder":273},[250,1448,1449],{"class":252,"line":302},[250,1450,1451],{"class":256},"# Переключаемся на непривилегированного пользователя\n",[250,1453,1454,1457],{"class":252,"line":310},[250,1455,1456],{"class":263},"USER",[250,1458,1459],{"class":266}," appuser\n",[250,1461,1462],{"class":252,"line":315},[250,1463,274],{"emptyLinePlaceholder":273},[250,1465,1466,1468,1470,1472],{"class":252,"line":321},[250,1467,172],{"class":263},[250,1469,592],{"class":266},[250,1471,596],{"class":595},[250,1473,599],{"class":266},[32,1475,1477],{"id":1476},"_4-один-процесс-на-контейнер","4. Один процесс на контейнер",[15,1479,1480],{},"Контейнер должен запускать один процесс (ваш Go-сервис). Не ставьте в один контейнер приложение + базу данных + nginx. Каждый компонент — отдельный контейнер. Это упрощает масштабирование, мониторинг и обновление.",[32,1482,1484],{"id":1483},"_5-expose-документация-не-конфигурация","5. EXPOSE — документация, не конфигурация",[15,1486,1487,1489,1490,1493,1494,1496,1497,1500],{},[65,1488,165],{}," не открывает порт. Это документация для пользователя образа. Реальный проброс порта делается через ",[65,1491,1492],{},"-p"," при ",[65,1495,193],{}," или ",[65,1498,1499],{},"ports"," в Compose.",[32,1502,1504],{"id":1503},"_6-используйте-конкретные-теги","6. Используйте конкретные теги",[241,1506,1508],{"className":243,"code":1507,"language":245,"meta":246,"style":246},"# Плохо — latest может указывать на разные версии в разное время\nFROM golang:latest\n\n# Хорошо — конкретная версия, воспроизводимый результат\nFROM golang:1.26-alpine3.23\n",[65,1509,1510,1515,1522,1526,1531],{"__ignoreMap":246},[250,1511,1512],{"class":252,"line":253},[250,1513,1514],{"class":256},"# Плохо — latest может указывать на разные версии в разное время\n",[250,1516,1517,1519],{"class":252,"line":260},[250,1518,67],{"class":263},[250,1520,1521],{"class":266}," golang:latest\n",[250,1523,1524],{"class":252,"line":270},[250,1525,274],{"emptyLinePlaceholder":273},[250,1527,1528],{"class":252,"line":277},[250,1529,1530],{"class":256},"# Хорошо — конкретная версия, воспроизводимый результат\n",[250,1532,1533,1535],{"class":252,"line":283},[250,1534,67],{"class":263},[250,1536,288],{"class":266},[32,1538,1540],{"id":1539},"production-ready-dockerfile-для-go-сервиса","Production-ready Dockerfile для Go-сервиса",[15,1542,1543],{},"Собирая все best practices вместе:",[241,1545,1547],{"className":243,"code":1546,"language":245,"meta":246,"style":246},"# === Build stage ===\nFROM golang:1.26-alpine3.23 AS builder\n\nRUN apk add --no-cache ca-certificates git\n\nWORKDIR \u002Fapp\n\nCOPY go.mod go.sum .\u002F\nRUN go mod download\n\nCOPY . .\n\nRUN CGO_ENABLED=0 GOOS=linux go build \\\n    -ldflags=\"-s -w\" \\\n    -o \u002Fapp\u002Fserver .\u002Fcmd\u002Fserver\n\n# === Final stage ===\nFROM alpine:3.23\n\nRUN apk add --no-cache ca-certificates tzdata && \\\n    addgroup -S appgroup && adduser -S appuser -G appgroup\n\nCOPY --from=builder \u002Fapp\u002Fserver \u002Fserver\n\nUSER appuser\n\nEXPOSE 8080\n\nENTRYPOINT [\"\u002Fserver\"]\n",[65,1548,1549,1554,1564,1568,1575,1579,1585,1589,1595,1601,1605,1611,1615,1622,1633,1638,1642,1647,1653,1657,1664,1669,1673,1679,1683,1689,1693,1699,1704],{"__ignoreMap":246},[250,1550,1551],{"class":252,"line":253},[250,1552,1553],{"class":256},"# === Build stage ===\n",[250,1555,1556,1558,1560,1562],{"class":252,"line":260},[250,1557,67],{"class":263},[250,1559,901],{"class":266},[250,1561,904],{"class":263},[250,1563,907],{"class":266},[250,1565,1566],{"class":252,"line":270},[250,1567,274],{"emptyLinePlaceholder":273},[250,1569,1570,1572],{"class":252,"line":277},[250,1571,127],{"class":263},[250,1573,1574],{"class":266}," apk add --no-cache ca-certificates git\n",[250,1576,1577],{"class":252,"line":283},[250,1578,274],{"emptyLinePlaceholder":273},[250,1580,1581,1583],{"class":252,"line":291},[250,1582,82],{"class":263},[250,1584,361],{"class":266},[250,1586,1587],{"class":252,"line":296},[250,1588,274],{"emptyLinePlaceholder":273},[250,1590,1591,1593],{"class":252,"line":302},[250,1592,97],{"class":263},[250,1594,430],{"class":266},[250,1596,1597,1599],{"class":252,"line":310},[250,1598,127],{"class":263},[250,1600,507],{"class":266},[250,1602,1603],{"class":252,"line":315},[250,1604,274],{"emptyLinePlaceholder":273},[250,1606,1607,1609],{"class":252,"line":321},[250,1608,97],{"class":263},[250,1610,437],{"class":266},[250,1612,1613],{"class":252,"line":805},[250,1614,274],{"emptyLinePlaceholder":273},[250,1616,1617,1619],{"class":252,"line":811},[250,1618,127],{"class":263},[250,1620,1621],{"class":266}," CGO_ENABLED=0 GOOS=linux go build \\\n",[250,1623,1624,1627,1630],{"class":252,"line":819},[250,1625,1626],{"class":266},"    -ldflags=",[250,1628,1629],{"class":595},"\"-s -w\"",[250,1631,1632],{"class":266}," \\\n",[250,1634,1635],{"class":252,"line":824},[250,1636,1637],{"class":266},"    -o \u002Fapp\u002Fserver .\u002Fcmd\u002Fserver\n",[250,1639,1640],{"class":252,"line":832},[250,1641,274],{"emptyLinePlaceholder":273},[250,1643,1644],{"class":252,"line":837},[250,1645,1646],{"class":256},"# === Final stage ===\n",[250,1648,1649,1651],{"class":252,"line":987},[250,1650,67],{"class":263},[250,1652,984],{"class":266},[250,1654,1655],{"class":252,"line":992},[250,1656,274],{"emptyLinePlaceholder":273},[250,1658,1659,1661],{"class":252,"line":998},[250,1660,127],{"class":263},[250,1662,1663],{"class":266}," apk add --no-cache ca-certificates tzdata && \\\n",[250,1665,1666],{"class":252,"line":1006},[250,1667,1668],{"class":266},"    addgroup -S appgroup && adduser -S appuser -G appgroup\n",[250,1670,1671],{"class":252,"line":1011},[250,1672,274],{"emptyLinePlaceholder":273},[250,1674,1675,1677],{"class":252,"line":1017},[250,1676,97],{"class":263},[250,1678,1022],{"class":266},[250,1680,1681],{"class":252,"line":1025},[250,1682,274],{"emptyLinePlaceholder":273},[250,1684,1685,1687],{"class":252,"line":1030},[250,1686,1456],{"class":263},[250,1688,1459],{"class":266},[250,1690,1691],{"class":252,"line":1037},[250,1692,274],{"emptyLinePlaceholder":273},[250,1694,1695,1697],{"class":252,"line":1042},[250,1696,157],{"class":263},[250,1698,829],{"class":266},[250,1700,1702],{"class":252,"line":1701},28,[250,1703,274],{"emptyLinePlaceholder":273},[250,1705,1707,1709,1711,1713],{"class":252,"line":1706},29,[250,1708,172],{"class":263},[250,1710,592],{"class":266},[250,1712,596],{"class":595},[250,1714,599],{"class":266},[15,1716,1717],{},"Этот Dockerfile:",[1061,1719,1720,1723,1726,1729,1732,1735],{},[1064,1721,1722],{},"Использует multi-stage build (минимальный размер)",[1064,1724,1725],{},"Кэширует зависимости отдельно от кода (быстрая пересборка)",[1064,1727,1728],{},"Создаёт статический бинарник (CGO_ENABLED=0)",[1064,1730,1731],{},"Убирает отладочную информацию (-ldflags=\"-s -w\")",[1064,1733,1734],{},"Запускает процесс от непривилегированного пользователя",[1064,1736,1737],{},"Включает CA-сертификаты (для HTTPS) и tzdata (для работы с часовыми поясами)",[21,1739],{},[24,1741,1743],{"id":1742},"docker-compose-оркестрация-на-одной-машине","Docker Compose — оркестрация на одной машине",[32,1745,1747],{"id":1746},"зачем-нужен-compose","Зачем нужен Compose",[15,1749,1750,1751,1753],{},"Реальное приложение редко работает в одиночку. Типичный Go-сервис зависит от PostgreSQL, Redis, может быть Kafka или RabbitMQ. Запускать каждый компонент вручную через ",[65,1752,193],{}," с десятком флагов — неудобно и ненадёжно.",[15,1755,1756],{},"Docker Compose решает три проблемы:",[15,1758,1759,1762,1763,1766],{},[571,1760,1761],{},"Декларативное описание."," Вся инфраструктура описана в одном YAML-файле. Новый разработчик клонирует репозиторий, запускает ",[65,1764,1765],{},"docker compose up"," и получает полностью рабочее окружение.",[15,1768,1769,1772],{},[571,1770,1771],{},"Оркестрация."," Compose поднимает все сервисы одной командой, создаёт для них общую сеть, управляет порядком запуска и остановки.",[15,1774,1775,1778,1779,1782],{},[571,1776,1777],{},"Воспроизводимость."," Файл ",[65,1780,1781],{},"docker-compose.yaml"," хранится в Git. Каждый член команды работает с одинаковым окружением.",[32,1784,1786],{"id":1785},"структура-docker-composeyaml","Структура docker-compose.yaml",[241,1788,1792],{"className":1789,"code":1790,"language":1791,"meta":246,"style":246},"language-yaml shiki shiki-themes github-dark","# docker-compose.yaml\n\nservices:\n  # Имя сервиса — используется как hostname в сети Docker\n  app:\n    build: .                        # Собрать образ из Dockerfile в текущей директории\n    ports:\n      - \"8080:8080\"                 # Проброс порта: хост:контейнер\n    environment:\n      - DB_HOST=postgres            # Имя сервиса = hostname в сети\n      - DB_PORT=5432\n      - DB_USER=app\n      - DB_PASSWORD=secret\n      - DB_NAME=mydb\n    depends_on:\n      - postgres\n      - redis\n\n  postgres:\n    image: postgres:15-alpine       # Готовый образ из Docker Hub\n    environment:\n      POSTGRES_USER: app\n      POSTGRES_PASSWORD: secret\n      POSTGRES_DB: mydb\n    volumes:\n      - pgdata:\u002Fvar\u002Flib\u002Fpostgresql\u002Fdata\n    ports:\n      - \"5432:5432\"\n\n  redis:\n    image: redis:7-alpine\n    ports:\n      - \"6379:6379\"\n\nvolumes:\n  pgdata:                           # Объявление named volume\n","yaml",[65,1793,1794,1799,1803,1812,1817,1824,1839,1846,1857,1864,1874,1881,1888,1895,1902,1909,1916,1923,1927,1934,1947,1953,1963,1973,1983,1990,1997,2003,2010,2014,2022,2032,2039,2047,2052,2060],{"__ignoreMap":246},[250,1795,1796],{"class":252,"line":253},[250,1797,1798],{"class":256},"# docker-compose.yaml\n",[250,1800,1801],{"class":252,"line":260},[250,1802,274],{"emptyLinePlaceholder":273},[250,1804,1805,1809],{"class":252,"line":270},[250,1806,1808],{"class":1807},"s4JwU","services",[250,1810,1811],{"class":266},":\n",[250,1813,1814],{"class":252,"line":277},[250,1815,1816],{"class":256},"  # Имя сервиса — используется как hostname в сети Docker\n",[250,1818,1819,1822],{"class":252,"line":283},[250,1820,1821],{"class":1807},"  app",[250,1823,1811],{"class":266},[250,1825,1826,1829,1832,1836],{"class":252,"line":291},[250,1827,1828],{"class":1807},"    build",[250,1830,1831],{"class":266},": ",[250,1833,1835],{"class":1834},"sDLfK",".",[250,1837,1838],{"class":256},"                        # Собрать образ из Dockerfile в текущей директории\n",[250,1840,1841,1844],{"class":252,"line":296},[250,1842,1843],{"class":1807},"    ports",[250,1845,1811],{"class":266},[250,1847,1848,1851,1854],{"class":252,"line":302},[250,1849,1850],{"class":266},"      - ",[250,1852,1853],{"class":595},"\"8080:8080\"",[250,1855,1856],{"class":256},"                 # Проброс порта: хост:контейнер\n",[250,1858,1859,1862],{"class":252,"line":310},[250,1860,1861],{"class":1807},"    environment",[250,1863,1811],{"class":266},[250,1865,1866,1868,1871],{"class":252,"line":315},[250,1867,1850],{"class":266},[250,1869,1870],{"class":595},"DB_HOST=postgres",[250,1872,1873],{"class":256},"            # Имя сервиса = hostname в сети\n",[250,1875,1876,1878],{"class":252,"line":321},[250,1877,1850],{"class":266},[250,1879,1880],{"class":595},"DB_PORT=5432\n",[250,1882,1883,1885],{"class":252,"line":805},[250,1884,1850],{"class":266},[250,1886,1887],{"class":595},"DB_USER=app\n",[250,1889,1890,1892],{"class":252,"line":811},[250,1891,1850],{"class":266},[250,1893,1894],{"class":595},"DB_PASSWORD=secret\n",[250,1896,1897,1899],{"class":252,"line":819},[250,1898,1850],{"class":266},[250,1900,1901],{"class":595},"DB_NAME=mydb\n",[250,1903,1904,1907],{"class":252,"line":824},[250,1905,1906],{"class":1807},"    depends_on",[250,1908,1811],{"class":266},[250,1910,1911,1913],{"class":252,"line":832},[250,1912,1850],{"class":266},[250,1914,1915],{"class":595},"postgres\n",[250,1917,1918,1920],{"class":252,"line":837},[250,1919,1850],{"class":266},[250,1921,1922],{"class":595},"redis\n",[250,1924,1925],{"class":252,"line":987},[250,1926,274],{"emptyLinePlaceholder":273},[250,1928,1929,1932],{"class":252,"line":992},[250,1930,1931],{"class":1807},"  postgres",[250,1933,1811],{"class":266},[250,1935,1936,1939,1941,1944],{"class":252,"line":998},[250,1937,1938],{"class":1807},"    image",[250,1940,1831],{"class":266},[250,1942,1943],{"class":595},"postgres:15-alpine",[250,1945,1946],{"class":256},"       # Готовый образ из Docker Hub\n",[250,1948,1949,1951],{"class":252,"line":1006},[250,1950,1861],{"class":1807},[250,1952,1811],{"class":266},[250,1954,1955,1958,1960],{"class":252,"line":1011},[250,1956,1957],{"class":1807},"      POSTGRES_USER",[250,1959,1831],{"class":266},[250,1961,1962],{"class":595},"app\n",[250,1964,1965,1968,1970],{"class":252,"line":1017},[250,1966,1967],{"class":1807},"      POSTGRES_PASSWORD",[250,1969,1831],{"class":266},[250,1971,1972],{"class":595},"secret\n",[250,1974,1975,1978,1980],{"class":252,"line":1025},[250,1976,1977],{"class":1807},"      POSTGRES_DB",[250,1979,1831],{"class":266},[250,1981,1982],{"class":595},"mydb\n",[250,1984,1985,1988],{"class":252,"line":1030},[250,1986,1987],{"class":1807},"    volumes",[250,1989,1811],{"class":266},[250,1991,1992,1994],{"class":252,"line":1037},[250,1993,1850],{"class":266},[250,1995,1996],{"class":595},"pgdata:\u002Fvar\u002Flib\u002Fpostgresql\u002Fdata\n",[250,1998,1999,2001],{"class":252,"line":1042},[250,2000,1843],{"class":1807},[250,2002,1811],{"class":266},[250,2004,2005,2007],{"class":252,"line":1701},[250,2006,1850],{"class":266},[250,2008,2009],{"class":595},"\"5432:5432\"\n",[250,2011,2012],{"class":252,"line":1706},[250,2013,274],{"emptyLinePlaceholder":273},[250,2015,2017,2020],{"class":252,"line":2016},30,[250,2018,2019],{"class":1807},"  redis",[250,2021,1811],{"class":266},[250,2023,2025,2027,2029],{"class":252,"line":2024},31,[250,2026,1938],{"class":1807},[250,2028,1831],{"class":266},[250,2030,2031],{"class":595},"redis:7-alpine\n",[250,2033,2035,2037],{"class":252,"line":2034},32,[250,2036,1843],{"class":1807},[250,2038,1811],{"class":266},[250,2040,2042,2044],{"class":252,"line":2041},33,[250,2043,1850],{"class":266},[250,2045,2046],{"class":595},"\"6379:6379\"\n",[250,2048,2050],{"class":252,"line":2049},34,[250,2051,274],{"emptyLinePlaceholder":273},[250,2053,2055,2058],{"class":252,"line":2054},35,[250,2056,2057],{"class":1807},"volumes",[250,2059,1811],{"class":266},[250,2061,2063,2066,2069],{"class":252,"line":2062},36,[250,2064,2065],{"class":1807},"  pgdata",[250,2067,2068],{"class":266},":                           ",[250,2070,2071],{"class":256},"# Объявление named volume\n",[32,2073,2075],{"id":2074},"ключевые-секции","Ключевые секции",[2077,2078,1808],"h4",{"id":1808},[15,2080,2081,2082,623,2085,623,2088,2091,2092,2094],{},"Каждый сервис — это один контейнер. Имя сервиса (",[65,2083,2084],{},"app",[65,2086,2087],{},"postgres",[65,2089,2090],{},"redis",") становится DNS-именем в общей сети Docker. Ваше Go-приложение обращается к PostgreSQL по hostname ",[65,2093,2087],{},", а не по IP-адресу.",[2077,2096,2098],{"id":2097},"build-vs-image","build vs image",[15,2100,2101,2104,2105,2108,2109,1835],{},[65,2102,2103],{},"build: ."," говорит Compose собрать образ из Dockerfile. ",[65,2106,2107],{},"image: postgres:15-alpine"," — использовать готовый образ из реестра. Можно указать кастомный Dockerfile через ",[65,2110,2111],{},"build: { context: ., dockerfile: Dockerfile.prod }",[2077,2113,2114],{"id":2114},"depends_on",[15,2116,2117,2119,2120,2122,2123,2125],{},[65,2118,2114],{}," управляет порядком запуска: ",[65,2121,2087],{}," запустится раньше ",[65,2124,2084],{},". Но это не гарантирует, что PostgreSQL будет готов принимать соединения — только что контейнер запущен. Для проверки готовности нужны healthchecks:",[241,2127,2129],{"className":1789,"code":2128,"language":1791,"meta":246,"style":246},"services:\n  app:\n    depends_on:\n      postgres:\n        condition: service_healthy\n\n  postgres:\n    image: postgres:15-alpine\n    healthcheck:\n      test: [\"CMD-SHELL\", \"pg_isready -U app\"]\n      interval: 5s\n      timeout: 5s\n      retries: 5\n",[65,2130,2131,2137,2143,2149,2156,2166,2170,2176,2185,2192,2210,2220,2229],{"__ignoreMap":246},[250,2132,2133,2135],{"class":252,"line":253},[250,2134,1808],{"class":1807},[250,2136,1811],{"class":266},[250,2138,2139,2141],{"class":252,"line":260},[250,2140,1821],{"class":1807},[250,2142,1811],{"class":266},[250,2144,2145,2147],{"class":252,"line":270},[250,2146,1906],{"class":1807},[250,2148,1811],{"class":266},[250,2150,2151,2154],{"class":252,"line":277},[250,2152,2153],{"class":1807},"      postgres",[250,2155,1811],{"class":266},[250,2157,2158,2161,2163],{"class":252,"line":283},[250,2159,2160],{"class":1807},"        condition",[250,2162,1831],{"class":266},[250,2164,2165],{"class":595},"service_healthy\n",[250,2167,2168],{"class":252,"line":291},[250,2169,274],{"emptyLinePlaceholder":273},[250,2171,2172,2174],{"class":252,"line":296},[250,2173,1931],{"class":1807},[250,2175,1811],{"class":266},[250,2177,2178,2180,2182],{"class":252,"line":302},[250,2179,1938],{"class":1807},[250,2181,1831],{"class":266},[250,2183,2184],{"class":595},"postgres:15-alpine\n",[250,2186,2187,2190],{"class":252,"line":310},[250,2188,2189],{"class":1807},"    healthcheck",[250,2191,1811],{"class":266},[250,2193,2194,2197,2200,2203,2205,2208],{"class":252,"line":315},[250,2195,2196],{"class":1807},"      test",[250,2198,2199],{"class":266},": [",[250,2201,2202],{"class":595},"\"CMD-SHELL\"",[250,2204,623],{"class":266},[250,2206,2207],{"class":595},"\"pg_isready -U app\"",[250,2209,599],{"class":266},[250,2211,2212,2215,2217],{"class":252,"line":321},[250,2213,2214],{"class":1807},"      interval",[250,2216,1831],{"class":266},[250,2218,2219],{"class":595},"5s\n",[250,2221,2222,2225,2227],{"class":252,"line":805},[250,2223,2224],{"class":1807},"      timeout",[250,2226,1831],{"class":266},[250,2228,2219],{"class":595},[250,2230,2231,2234,2236],{"class":252,"line":811},[250,2232,2233],{"class":1807},"      retries",[250,2235,1831],{"class":266},[250,2237,2238],{"class":1834},"5\n",[15,2240,2241,2242,2244],{},"Теперь ",[65,2243,2084],{}," запустится только когда PostgreSQL реально готов принимать соединения, а не просто когда контейнер стартовал.",[2077,2246,2247],{"id":2247},"networks",[15,2249,2250],{},"По умолчанию Compose создаёт одну сеть для всех сервисов в файле. Сервисы видят друг друга по именам. Но можно создать изолированные сети:",[241,2252,2254],{"className":1789,"code":2253,"language":1791,"meta":246,"style":246},"services:\n  app:\n    networks:\n      - frontend\n      - backend\n\n  postgres:\n    networks:\n      - backend          # postgres доступен только из backend-сети\n\n  nginx:\n    networks:\n      - frontend         # nginx не видит postgres напрямую\n\nnetworks:\n  frontend:\n  backend:\n",[65,2255,2256,2262,2268,2275,2282,2289,2293,2299,2305,2315,2319,2326,2332,2342,2346,2352,2359],{"__ignoreMap":246},[250,2257,2258,2260],{"class":252,"line":253},[250,2259,1808],{"class":1807},[250,2261,1811],{"class":266},[250,2263,2264,2266],{"class":252,"line":260},[250,2265,1821],{"class":1807},[250,2267,1811],{"class":266},[250,2269,2270,2273],{"class":252,"line":270},[250,2271,2272],{"class":1807},"    networks",[250,2274,1811],{"class":266},[250,2276,2277,2279],{"class":252,"line":277},[250,2278,1850],{"class":266},[250,2280,2281],{"class":595},"frontend\n",[250,2283,2284,2286],{"class":252,"line":283},[250,2285,1850],{"class":266},[250,2287,2288],{"class":595},"backend\n",[250,2290,2291],{"class":252,"line":291},[250,2292,274],{"emptyLinePlaceholder":273},[250,2294,2295,2297],{"class":252,"line":296},[250,2296,1931],{"class":1807},[250,2298,1811],{"class":266},[250,2300,2301,2303],{"class":252,"line":302},[250,2302,2272],{"class":1807},[250,2304,1811],{"class":266},[250,2306,2307,2309,2312],{"class":252,"line":310},[250,2308,1850],{"class":266},[250,2310,2311],{"class":595},"backend",[250,2313,2314],{"class":256},"          # postgres доступен только из backend-сети\n",[250,2316,2317],{"class":252,"line":315},[250,2318,274],{"emptyLinePlaceholder":273},[250,2320,2321,2324],{"class":252,"line":321},[250,2322,2323],{"class":1807},"  nginx",[250,2325,1811],{"class":266},[250,2327,2328,2330],{"class":252,"line":805},[250,2329,2272],{"class":1807},[250,2331,1811],{"class":266},[250,2333,2334,2336,2339],{"class":252,"line":811},[250,2335,1850],{"class":266},[250,2337,2338],{"class":595},"frontend",[250,2340,2341],{"class":256},"         # nginx не видит postgres напрямую\n",[250,2343,2344],{"class":252,"line":819},[250,2345,274],{"emptyLinePlaceholder":273},[250,2347,2348,2350],{"class":252,"line":824},[250,2349,2247],{"class":1807},[250,2351,1811],{"class":266},[250,2353,2354,2357],{"class":252,"line":832},[250,2355,2356],{"class":1807},"  frontend",[250,2358,1811],{"class":266},[250,2360,2361,2364],{"class":252,"line":837},[250,2362,2363],{"class":1807},"  backend",[250,2365,1811],{"class":266},[15,2367,2368],{},"Это повышает безопасность: nginx не имеет прямого доступа к базе данных.",[2077,2370,2057],{"id":2057},[15,2372,2373],{},"Volumes в Compose работают так же, как в обычном Docker, но с удобным синтаксисом:",[241,2375,2377],{"className":1789,"code":2376,"language":1791,"meta":246,"style":246},"services:\n  postgres:\n    volumes:\n      # Named volume — данные БД\n      - pgdata:\u002Fvar\u002Flib\u002Fpostgresql\u002Fdata\n      # Bind mount — инициализационные скрипты\n      - .\u002Finit.sql:\u002Fdocker-entrypoint-initdb.d\u002Finit.sql\n\n  app:\n    volumes:\n      # Bind mount — исходники для hot reload в dev\n      - .:\u002Fapp\n\nvolumes:\n  pgdata:              # Объявление named volume\n",[65,2378,2379,2385,2391,2397,2402,2408,2413,2420,2424,2430,2436,2441,2448,2452,2458],{"__ignoreMap":246},[250,2380,2381,2383],{"class":252,"line":253},[250,2382,1808],{"class":1807},[250,2384,1811],{"class":266},[250,2386,2387,2389],{"class":252,"line":260},[250,2388,1931],{"class":1807},[250,2390,1811],{"class":266},[250,2392,2393,2395],{"class":252,"line":270},[250,2394,1987],{"class":1807},[250,2396,1811],{"class":266},[250,2398,2399],{"class":252,"line":277},[250,2400,2401],{"class":256},"      # Named volume — данные БД\n",[250,2403,2404,2406],{"class":252,"line":283},[250,2405,1850],{"class":266},[250,2407,1996],{"class":595},[250,2409,2410],{"class":252,"line":291},[250,2411,2412],{"class":256},"      # Bind mount — инициализационные скрипты\n",[250,2414,2415,2417],{"class":252,"line":296},[250,2416,1850],{"class":266},[250,2418,2419],{"class":595},".\u002Finit.sql:\u002Fdocker-entrypoint-initdb.d\u002Finit.sql\n",[250,2421,2422],{"class":252,"line":302},[250,2423,274],{"emptyLinePlaceholder":273},[250,2425,2426,2428],{"class":252,"line":310},[250,2427,1821],{"class":1807},[250,2429,1811],{"class":266},[250,2431,2432,2434],{"class":252,"line":315},[250,2433,1987],{"class":1807},[250,2435,1811],{"class":266},[250,2437,2438],{"class":252,"line":321},[250,2439,2440],{"class":256},"      # Bind mount — исходники для hot reload в dev\n",[250,2442,2443,2445],{"class":252,"line":805},[250,2444,1850],{"class":266},[250,2446,2447],{"class":595},".:\u002Fapp\n",[250,2449,2450],{"class":252,"line":811},[250,2451,274],{"emptyLinePlaceholder":273},[250,2453,2454,2456],{"class":252,"line":819},[250,2455,2057],{"class":1807},[250,2457,1811],{"class":266},[250,2459,2460,2462,2465],{"class":252,"line":824},[250,2461,2065],{"class":1807},[250,2463,2464],{"class":266},":              ",[250,2466,2071],{"class":256},[15,2468,2469,2470,2473],{},"Named volumes должны быть объявлены в секции ",[65,2471,2472],{},"volumes:"," на верхнем уровне файла.",[2077,2475,2477],{"id":2476},"environment-и-env_file","environment и env_file",[15,2479,2480,2481,2484,2485,2488,2489,2492,2493,2496],{},"Переменные окружения задают inline (",[65,2482,2483],{},"environment: DB_HOST: postgres",") или из файла (",[65,2486,2487],{},"env_file: .env","). Файл ",[65,2490,2491],{},".env"," не коммитится в Git — для команды создайте ",[65,2494,2495],{},".env.example"," с пустыми значениями.",[2077,2498,2499],{"id":2499},"restart",[37,2501,2502,2512],{},[40,2503,2504],{},[43,2505,2506,2509],{},[46,2507,2508],{"align":48},"Политика",[46,2510,2511],{"align":48},"Поведение",[57,2513,2514,2524,2534,2544],{},[43,2515,2516,2521],{},[62,2517,2518],{"align":48},[65,2519,2520],{},"no",[62,2522,2523],{"align":48},"Не перезапускать (по умолчанию)",[43,2525,2526,2531],{},[62,2527,2528],{"align":48},[65,2529,2530],{},"always",[62,2532,2533],{"align":48},"Перезапускать всегда",[43,2535,2536,2541],{},[62,2537,2538],{"align":48},[65,2539,2540],{},"on-failure",[62,2542,2543],{"align":48},"Перезапускать только при ненулевом exit code",[43,2545,2546,2551],{},[62,2547,2548],{"align":48},[65,2549,2550],{},"unless-stopped",[62,2552,2553],{"align":48},"Перезапускать, пока не остановлен вручную",[21,2555],{},[24,2557,2559],{"id":2558},"основные-команды-docker-compose","Основные команды Docker Compose",[32,2561,2563],{"id":2562},"запуск-и-остановка","Запуск и остановка",[241,2565,2569],{"className":2566,"code":2567,"language":2568,"meta":246,"style":246},"language-bash shiki shiki-themes github-dark","# Запуск всех сервисов (с выводом логов в консоль)\ndocker compose up\n\n# Запуск в фоновом режиме\ndocker compose up -d\n\n# Запуск с пересборкой образов\ndocker compose up -d --build\n\n# Запуск конкретного сервиса (и его зависимостей)\ndocker compose up -d postgres\n\n# Остановка всех сервисов (контейнеры сохраняются)\ndocker compose stop\n\n# Остановка и удаление контейнеров и сетей\ndocker compose down\n\n# Остановка, удаление контейнеров, сетей И volumes\ndocker compose down -v\n","bash",[65,2570,2571,2576,2588,2592,2597,2609,2613,2618,2632,2636,2641,2654,2658,2663,2672,2676,2681,2690,2694,2699],{"__ignoreMap":246},[250,2572,2573],{"class":252,"line":253},[250,2574,2575],{"class":256},"# Запуск всех сервисов (с выводом логов в консоль)\n",[250,2577,2578,2582,2585],{"class":252,"line":260},[250,2579,2581],{"class":2580},"svObZ","docker",[250,2583,2584],{"class":595}," compose",[250,2586,2587],{"class":595}," up\n",[250,2589,2590],{"class":252,"line":270},[250,2591,274],{"emptyLinePlaceholder":273},[250,2593,2594],{"class":252,"line":277},[250,2595,2596],{"class":256},"# Запуск в фоновом режиме\n",[250,2598,2599,2601,2603,2606],{"class":252,"line":283},[250,2600,2581],{"class":2580},[250,2602,2584],{"class":595},[250,2604,2605],{"class":595}," up",[250,2607,2608],{"class":1834}," -d\n",[250,2610,2611],{"class":252,"line":291},[250,2612,274],{"emptyLinePlaceholder":273},[250,2614,2615],{"class":252,"line":296},[250,2616,2617],{"class":256},"# Запуск с пересборкой образов\n",[250,2619,2620,2622,2624,2626,2629],{"class":252,"line":302},[250,2621,2581],{"class":2580},[250,2623,2584],{"class":595},[250,2625,2605],{"class":595},[250,2627,2628],{"class":1834}," -d",[250,2630,2631],{"class":1834}," --build\n",[250,2633,2634],{"class":252,"line":310},[250,2635,274],{"emptyLinePlaceholder":273},[250,2637,2638],{"class":252,"line":315},[250,2639,2640],{"class":256},"# Запуск конкретного сервиса (и его зависимостей)\n",[250,2642,2643,2645,2647,2649,2651],{"class":252,"line":321},[250,2644,2581],{"class":2580},[250,2646,2584],{"class":595},[250,2648,2605],{"class":595},[250,2650,2628],{"class":1834},[250,2652,2653],{"class":595}," postgres\n",[250,2655,2656],{"class":252,"line":805},[250,2657,274],{"emptyLinePlaceholder":273},[250,2659,2660],{"class":252,"line":811},[250,2661,2662],{"class":256},"# Остановка всех сервисов (контейнеры сохраняются)\n",[250,2664,2665,2667,2669],{"class":252,"line":819},[250,2666,2581],{"class":2580},[250,2668,2584],{"class":595},[250,2670,2671],{"class":595}," stop\n",[250,2673,2674],{"class":252,"line":824},[250,2675,274],{"emptyLinePlaceholder":273},[250,2677,2678],{"class":252,"line":832},[250,2679,2680],{"class":256},"# Остановка и удаление контейнеров и сетей\n",[250,2682,2683,2685,2687],{"class":252,"line":837},[250,2684,2581],{"class":2580},[250,2686,2584],{"class":595},[250,2688,2689],{"class":595}," down\n",[250,2691,2692],{"class":252,"line":987},[250,2693,274],{"emptyLinePlaceholder":273},[250,2695,2696],{"class":252,"line":992},[250,2697,2698],{"class":256},"# Остановка, удаление контейнеров, сетей И volumes\n",[250,2700,2701,2703,2705,2708],{"class":252,"line":998},[250,2702,2581],{"class":2580},[250,2704,2584],{"class":595},[250,2706,2707],{"class":595}," down",[250,2709,2710],{"class":1834}," -v\n",[15,2712,2713,2714,2717,2718,1831,2721,2723,2724,2726,2727,2730,2731,2733],{},"Разница между ",[65,2715,2716],{},"stop"," и ",[65,2719,2720],{},"down",[65,2722,2716],{}," только останавливает контейнеры, ",[65,2725,2720],{}," удаляет их и созданные сети. Флаг ",[65,2728,2729],{},"-v"," в ",[65,2732,2720],{}," также удаляет volumes — используйте с осторожностью, чтобы не потерять данные БД.",[32,2735,2737],{"id":2736},"логи","Логи",[241,2739,2741],{"className":2566,"code":2740,"language":2568,"meta":246,"style":246},"# Логи всех сервисов\ndocker compose logs\n\n# Логи конкретного сервиса\ndocker compose logs app\n\n# Логи в режиме follow (аналог tail -f)\ndocker compose logs -f app\n\n# Последние 100 строк логов\ndocker compose logs --tail=100 app\n",[65,2742,2743,2748,2757,2761,2766,2778,2782,2787,2800,2804,2809],{"__ignoreMap":246},[250,2744,2745],{"class":252,"line":253},[250,2746,2747],{"class":256},"# Логи всех сервисов\n",[250,2749,2750,2752,2754],{"class":252,"line":260},[250,2751,2581],{"class":2580},[250,2753,2584],{"class":595},[250,2755,2756],{"class":595}," logs\n",[250,2758,2759],{"class":252,"line":270},[250,2760,274],{"emptyLinePlaceholder":273},[250,2762,2763],{"class":252,"line":277},[250,2764,2765],{"class":256},"# Логи конкретного сервиса\n",[250,2767,2768,2770,2772,2775],{"class":252,"line":283},[250,2769,2581],{"class":2580},[250,2771,2584],{"class":595},[250,2773,2774],{"class":595}," logs",[250,2776,2777],{"class":595}," app\n",[250,2779,2780],{"class":252,"line":291},[250,2781,274],{"emptyLinePlaceholder":273},[250,2783,2784],{"class":252,"line":296},[250,2785,2786],{"class":256},"# Логи в режиме follow (аналог tail -f)\n",[250,2788,2789,2791,2793,2795,2798],{"class":252,"line":302},[250,2790,2581],{"class":2580},[250,2792,2584],{"class":595},[250,2794,2774],{"class":595},[250,2796,2797],{"class":1834}," -f",[250,2799,2777],{"class":595},[250,2801,2802],{"class":252,"line":310},[250,2803,274],{"emptyLinePlaceholder":273},[250,2805,2806],{"class":252,"line":315},[250,2807,2808],{"class":256},"# Последние 100 строк логов\n",[250,2810,2811,2813,2815,2817,2820],{"class":252,"line":321},[250,2812,2581],{"class":2580},[250,2814,2584],{"class":595},[250,2816,2774],{"class":595},[250,2818,2819],{"class":1834}," --tail=100",[250,2821,2777],{"class":595},[32,2823,2825],{"id":2824},"состояние","Состояние",[241,2827,2829],{"className":2566,"code":2828,"language":2568,"meta":246,"style":246},"# Статус сервисов\ndocker compose ps\n\n# Запущенные процессы внутри сервисов\ndocker compose top\n",[65,2830,2831,2836,2845,2849,2854],{"__ignoreMap":246},[250,2832,2833],{"class":252,"line":253},[250,2834,2835],{"class":256},"# Статус сервисов\n",[250,2837,2838,2840,2842],{"class":252,"line":260},[250,2839,2581],{"class":2580},[250,2841,2584],{"class":595},[250,2843,2844],{"class":595}," ps\n",[250,2846,2847],{"class":252,"line":270},[250,2848,274],{"emptyLinePlaceholder":273},[250,2850,2851],{"class":252,"line":277},[250,2852,2853],{"class":256},"# Запущенные процессы внутри сервисов\n",[250,2855,2856,2858,2860],{"class":252,"line":283},[250,2857,2581],{"class":2580},[250,2859,2584],{"class":595},[250,2861,2862],{"class":595}," top\n",[32,2864,2866],{"id":2865},"выполнение-команд","Выполнение команд",[241,2868,2870],{"className":2566,"code":2869,"language":2568,"meta":246,"style":246},"# Выполнить команду внутри запущенного контейнера\ndocker compose exec app \u002Fbin\u002Fsh\n\n# Запустить одноразовый контейнер сервиса\ndocker compose run --rm app go test .\u002F...\n",[65,2871,2872,2877,2892,2896,2901],{"__ignoreMap":246},[250,2873,2874],{"class":252,"line":253},[250,2875,2876],{"class":256},"# Выполнить команду внутри запущенного контейнера\n",[250,2878,2879,2881,2883,2886,2889],{"class":252,"line":260},[250,2880,2581],{"class":2580},[250,2882,2584],{"class":595},[250,2884,2885],{"class":595}," exec",[250,2887,2888],{"class":595}," app",[250,2890,2891],{"class":595}," \u002Fbin\u002Fsh\n",[250,2893,2894],{"class":252,"line":270},[250,2895,274],{"emptyLinePlaceholder":273},[250,2897,2898],{"class":252,"line":277},[250,2899,2900],{"class":256},"# Запустить одноразовый контейнер сервиса\n",[250,2902,2903,2905,2907,2910,2913,2915,2918,2921],{"class":252,"line":283},[250,2904,2581],{"class":2580},[250,2906,2584],{"class":595},[250,2908,2909],{"class":595}," run",[250,2911,2912],{"class":1834}," --rm",[250,2914,2888],{"class":595},[250,2916,2917],{"class":595}," go",[250,2919,2920],{"class":595}," test",[250,2922,2923],{"class":595}," .\u002F...\n",[15,2925,2713,2926,2717,2928,1831,2931,2933,2934,2936,2937,2940],{},[65,2927,1190],{},[65,2929,2930],{},"run",[65,2932,1190],{}," выполняет команду в уже работающем контейнере, ",[65,2935,2930],{}," создаёт новый контейнер. ",[65,2938,2939],{},"--rm"," удаляет контейнер после завершения.",[32,2942,2944],{"id":2943},"пересборка","Пересборка",[241,2946,2948],{"className":2566,"code":2947,"language":2568,"meta":246,"style":246},"# Пересобрать образы без запуска\ndocker compose build\n\n# Пересобрать без кэша (полная пересборка)\ndocker compose build --no-cache\n",[65,2949,2950,2955,2964,2968,2973],{"__ignoreMap":246},[250,2951,2952],{"class":252,"line":253},[250,2953,2954],{"class":256},"# Пересобрать образы без запуска\n",[250,2956,2957,2959,2961],{"class":252,"line":260},[250,2958,2581],{"class":2580},[250,2960,2584],{"class":595},[250,2962,2963],{"class":595}," build\n",[250,2965,2966],{"class":252,"line":270},[250,2967,274],{"emptyLinePlaceholder":273},[250,2969,2970],{"class":252,"line":277},[250,2971,2972],{"class":256},"# Пересобрать без кэша (полная пересборка)\n",[250,2974,2975,2977,2979,2982],{"class":252,"line":283},[250,2976,2581],{"class":2580},[250,2978,2584],{"class":595},[250,2980,2981],{"class":595}," build",[250,2983,2984],{"class":1834}," --no-cache\n",[21,2986],{},[24,2988,2990],{"id":2989},"docker-compose-для-разработки-vs-production","Docker Compose для разработки vs production",[15,2992,2993],{},"Compose-файлы для dev и production обычно отличаются. Типичный подход — базовый файл + переопределение:",[241,2995,2997],{"className":1789,"code":2996,"language":1791,"meta":246,"style":246},"# docker-compose.override.yaml (dev — применяется автоматически)\nservices:\n  app:\n    volumes:\n      - .:\u002Fapp                     # Монтирование исходников для hot reload\n    ports:\n      - \"8080:8080\"\n    command: [\"go\", \"run\", \".\u002Fcmd\u002Fserver\"]\n  postgres:\n    ports:\n      - \"5432:5432\"                # Доступ к БД с хоста\n",[65,2998,2999,3004,3010,3016,3022,3032,3038,3045,3067,3073,3079],{"__ignoreMap":246},[250,3000,3001],{"class":252,"line":253},[250,3002,3003],{"class":256},"# docker-compose.override.yaml (dev — применяется автоматически)\n",[250,3005,3006,3008],{"class":252,"line":260},[250,3007,1808],{"class":1807},[250,3009,1811],{"class":266},[250,3011,3012,3014],{"class":252,"line":270},[250,3013,1821],{"class":1807},[250,3015,1811],{"class":266},[250,3017,3018,3020],{"class":252,"line":277},[250,3019,1987],{"class":1807},[250,3021,1811],{"class":266},[250,3023,3024,3026,3029],{"class":252,"line":283},[250,3025,1850],{"class":266},[250,3027,3028],{"class":595},".:\u002Fapp",[250,3030,3031],{"class":256},"                     # Монтирование исходников для hot reload\n",[250,3033,3034,3036],{"class":252,"line":291},[250,3035,1843],{"class":1807},[250,3037,1811],{"class":266},[250,3039,3040,3042],{"class":252,"line":296},[250,3041,1850],{"class":266},[250,3043,3044],{"class":595},"\"8080:8080\"\n",[250,3046,3047,3050,3052,3055,3057,3060,3062,3065],{"class":252,"line":302},[250,3048,3049],{"class":1807},"    command",[250,3051,2199],{"class":266},[250,3053,3054],{"class":595},"\"go\"",[250,3056,623],{"class":266},[250,3058,3059],{"class":595},"\"run\"",[250,3061,623],{"class":266},[250,3063,3064],{"class":595},"\".\u002Fcmd\u002Fserver\"",[250,3066,599],{"class":266},[250,3068,3069,3071],{"class":252,"line":310},[250,3070,1931],{"class":1807},[250,3072,1811],{"class":266},[250,3074,3075,3077],{"class":252,"line":315},[250,3076,1843],{"class":1807},[250,3078,1811],{"class":266},[250,3080,3081,3083,3086],{"class":252,"line":321},[250,3082,1850],{"class":266},[250,3084,3085],{"class":595},"\"5432:5432\"",[250,3087,3088],{"class":256},"                # Доступ к БД с хоста\n",[241,3090,3092],{"className":1789,"code":3091,"language":1791,"meta":246,"style":246},"# docker-compose.prod.yaml (production)\nservices:\n  app:\n    image: registry.company.com\u002Fmy-app:${VERSION}\n    restart: always\n    deploy:\n      resources:\n        limits:\n          memory: 512M\n          cpus: \"1.0\"\n  postgres:\n    restart: always\n    # Порт НЕ пробрасывается — доступ только из сети Docker\n",[65,3093,3094,3099,3105,3111,3120,3130,3137,3144,3151,3161,3171,3177,3185],{"__ignoreMap":246},[250,3095,3096],{"class":252,"line":253},[250,3097,3098],{"class":256},"# docker-compose.prod.yaml (production)\n",[250,3100,3101,3103],{"class":252,"line":260},[250,3102,1808],{"class":1807},[250,3104,1811],{"class":266},[250,3106,3107,3109],{"class":252,"line":270},[250,3108,1821],{"class":1807},[250,3110,1811],{"class":266},[250,3112,3113,3115,3117],{"class":252,"line":277},[250,3114,1938],{"class":1807},[250,3116,1831],{"class":266},[250,3118,3119],{"class":595},"registry.company.com\u002Fmy-app:${VERSION}\n",[250,3121,3122,3125,3127],{"class":252,"line":283},[250,3123,3124],{"class":1807},"    restart",[250,3126,1831],{"class":266},[250,3128,3129],{"class":595},"always\n",[250,3131,3132,3135],{"class":252,"line":291},[250,3133,3134],{"class":1807},"    deploy",[250,3136,1811],{"class":266},[250,3138,3139,3142],{"class":252,"line":296},[250,3140,3141],{"class":1807},"      resources",[250,3143,1811],{"class":266},[250,3145,3146,3149],{"class":252,"line":302},[250,3147,3148],{"class":1807},"        limits",[250,3150,1811],{"class":266},[250,3152,3153,3156,3158],{"class":252,"line":310},[250,3154,3155],{"class":1807},"          memory",[250,3157,1831],{"class":266},[250,3159,3160],{"class":595},"512M\n",[250,3162,3163,3166,3168],{"class":252,"line":315},[250,3164,3165],{"class":1807},"          cpus",[250,3167,1831],{"class":266},[250,3169,3170],{"class":595},"\"1.0\"\n",[250,3172,3173,3175],{"class":252,"line":321},[250,3174,1931],{"class":1807},[250,3176,1811],{"class":266},[250,3178,3179,3181,3183],{"class":252,"line":805},[250,3180,3124],{"class":1807},[250,3182,1831],{"class":266},[250,3184,3129],{"class":595},[250,3186,3187],{"class":252,"line":811},[250,3188,3189],{"class":256},"    # Порт НЕ пробрасывается — доступ только из сети Docker\n",[241,3191,3193],{"className":2566,"code":3192,"language":2568,"meta":246,"style":246},"# Dev (берёт docker-compose.yaml + docker-compose.override.yaml автоматически)\ndocker compose up\n\n# Production\ndocker compose -f docker-compose.yaml -f docker-compose.prod.yaml up -d\n",[65,3194,3195,3200,3208,3212,3217],{"__ignoreMap":246},[250,3196,3197],{"class":252,"line":253},[250,3198,3199],{"class":256},"# Dev (берёт docker-compose.yaml + docker-compose.override.yaml автоматически)\n",[250,3201,3202,3204,3206],{"class":252,"line":260},[250,3203,2581],{"class":2580},[250,3205,2584],{"class":595},[250,3207,2587],{"class":595},[250,3209,3210],{"class":252,"line":270},[250,3211,274],{"emptyLinePlaceholder":273},[250,3213,3214],{"class":252,"line":277},[250,3215,3216],{"class":256},"# Production\n",[250,3218,3219,3221,3223,3225,3228,3230,3233,3235],{"class":252,"line":283},[250,3220,2581],{"class":2580},[250,3222,2584],{"class":595},[250,3224,2797],{"class":1834},[250,3226,3227],{"class":595}," docker-compose.yaml",[250,3229,2797],{"class":1834},[250,3231,3232],{"class":595}," docker-compose.prod.yaml",[250,3234,2605],{"class":595},[250,3236,2608],{"class":1834},[21,3238],{},[24,3240,3242],{"id":3241},"типичные-ошибки","Типичные ошибки",[32,3244,3246],{"id":3245},"_1-забыли-dockerignore","1. Забыли .dockerignore",[15,3248,1391,3249,3251,3252,623,3254,3257],{},[65,3250,1377],{}," в контекст сборки попадает ",[65,3253,1397],{},[65,3255,3256],{},"vendor\u002F",", тестовые данные. Это замедляет сборку и раздувает образ.",[32,3259,3261],{"id":3260},"_2-зависимости-не-кэшируются","2. Зависимости не кэшируются",[241,3263,3265],{"className":243,"code":3264,"language":245,"meta":246,"style":246},"# Плохо — каждое изменение кода перекачивает зависимости\nCOPY . .\nRUN go mod download && go build -o server .\u002Fcmd\u002Fserver\n\n# Хорошо — зависимости кэшируются\nCOPY go.mod go.sum .\u002F\nRUN go mod download\nCOPY . .\nRUN go build -o server .\u002Fcmd\u002Fserver\n",[65,3266,3267,3272,3278,3285,3289,3294,3300,3306,3312],{"__ignoreMap":246},[250,3268,3269],{"class":252,"line":253},[250,3270,3271],{"class":256},"# Плохо — каждое изменение кода перекачивает зависимости\n",[250,3273,3274,3276],{"class":252,"line":260},[250,3275,97],{"class":263},[250,3277,437],{"class":266},[250,3279,3280,3282],{"class":252,"line":270},[250,3281,127],{"class":263},[250,3283,3284],{"class":266}," go mod download && go build -o server .\u002Fcmd\u002Fserver\n",[250,3286,3287],{"class":252,"line":277},[250,3288,274],{"emptyLinePlaceholder":273},[250,3290,3291],{"class":252,"line":283},[250,3292,3293],{"class":256},"# Хорошо — зависимости кэшируются\n",[250,3295,3296,3298],{"class":252,"line":291},[250,3297,97],{"class":263},[250,3299,430],{"class":266},[250,3301,3302,3304],{"class":252,"line":296},[250,3303,127],{"class":263},[250,3305,507],{"class":266},[250,3307,3308,3310],{"class":252,"line":302},[250,3309,97],{"class":263},[250,3311,437],{"class":266},[250,3313,3314,3316],{"class":252,"line":310},[250,3315,127],{"class":263},[250,3317,3318],{"class":266}," go build -o server .\u002Fcmd\u002Fserver\n",[32,3320,3322],{"id":3321},"_3-depends_on-без-healthcheck","3. depends_on без healthcheck",[241,3324,3326],{"className":1789,"code":3325,"language":1791,"meta":246,"style":246},"# Плохо — приложение падает, потому что PostgreSQL ещё не готов\ndepends_on:\n  - postgres\n\n# Хорошо — ждём реальной готовности\ndepends_on:\n  postgres:\n    condition: service_healthy\n",[65,3327,3328,3333,3339,3346,3350,3355,3361,3367],{"__ignoreMap":246},[250,3329,3330],{"class":252,"line":253},[250,3331,3332],{"class":256},"# Плохо — приложение падает, потому что PostgreSQL ещё не готов\n",[250,3334,3335,3337],{"class":252,"line":260},[250,3336,2114],{"class":1807},[250,3338,1811],{"class":266},[250,3340,3341,3344],{"class":252,"line":270},[250,3342,3343],{"class":266},"  - ",[250,3345,1915],{"class":595},[250,3347,3348],{"class":252,"line":277},[250,3349,274],{"emptyLinePlaceholder":273},[250,3351,3352],{"class":252,"line":283},[250,3353,3354],{"class":256},"# Хорошо — ждём реальной готовности\n",[250,3356,3357,3359],{"class":252,"line":291},[250,3358,2114],{"class":1807},[250,3360,1811],{"class":266},[250,3362,3363,3365],{"class":252,"line":296},[250,3364,1931],{"class":1807},[250,3366,1811],{"class":266},[250,3368,3369,3372,3374],{"class":252,"line":302},[250,3370,3371],{"class":1807},"    condition",[250,3373,1831],{"class":266},[250,3375,2165],{"class":595},[32,3377,3379],{"id":3378},"_4-latest-тег-в-production","4. latest-тег в production",[15,3381,3382,3385,3386,1835],{},[65,3383,3384],{},"image: my-app:latest"," непредсказуемо — latest может указывать на разные версии. Используйте конкретные теги: ",[65,3387,3388],{},"image: my-app:v1.2.3",[32,3390,3392],{"id":3391},"_5-процесс-от-root","5. Процесс от root",[15,3394,3395,3396,2717,3399,3402,3403,1835],{},"По умолчанию процесс работает от root. Добавьте ",[65,3397,3398],{},"RUN adduser -S appuser",[65,3400,3401],{},"USER appuser"," перед ",[65,3404,172],{},[21,3406],{},[24,3408,3410],{"id":3409},"вопросы-на-собеседовании","Вопросы на собеседовании",[15,3412,3413],{},[571,3414,3415],{},"1. Что такое multi-stage build и зачем он нужен?",[15,3417,3418,3419,3421],{},"Multi-stage build использует несколько инструкций ",[65,3420,67],{}," в одном Dockerfile. В первой стадии (builder) собирается бинарник со всеми инструментами (Go SDK, компилятор), во второй — создаётся чистый образ, куда копируется только готовый бинарник. Для Go это уменьшает размер образа с ~800 МБ (golang:1.26) до ~15-25 МБ (alpine + бинарник). SDK, исходники и промежуточные файлы не попадают в финальный образ.",[15,3423,3424],{},[571,3425,3426],{},"2. Зачем нужен CGO_ENABLED=0 при сборке Go-приложения для Docker?",[15,3428,3429,3431,3432,3434],{},[65,3430,1131],{}," отключает CGO и создаёт полностью статический бинарник без зависимости от системных C-библиотек (libc). Это необходимо, потому что в финальном образе (alpine, scratch) может не быть тех библиотек, которые были в builder-образе. Со статическим бинарником можно использовать даже ",[65,3433,339],{}," (пустой образ).",[15,3436,3437],{},[571,3438,3439],{},"3. В чём разница между ENTRYPOINT и CMD?",[15,3441,3442,3443,3445,3446,1257,3448,3450,3451,3454,3455,3458],{},"ENTRYPOINT задаёт основную команду контейнера — она не перезаписывается аргументами ",[65,3444,193],{},". CMD задаёт аргументы по умолчанию — они заменяются, если пользователь передаёт свои. В связке: ",[65,3447,180],{},[65,3449,199],{}," — ",[65,3452,3453],{},"docker run my-app --port 9090"," выполнит ",[65,3456,3457],{},"\u002Fserver --port 9090",". Важно использовать exec-форму (с квадратными скобками), чтобы процесс получал PID 1 и корректно обрабатывал сигналы.",[15,3460,3461],{},[571,3462,3463],{},"4. Как Docker Compose обеспечивает сетевое взаимодействие между сервисами?",[15,3465,3466,3467,3469,3470,3473],{},"Compose автоматически создаёт bridge-сеть для всех сервисов в файле. Каждый сервис получает DNS-имя, совпадающее с его именем в ",[65,3468,1781],{},". Приложение обращается к PostgreSQL как ",[65,3471,3472],{},"postgres:5432",", а Docker DNS резолвит это имя в IP-адрес контейнера. Можно создать несколько сетей для изоляции (frontend, backend).",[15,3475,3476],{},[571,3477,3478],{},"5. Как правильно организовать кэширование слоёв при сборке Go-приложения?",[15,3480,3481,3482,2717,3484,3486,3487,3490,3491,3493,3494,3496,3497,3499],{},"Ключевой принцип: от редко меняющегося к часто меняющемуся. Сначала копируются ",[65,3483,1078],{},[65,3485,1082],{},", затем выполняется ",[65,3488,3489],{},"go mod download"," (этот слой кэшируется, пока зависимости не изменятся). Только потом копируется весь исходный код (",[65,3492,105],{},") и запускается сборка. Если скопировать всё сразу (",[65,3495,105],{}," до ",[65,3498,3489],{},"), любое изменение в коде инвалидирует кэш зависимостей, и они будут скачиваться при каждой сборке.",[15,3501,3502],{},[571,3503,3504],{},"6. Что произойдёт, если в depends_on не использовать healthcheck?",[15,3506,3507,3509,3510,3513],{},[65,3508,2114],{}," без healthcheck гарантирует только то, что контейнер зависимости запущен, но не то, что сервис внутри него готов принимать соединения. PostgreSQL может стартовать за 5-10 секунд, и в это время приложение будет получать connection refused. С ",[65,3511,3512],{},"condition: service_healthy"," Compose ждёт, пока healthcheck не пройдёт успешно.",[15,3515,3516],{},[571,3517,3518],{},"7. Чем docker compose stop отличается от docker compose down?",[15,3520,3521,3523,3524,3527,3528,3530,3531,3533,3534,3536,3537,3540],{},[65,3522,2716],{}," только останавливает контейнеры — их можно запустить снова через ",[65,3525,3526],{},"start",". ",[65,3529,2720],{}," останавливает контейнеры, удаляет их и удаляет созданные сети. С флагом ",[65,3532,2729],{}," также удаляются named volumes. ",[65,3535,2716],{}," безопасен для данных, ",[65,3538,3539],{},"down -v"," — нет.",[21,3542],{},[24,3544,3546],{"id":3545},"задачи","Задачи",[32,3548,3550],{"id":3549},"задача-1-multi-stage-dockerfile-для-go-сервиса","Задача 1: Multi-stage Dockerfile для Go-сервиса",[15,3552,3553],{},"Напишите production-ready Dockerfile для Go-сервиса со следующими требованиями:",[1061,3555,3556,3559,3565,3570,3573,3576,3579,3582],{},[1064,3557,3558],{},"Multi-stage build (builder + final)",[1064,3560,3561,3562],{},"Базовый образ для сборки: ",[65,3563,3564],{},"golang:1.26-alpine3.23",[1064,3566,3567,3568],{},"Финальный образ: ",[65,3569,1247],{},[1064,3571,3572],{},"Статическая линковка (CGO_ENABLED=0)",[1064,3574,3575],{},"Оптимизация размера через ldflags",[1064,3577,3578],{},"Непривилегированный пользователь",[1064,3580,3581],{},"Кэширование зависимостей отдельно от кода",[1064,3583,3584],{},"CA-сертификаты и tzdata в финальном образе",[15,3586,3587,3588,3591],{},"Проверьте размер полученного образа через ",[65,3589,3590],{},"docker images"," и сравните с вариантом без multi-stage.",[32,3593,3595],{"id":3594},"задача-2-docker-compose-для-микросервисного-окружения","Задача 2: Docker Compose для микросервисного окружения",[15,3597,3598,3599,3601],{},"Создайте ",[65,3600,1781],{}," для следующей архитектуры:",[1061,3603,3604,3607,3610,3613,3616,3622],{},[1064,3605,3606],{},"Go API-сервис (собирается из Dockerfile)",[1064,3608,3609],{},"PostgreSQL 15 с named volume для данных и healthcheck",[1064,3611,3612],{},"Redis 7 для кэширования",[1064,3614,3615],{},"API-сервис не стартует, пока PostgreSQL не будет готов",[1064,3617,3618,3619,3621],{},"Все пароли вынесены в ",[65,3620,2491],{}," файл",[1064,3623,3624],{},"PostgreSQL доступен с хоста (для разработки), Redis — нет",[15,3626,3627,3628,3631,3632,3634],{},"Убедитесь, что после ",[65,3629,3630],{},"docker compose down"," и повторного ",[65,3633,1765],{}," данные в PostgreSQL сохраняются.",[32,3636,3638],{"id":3637},"задача-3-оптимизация-кэширования","Задача 3: Оптимизация кэширования",[15,3640,3641],{},"Возьмите следующий «плохой» Dockerfile и оптимизируйте его, сохранив функциональность. Измерьте время сборки до и после оптимизации (при изменении только Go-кода):",[241,3643,3645],{"className":243,"code":3644,"language":245,"meta":246,"style":246},"FROM golang:1.26\nWORKDIR \u002Fapp\nCOPY . .\nRUN go mod download\nRUN CGO_ENABLED=0 go build -o server .\u002Fcmd\u002Fserver\nEXPOSE 8080\nCMD [\"\u002Fapp\u002Fserver\"]\n",[65,3646,3647,3653,3659,3665,3671,3678,3684],{"__ignoreMap":246},[250,3648,3649,3651],{"class":252,"line":253},[250,3650,67],{"class":263},[250,3652,267],{"class":266},[250,3654,3655,3657],{"class":252,"line":260},[250,3656,82],{"class":263},[250,3658,361],{"class":266},[250,3660,3661,3663],{"class":252,"line":270},[250,3662,97],{"class":263},[250,3664,437],{"class":266},[250,3666,3667,3669],{"class":252,"line":277},[250,3668,127],{"class":263},[250,3670,507],{"class":266},[250,3672,3673,3675],{"class":252,"line":283},[250,3674,127],{"class":263},[250,3676,3677],{"class":266}," CGO_ENABLED=0 go build -o server .\u002Fcmd\u002Fserver\n",[250,3679,3680,3682],{"class":252,"line":291},[250,3681,157],{"class":263},[250,3683,829],{"class":266},[250,3685,3686,3688,3690,3692],{"class":252,"line":296},[250,3687,187],{"class":263},[250,3689,592],{"class":266},[250,3691,844],{"class":595},[250,3693,599],{"class":266},[15,3695,3696],{},"Объясните каждое изменение и почему оно улучшает сборку.",[21,3698],{},[24,3700,3702],{"id":3701},"интерактивная-практика","Интерактивная практика",[3704,3705,3709,3712,3736],"quiz",{"answer":3706,"id":3707,"xp":3708},"1","infra-dockerfile-q1","10",[15,3710,3711],{},"Какой порядок чаще всего даёт лучший cache hit для Go Dockerfile?",[3713,3714,3715],"template",{"v-slot:options":246},[1061,3716,3717,3725,3728,3731],{},[1064,3718,3719,3720,2717,3722,3724],{},"Сначала скопировать ",[65,3721,1078],{},[65,3723,1082],{},", скачать зависимости, потом копировать исходники",[1064,3726,3727],{},"Сначала скопировать весь проект, потом скачать зависимости",[1064,3729,3730],{},"Сначала собрать бинарник, потом скачать зависимости",[1064,3732,3719,3733,3735],{},[65,3734,1397],{},", чтобы Docker видел историю изменений",[3713,3737,3738],{"v-slot:explanation":246},[15,3739,3740,3741,1496,3743,3745,3746,3748,3749,3752,3753,1835],{},"Слой с зависимостями должен инвалидироваться только при изменении ",[65,3742,1078],{},[65,3744,1082],{},". Если сначала делать ",[65,3747,105],{},", любое изменение ",[65,3750,3751],{},".go"," файла ломает кэш для ",[65,3754,3489],{},[3756,3757,3761,3764,3938],"predict",{"answer":3758,"id":3759,"xp":3760},"cache-hit\\nrebuild","infra-dockerfile-p1","15",[15,3762,3763],{},"Что выведет программа?",[3713,3765,3766],{"v-slot:code":246},[241,3767,3771],{"className":3768,"code":3769,"language":3770,"meta":246,"style":246},"language-go shiki shiki-themes github-dark","package main\n\nimport \"fmt\"\n\nfunc depsLayer(changed string) string {\n    if changed == \"go.mod\" || changed == \"go.sum\" {\n        return \"rebuild\"\n    }\n    return \"cache-hit\"\n}\n\nfunc main() {\n    fmt.Println(depsLayer(\"main.go\"))\n    fmt.Println(depsLayer(\"go.mod\"))\n}\n","go",[65,3772,3773,3781,3785,3799,3803,3830,3856,3864,3869,3877,3882,3886,3896,3917,3934],{"__ignoreMap":246},[250,3774,3775,3778],{"class":252,"line":253},[250,3776,3777],{"class":263},"package",[250,3779,3780],{"class":2580}," main\n",[250,3782,3783],{"class":252,"line":260},[250,3784,274],{"emptyLinePlaceholder":273},[250,3786,3787,3790,3793,3796],{"class":252,"line":270},[250,3788,3789],{"class":263},"import",[250,3791,3792],{"class":595}," \"",[250,3794,3795],{"class":2580},"fmt",[250,3797,3798],{"class":595},"\"\n",[250,3800,3801],{"class":252,"line":277},[250,3802,274],{"emptyLinePlaceholder":273},[250,3804,3805,3808,3811,3814,3818,3821,3824,3827],{"class":252,"line":283},[250,3806,3807],{"class":263},"func",[250,3809,3810],{"class":2580}," depsLayer",[250,3812,3813],{"class":266},"(",[250,3815,3817],{"class":3816},"s9osk","changed",[250,3819,3820],{"class":263}," string",[250,3822,3823],{"class":266},") ",[250,3825,3826],{"class":263},"string",[250,3828,3829],{"class":266}," {\n",[250,3831,3832,3835,3838,3841,3844,3847,3849,3851,3854],{"class":252,"line":291},[250,3833,3834],{"class":263},"    if",[250,3836,3837],{"class":266}," changed ",[250,3839,3840],{"class":263},"==",[250,3842,3843],{"class":595}," \"go.mod\"",[250,3845,3846],{"class":263}," ||",[250,3848,3837],{"class":266},[250,3850,3840],{"class":263},[250,3852,3853],{"class":595}," \"go.sum\"",[250,3855,3829],{"class":266},[250,3857,3858,3861],{"class":252,"line":296},[250,3859,3860],{"class":263},"        return",[250,3862,3863],{"class":595}," \"rebuild\"\n",[250,3865,3866],{"class":252,"line":302},[250,3867,3868],{"class":266},"    }\n",[250,3870,3871,3874],{"class":252,"line":310},[250,3872,3873],{"class":263},"    return",[250,3875,3876],{"class":595}," \"cache-hit\"\n",[250,3878,3879],{"class":252,"line":315},[250,3880,3881],{"class":266},"}\n",[250,3883,3884],{"class":252,"line":321},[250,3885,274],{"emptyLinePlaceholder":273},[250,3887,3888,3890,3893],{"class":252,"line":805},[250,3889,3807],{"class":263},[250,3891,3892],{"class":2580}," main",[250,3894,3895],{"class":266},"() {\n",[250,3897,3898,3901,3904,3906,3909,3911,3914],{"class":252,"line":811},[250,3899,3900],{"class":266},"    fmt.",[250,3902,3903],{"class":2580},"Println",[250,3905,3813],{"class":266},[250,3907,3908],{"class":2580},"depsLayer",[250,3910,3813],{"class":266},[250,3912,3913],{"class":595},"\"main.go\"",[250,3915,3916],{"class":266},"))\n",[250,3918,3919,3921,3923,3925,3927,3929,3932],{"class":252,"line":819},[250,3920,3900],{"class":266},[250,3922,3903],{"class":2580},[250,3924,3813],{"class":266},[250,3926,3908],{"class":2580},[250,3928,3813],{"class":266},[250,3930,3931],{"class":595},"\"go.mod\"",[250,3933,3916],{"class":266},[250,3935,3936],{"class":252,"line":824},[250,3937,3881],{"class":266},[3713,3939,3940],{"v-slot:hint":246},[15,3941,3942],{},"Изменение обычного Go-файла не должно заново скачивать зависимости, если Dockerfile разделяет слои корректно.",[3944,3945,3949,3960,4108],"code-task",{"expected":3946,"id":3947,"xp":3948},"bad\\nok\\nbad","infra-dockerfile-ct1","20",[15,3950,3951,3952,3955,3956,3959],{},"Реализуй ",[65,3953,3954],{},"DockerfileReview",": production-образ считаем ",[65,3957,3958],{},"ok",", только если контейнер запускается не от root-пользователя и имеет healthcheck.",[3713,3961,3962],{"v-slot:template":246},[241,3963,3965],{"className":3768,"code":3964,"language":3770,"meta":246,"style":246},"package main\n\nimport \"fmt\"\n\nfunc DockerfileReview(hasUser bool, hasHealthcheck bool) string {\n    return \"todo\"\n}\n\nfunc main() {\n    fmt.Println(DockerfileReview(false, true))\n    fmt.Println(DockerfileReview(true, true))\n    fmt.Println(DockerfileReview(true, false))\n}\n",[65,3966,3967,3973,3977,3987,3991,4019,4026,4030,4034,4042,4064,4084,4104],{"__ignoreMap":246},[250,3968,3969,3971],{"class":252,"line":253},[250,3970,3777],{"class":263},[250,3972,3780],{"class":2580},[250,3974,3975],{"class":252,"line":260},[250,3976,274],{"emptyLinePlaceholder":273},[250,3978,3979,3981,3983,3985],{"class":252,"line":270},[250,3980,3789],{"class":263},[250,3982,3792],{"class":595},[250,3984,3795],{"class":2580},[250,3986,3798],{"class":595},[250,3988,3989],{"class":252,"line":277},[250,3990,274],{"emptyLinePlaceholder":273},[250,3992,3993,3995,3998,4000,4003,4006,4008,4011,4013,4015,4017],{"class":252,"line":283},[250,3994,3807],{"class":263},[250,3996,3997],{"class":2580}," DockerfileReview",[250,3999,3813],{"class":266},[250,4001,4002],{"class":3816},"hasUser",[250,4004,4005],{"class":263}," bool",[250,4007,623],{"class":266},[250,4009,4010],{"class":3816},"hasHealthcheck",[250,4012,4005],{"class":263},[250,4014,3823],{"class":266},[250,4016,3826],{"class":263},[250,4018,3829],{"class":266},[250,4020,4021,4023],{"class":252,"line":291},[250,4022,3873],{"class":263},[250,4024,4025],{"class":595}," \"todo\"\n",[250,4027,4028],{"class":252,"line":296},[250,4029,3881],{"class":266},[250,4031,4032],{"class":252,"line":302},[250,4033,274],{"emptyLinePlaceholder":273},[250,4035,4036,4038,4040],{"class":252,"line":310},[250,4037,3807],{"class":263},[250,4039,3892],{"class":2580},[250,4041,3895],{"class":266},[250,4043,4044,4046,4048,4050,4052,4054,4057,4059,4062],{"class":252,"line":315},[250,4045,3900],{"class":266},[250,4047,3903],{"class":2580},[250,4049,3813],{"class":266},[250,4051,3954],{"class":2580},[250,4053,3813],{"class":266},[250,4055,4056],{"class":1834},"false",[250,4058,623],{"class":266},[250,4060,4061],{"class":1834},"true",[250,4063,3916],{"class":266},[250,4065,4066,4068,4070,4072,4074,4076,4078,4080,4082],{"class":252,"line":321},[250,4067,3900],{"class":266},[250,4069,3903],{"class":2580},[250,4071,3813],{"class":266},[250,4073,3954],{"class":2580},[250,4075,3813],{"class":266},[250,4077,4061],{"class":1834},[250,4079,623],{"class":266},[250,4081,4061],{"class":1834},[250,4083,3916],{"class":266},[250,4085,4086,4088,4090,4092,4094,4096,4098,4100,4102],{"class":252,"line":805},[250,4087,3900],{"class":266},[250,4089,3903],{"class":2580},[250,4091,3813],{"class":266},[250,4093,3954],{"class":2580},[250,4095,3813],{"class":266},[250,4097,4061],{"class":1834},[250,4099,623],{"class":266},[250,4101,4056],{"class":1834},[250,4103,3916],{"class":266},[250,4105,4106],{"class":252,"line":811},[250,4107,3881],{"class":266},[3713,4109,4110],{"v-slot:hints":246},[1061,4111,4112,4117,4123],{},[1064,4113,4114,4116],{},[65,4115,1456],{}," снижает последствия компрометации контейнера.",[1064,4118,4119,4122],{},[65,4120,4121],{},"HEALTHCHECK"," помогает оркестратору отличать живой процесс от готового сервиса.",[1064,4124,4125],{},"Нужны оба признака одновременно.",[4127,4128,4129],"style",{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}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 pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":246,"searchDepth":260,"depth":260,"links":4131},[4132,4141,4149,4158,4163,4170,4171,4178,4179,4184],{"id":26,"depth":260,"text":27,"children":4133},[4134,4135,4136,4137,4138,4139,4140],{"id":34,"depth":270,"text":35},{"id":232,"depth":270,"text":233},{"id":343,"depth":270,"text":344},{"id":400,"depth":270,"text":401},{"id":465,"depth":270,"text":466},{"id":563,"depth":270,"text":564},{"id":740,"depth":270,"text":741},{"id":854,"depth":260,"text":855,"children":4142},[4143,4144,4145,4146,4147,4148],{"id":861,"depth":270,"text":862},{"id":883,"depth":270,"text":884},{"id":1053,"depth":270,"text":1054},{"id":1114,"depth":270,"text":1115},{"id":1194,"depth":270,"text":1195},{"id":1213,"depth":270,"text":1214},{"id":1278,"depth":260,"text":1279,"children":4150},[4151,4152,4153,4154,4155,4156,4157],{"id":1282,"depth":270,"text":1283},{"id":1370,"depth":270,"text":1371},{"id":1401,"depth":270,"text":1402},{"id":1476,"depth":270,"text":1477},{"id":1483,"depth":270,"text":1484},{"id":1503,"depth":270,"text":1504},{"id":1539,"depth":270,"text":1540},{"id":1742,"depth":260,"text":1743,"children":4159},[4160,4161,4162],{"id":1746,"depth":270,"text":1747},{"id":1785,"depth":270,"text":1786},{"id":2074,"depth":270,"text":2075},{"id":2558,"depth":260,"text":2559,"children":4164},[4165,4166,4167,4168,4169],{"id":2562,"depth":270,"text":2563},{"id":2736,"depth":270,"text":2737},{"id":2824,"depth":270,"text":2825},{"id":2865,"depth":270,"text":2866},{"id":2943,"depth":270,"text":2944},{"id":2989,"depth":260,"text":2990},{"id":3241,"depth":260,"text":3242,"children":4172},[4173,4174,4175,4176,4177],{"id":3245,"depth":270,"text":3246},{"id":3260,"depth":270,"text":3261},{"id":3321,"depth":270,"text":3322},{"id":3378,"depth":270,"text":3379},{"id":3391,"depth":270,"text":3392},{"id":3409,"depth":260,"text":3410},{"id":3545,"depth":260,"text":3546,"children":4180},[4181,4182,4183],{"id":3549,"depth":270,"text":3550},{"id":3594,"depth":270,"text":3595},{"id":3637,"depth":270,"text":3638},{"id":3701,"depth":260,"text":3702},1781022065153]