É fraco levantar um recipiente tão pequeno? Criação de um servidor HTTP em contêiner de 6kB

TL; DR   Decidi criar a menor imagem de contêiner com a qual você ainda pode fazer algo útil. Tirando proveito das compilações de vários estágios, da imagem de base  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!



All Articles