scratch
e do pequeno servidor http com base nessa compilação, consegui reduzir o resultado para 6,32kB!
Se você preferir vídeo, aqui está um vídeo do YouTube para o artigo!
Recipientes inchados
Os contêineres são frequentemente apresentados como uma panacéia para lidar com qualquer desafio de manutenção de software. Além disso, como gosto de contêineres, na prática muitas vezes me deparo com imagens de contêineres com vários problemas. Um problema comum é o tamanho do contêiner; para algumas imagens chega a muitos gigabytes!
Então decidi desafiar a mim mesmo e a todos os outros e tentar criar uma imagem o mais compacta possível.
Uma tarefa
As regras são bem simples:
- O contêiner deve servir o conteúdo do arquivo via http para a porta de sua escolha
- A montagem de volumes não é permitida (a chamada "Regra de Marek")
Solução simplificada
Para descobrir o tamanho da imagem de base, você pode usar node.js e criar um servidor simples
index.js
:
const fs = require("fs"); const http = require('http'); const server = http.createServer((req, res) => { res.writeHead(200, { 'content-type': 'text/html' }) fs.createReadStream('index.html').pipe(res) }) server.listen(port, hostname, () => { console.log(`Server: http://0.0.0.0:8080/`); });
e faça uma imagem dele executando a imagem de base oficial do nó:
FROM node:14 COPY . . CMD ["node", "index.js"]
Este aguentou
943MB
!
Imagem de base reduzida
Uma das abordagens táticas mais simples e óbvias para reduzir o tamanho da pele é optar por uma pele de base mais esguia. A imagem base oficial do nó existe em uma variante
slim
(ainda baseada no debian, mas com menos dependências pré-instaladas) e uma variante
alpine
baseada no Alpine Linux .
Usando
node:14-slim
e
node:14-alpine
como base, é possível reduzir o tamanho da imagem para
167MB
e de
116MB
acordo.
Como as imagens docker são aditivas, com cada camada construída sobre a próxima, não há quase nada a fazer aqui para reduzir ainda mais a solução node.js.
Linguagens compiladas
Para levar as coisas para o próximo nível, você pode mover para uma linguagem compilada que tenha muito menos dependências de tempo de execução. Existem várias opções, mas o golang é frequentemente usado para criar serviços da web .
Criei o servidor de arquivos mais simples
server.go
:
package main import ( "fmt" "log" "net/http" ) func main() { fileServer := http.FileServer(http.Dir("./")) http.Handle("/", fileServer) fmt.Printf("Starting server at port 8080\n") if err := http.ListenAndServe(":8080", nil); err != nil { log.Fatal(err) } }
E eu o construí na imagem do contêiner usando a imagem base oficial do golang:
FROM golang:1.14 COPY . . RUN go build -o server . CMD ["./server"]
Que aguentou…
818MB
.
Há um problema aqui: há muitas dependências instaladas na imagem do golang base, que são úteis na construção de programas go, mas não são necessárias para executá-los.
Conjuntos multiestágios
O Docker tem um recurso chamado builds de vários estágios , com o qual é fácil construir código em um ambiente que contém todas as dependências necessárias e, em seguida, copiar o arquivo executável resultante em outra imagem.
Isso é útil por vários motivos, mas um dos mais óbvios é o tamanho da imagem! Refatorando o dockerfile assim:
### ### FROM golang:1.14-alpine AS builder COPY . . RUN go build -o server . ### ### FROM alpine:3.12 COPY --from=builder /go/server ./server COPY index.html index.html CMD ["./server"]
O tamanho da imagem resultante é tudo
13.2MB
!
Compilação estática + imagem Scratch
13 MB não é nada ruim, mas ainda temos alguns truques para deixar isso ainda mais apertado.
Existe uma imagem de base chamada scratch , que está inequivocamente vazia, seu tamanho é zero. Como
scratch
não há nada dentro , qualquer imagem construída em sua base deve conter todas as dependências necessárias.
Para tornar isso possível com base em nosso servidor go, precisamos adicionar alguns sinalizadores em tempo de compilação para garantir que todas as bibliotecas necessárias sejam vinculadas estaticamente ao executável:
### ### FROM golang:1.14 as builder COPY . . RUN go build \ -ldflags "-linkmode external -extldflags -static" \ -a server.go ### ### FROM scratch COPY --from=builder /go/server ./server COPY index.html index.html CMD ["./server"]
Em particular, definimos
external
o modo de vinculação e passamos o sinalizador para o
-static
vinculador externo.
Graças a essas duas mudanças, é possível aumentar o tamanho da imagem para
8.65MB
ASM como garantia de vitória!
Uma imagem com menos de 10 MB de tamanho, escrita em uma linguagem como Go, é nitidamente miniaturizada para quase todas as circunstâncias ... mas você pode torná-la ainda menor! O usuário nemasu postou um servidor http completo escrito em assembler no Github. É chamado de assmttpd .
Para colocá-lo em contêiner, bastou instalar algumas dependências de compilação na imagem base do Ubuntu, antes de executar a receita fornecida
make release
:
### ### FROM ubuntu:18.04 as builder RUN apt update RUN apt install -y make yasm as31 nasm binutils COPY . . RUN make release ### ### FROM scratch COPY --from=builder /asmttpd /asmttpd COPY /web_root/index.html /web_root/index.html CMD ["/asmttpd", "/web_root", "8080"]
O executável resultante é então
asmttpd
copiado para a imagem de rascunho e chamado por meio da linha de comando. O tamanho da imagem resultante é de apenas 6,34kB!