23.11.2021

Kuinka tehdä minimaalinen Docker-kontti, jossa on vain Rust-ohjelma

Minimaalisessa Docker-kontissa ei ole muuta kuin yksi staattisesti linkitetty ohjelma.

Staattisessa ohjelmassa on kaikki mahdolliset kirjastot otettu mukaan samaan suoritettavaan tiedostoon.

Scratch-image

Kun käytämme pohjana Docker-konttia Scratch, imagen koko muu tiedostojärjestelmä on tyhjä.

Itse asiassa Docker toteuttaa tämän imagen sisäisesti. Vanhemmissa Dockereissa FROM Scratch loi oikeasti tyhjän tiedostolayerin, mutta uusimmissa ei luoda erillistä tiedostolayeriä. [5]

Rust-ohjelmamme tulee olemaan tiedostojärjestelmän ainoa tiedosto.

Esimerkkiohjelma

Esimerkkiohjelma käy pyytämässä httpbin.org -palvelusta kysyjän eli meidän ulkoisen IP-osoitteemme merkkijonona, esim. "122.123.124.125". [1]

Haussa käytetään webbikutsun tekevää reqwest-pakettia ja JSON-vastauksen aukaisevaa Serde-pakettia.

use serde::Deserialize;
use std::error::Error;

#[derive(Deserialize, Debug)]
struct ApiRes {
    origin: String,
}

fn main() -> Result<(), Box<dyn Error>> {
    let res = reqwest::blocking::get("http://httpbin.org/ip")?.json::<ApiRes>()?;
    println!("{}", res.origin);
    Ok(())
}

Cargo.toml

Projektin kuvauksessa joudutaan asettamaan muutama valinnainen ominaisuus.

Serden ominaisuus "derive" on valinnainen. Sen ja reqwest-craten "json"-ominasuuden avulla web-kutsun paluu-JSON luetaan suoraan kentän "origin" arvoksi.

Web-kutsun tekevän reqwest-paketin oletus on asynkroninen toiminta. Yksinkertaisesti vastausta odottamaan jäävää blocking-ominaisuutta täytyy erikseen pyytää.

[package]
name = "minimal-docker"
version = "0.1.0"
edition = "2018"

[dependencies]
serde = { version = "1.0.130", features = ["derive"] }
reqwest = { version = "0.11.6", default-features = false, features = ["json", "blocking"] }

Käännös

Staattisessa käännöksessä kaikki kirjastot halutaan ohjelman sisään.

Yleensä Linuxissa käännettävät Rust-ohjelmat olettavat koneesta löytyvän dynaamisesti ladattava glibc-kirjasto. glibc soveltuu kuitenkin huonosti staattiseen käännökseen. Vaikka glibc olisi käännetty staattisesti, se haluaa silti ladata muutaman alikirjaston dynaamisesti. [3]

Helpointa on käyttää pienempää MUSL-kirjastoa ja kääntää se staattisesti ohjelmaan.

Kun valitsemme käännöksen target-arkkitehtuuriksi

x86_64-unknown-linux-musl

saamme käyttöön MUSL-kirjaston ja staattisen käännöksen.

Tämä käännöstargetin kääntäjät ja kirjastot saadaan käyttöön rustup-komennolla

rustup target add x86_64-unknown-linux-musl

Lisäksi saatetaan tarvita paketit (Debian/Ubuntu) musl-tools ja musl-dev, mutta esimerkissämme näitä ei tarvita.

Imagen luonti

Dockerfile on seuraavan näköinen (Yksinkertaistettu lähteen [1] esimerkistä.)

#
# Käännösimage
#

FROM rust:latest AS builder

RUN rustup target add x86_64-unknown-linux-musl

ENV USER=minimal-docker
ENV UID=10001

RUN adduser \
    --disabled-password \
    --gecos "" \
    --home "/nonexistent" \
    --shell "/sbin/nologin" \
    --no-create-home \
    --uid "${UID}" \
    "${USER}"


WORKDIR /minimal-docker
COPY ./ .

RUN cargo build --target x86_64-unknown-linux-musl --release

#
# Lopullinen image
#

FROM scratch

COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /etc/group /etc/group

WORKDIR /minimal-docker
COPY --from=builder minimal-docker/target/x86_64-unknown-linux-musl/release/minimal-docker ./

USER minimal-docker:minimal-docker
CMD ["/minimal-docker/minimal-docker"]

Tämä ajetaan hakemistossa, jossa on Cargo.toml ja src/main.rs

docker build .

(Vihje: jos olet jo kääntänyt ohjelman paikallisesti, poista hakemisto /target ennen docker buildia. Mahdollisesti unohtunut /target kopioidaan tarpeettomasti käännösimageen. Sen koko on puolisen gigatavua ja kopio voi kestää minuutin verran.)

Yhteenveto

Tuloksena oleva Docker-image on kooltaan 8,14 MB.

Tämä siis sisältää perustavaran, mutta ainoa tiedostojärjestelmän tiedosto on staattisesti linkitetty ohjelma minimal-docker.

(Ja /etc/passwd ja /etc/group, joiden avulla voimme ajaa ohjelman ei-root-käyttäjänä.)

Windowsissa kontin ajo kestää noin viisi sekuntia.

Jos keksit tavan tehdä vielä pienempiä kontteja, kerro ideasi esa@learners.fi.

Ratkaisun lähdekoodi (kolme tiedostoa) on ladattavissa

https://blog.learners.fi/minimal-docker.zip

Lähteitä

[1] Sylvain Kerkour, How to create small Docker images for Rust, https://kerkour.com/rust-small-docker-image/

[2] Pyry Kontio, Container for building Rust crates for MUSL target https://gitlab.com/rust_musl_docker/image

[3] Miksi glibc ei toimi staattisesti linkitettynä https://stackoverflow.com/questions/57476533/why-is-statically-linking-glibc-discouraged

[4] James Walker, How to Create Your Own Docker Base Images From “Scratch” https://www.cloudsavvyit.com/14340/how-to-create-your-own-docker-base-images-from-scratch/

[5] Scracth-imagen muuttuminen sisäisesti toteutetuksi https://github.com/moby/moby/pull/8827