From 074e4476a8f3efeda84b2fd39e266fb412f795d1 Mon Sep 17 00:00:00 2001 From: Joe Groocock Date: Wed, 28 Apr 2021 22:18:44 +0100 Subject: [PATCH] Build Plex on musl, from scratch Plex now provide a first-party musl Plex build that works without any external dependencies whatsoever. It's built with LLVM with many compiler and linker optimisations enabled: https://forums.plex.tv/t/plex-media-server-forum-preview-faster-and-smaller-builds-with-new-toolchain/699575 Restructure build into multiple distinct Docker build stages to better leverage caching and significantly improve build time on multicore systems with BuildKit, particularly with LTO enabled. Changes for this release include: - Build `FROM spritsail/alpine` instead of `FROM debian` to ensure musl compatibility with all compiled binaries. Use `FROM scratch` for the resulting image. ld-musl is provided by Plex. - Build busybox, su-exec and tini as they're no longer provided by the base image. - Build binaries/libraries with standard hardening flags, including the popular -flto. Signed-off-by: Joe Groocock --- .drone.yml | 10 +- Dockerfile | 272 ++++++++++++++++++++++++++++++++++++------------ README.md | 6 +- claim-server.sh | 23 ++-- 4 files changed, 224 insertions(+), 87 deletions(-) diff --git a/.drone.yml b/.drone.yml index ad4d60f..be2d5e7 100644 --- a/.drone.yml +++ b/.drone.yml @@ -18,7 +18,8 @@ steps: image: spritsail/docker-test settings: run: | - curl --version && + busybox && \ + curl --version && \ xmlstarlet --version - name: test @@ -40,10 +41,7 @@ steps: tags: - plexpass - "%label io.spritsail.version.plex | %prefix plexpass | %remsuf [0-9a-f]+$ | %auto 2" - username: - from_secret: docker_username - password: - from_secret: docker_password + login: {from_secret: docker_login} when: branch: - pass @@ -51,6 +49,6 @@ steps: - push --- kind: signature -hmac: d7ad7e2102295415a2f975e1cb216c712d3b7fd78b11bd48020eb8f464a13b88 +hmac: a6e529b62ed2ecf24b2c3125f7267c321d56ce16d1cd8178f96e0399dd3096bd ... diff --git a/Dockerfile b/Dockerfile index c589c86..63a0124 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,25 +1,52 @@ -ARG PLEX_VER=1.23.0.4438-5eef0fd80 -ARG PLEX_SHA=3fa5b5410fba1720d55f1a3e20920c799cdf559e -ARG XMLSTAR_VER=1.6.1 -ARG CURL_VER=curl-7_74_0 +ARG PLEX_VER=1.23.0.4482-62106842a +ARG PLEX_SHA=022f2ac2a18ec602f0402baf94d69f405b9207a4 +ARG BUSYBOX_VER=1.33.0 +ARG SU_EXEC_VER=0.4 +ARG TINI_VER=0.19.0 ARG ZLIB_VER=1.2.11 +ARG LIBXML2_VER=v2.9.10 +ARG LIBXSLT_VER=v1.1.34 +ARG XMLSTAR_VER=1.6.1 ARG OPENSSL_VER=1.1.1i +ARG CURL_VER=curl-7_76_1 -FROM spritsail/debian-builder:buster-slim as builder +ARG OUTPUT=/output +ARG DESTDIR=/prefix + +ARG CFLAGS="-O2 -pipe -fstack-protector-strong -D_FORTIFY_SOURCE=2 -flto" +ARG LDFLAGS="$CFLAGS -Wl,-O1,--sort-common,--as-needed,-z,relro,-z,now" + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +FROM spritsail/alpine:3.13 AS builder + +RUN apk add --no-cache \ + autoconf \ + automake \ + binutils \ + cmake \ + curl \ + dpkg \ + file \ + gcc \ + git \ + libtool \ + linux-headers \ + make \ + musl-dev \ + nghttp2-dev \ + pkgconfig \ + xxd + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +FROM builder AS plex ARG PLEX_VER ARG PLEX_SHA -ARG LIBXML2_VER=v2.9.10 -ARG LIBXSLT_VER=v1.1.34 -ARG XMLSTAR_VER -ARG OPENSSL_VER -ARG CURL_VER -ARG ZLIB_VER -ARG MAKEFLAGS +ARG OUTPUT -ARG PREFIX=/prefix - -WORKDIR /plex +WORKDIR $OUTPUT # Fetch Plex and required libraries RUN curl -fsSL -o plexmediaserver.deb https://downloads.plex.tv/plex-media-server-new/${PLEX_VER}/debian/plexmediaserver_${PLEX_VER}_amd64.deb \ @@ -35,31 +62,101 @@ RUN curl -fsSL -o plexmediaserver.deb https://downloads.plex.tv/plex-media-serve lib/libcrypto.so* \ lib/libcurl.so* \ lib/libssl.so* \ + lib/libnghttp2.so* \ lib/libxml2.so* \ lib/libxslt.so* \ lib/libexslt.so* \ lib/plexmediaserver.* \ + etc/ld-musl-x86_64.path \ Resources/start.sh \ + \ # Place shared libraries in usr/lib so they can be actually shared && mv lib/*.so* lib/dri ../ \ - && rmdir lib \ - && cp /lib/x86_64-linux-gnu/libgcc_s.so.1 ../ + && rmdir lib etc \ + && ln -sv ../ lib \ + # Replace hardlink with a symlink; these files are the same + && cd .. && ln -sfvn ld-musl-x86_64.so.1 libc.so + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +FROM builder AS busybox + +ARG BUSYBOX_VER +ARG SU_EXEC_VER +ARG TINI_VER +ARG CFLAGS +ARG LDFLAGS +ARG MAKEFLAGS +ARG OUTPUT + +WORKDIR /tmp/busybox + +RUN curl -fsSL https://busybox.net/downloads/busybox-${BUSYBOX_VER}.tar.bz2 \ + | tar xj --strip-components=1 \ + && make defconfig \ + && make \ + && install -Dm755 busybox "$OUTPUT/usr/bin/busybox" \ + # "Install" busybox, creating symlinks to all binaries it provides + && mkdir -p "$OUTPUT/usr/bin" "$OUTPUT/usr/sbin" \ + && ./busybox --list-full | sed -E 's@^(s?bin)@usr/\1@' | xargs -i ln -Tsv /usr/bin/busybox "$OUTPUT/{}" + +WORKDIR /tmp/su-exec + +RUN curl -fL https://github.com/frebib/su-exec/archive/v${SU_EXEC_VER}.tar.gz \ + | tar xz --strip-components=1 \ + && make \ + && install -Dm755 su-exec "$OUTPUT/usr/sbin/su-exec" + +WORKDIR /tmp/tini + +RUN curl -fL https://github.com/krallin/tini/archive/v${TINI_VER}.tar.gz \ + | tar xz --strip-components=1 \ + && cmake . \ + && make tini \ + && install -Dm755 tini "$OUTPUT/usr/sbin/tini" + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +FROM builder AS zlib + +ARG ZLIB_VER +ARG CFLAGS +ARG LDFLAGS +ARG MAKEFLAGS +ARG OUTPUT +ARG DESTDIR -# Download and build zlib WORKDIR /tmp/zlib + RUN curl -sSf https://www.zlib.net/zlib-$ZLIB_VER.tar.xz \ | tar xJ --strip-components=1 \ && ./configure \ --prefix=/usr \ --shared \ - && make DESTDIR=$PREFIX install + && make DESTDIR="$DESTDIR" install \ + && mkdir -p "$OUTPUT/usr/lib" \ + && cp -a "$DESTDIR"/usr/lib/*.so* "$OUTPUT/usr/lib" + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +FROM builder AS xml + +ARG LIBXML2_VER +ARG LIBXSLT_VER +ARG XMLSTAR_VER +ARG CFLAGS +ARG LDFLAGS +ARG MAKEFLAGS +ARG OUTPUT +ARG DESTDIR + +COPY --from=zlib "$DESTDIR" "$DESTDIR" -# Download and build libxml2 WORKDIR /tmp/libxml2 RUN git clone https://gitlab.gnome.org/GNOME/libxml2.git --branch $LIBXML2_VER --depth 1 . \ && ./autogen.sh \ --prefix=/usr \ - --with-zlib=$PREFIX/usr \ + --with-zlib="$DESTDIR/usr" \ --without-catalog \ --without-docbook \ --without-ftp \ @@ -69,20 +166,22 @@ RUN git clone https://gitlab.gnome.org/GNOME/libxml2.git --branch $LIBXML2_VER - --without-legacy \ --without-modules \ --without-python \ - && make DESTDIR=$PREFIX install + && make DESTDIR="$DESTDIR" install \ + && mkdir -p "$OUTPUT/usr/lib" \ + && cp -a "$DESTDIR"/usr/lib/*.so* "$OUTPUT/usr/lib" -# Download and build libxslt WORKDIR /tmp/libxslt RUN git clone https://gitlab.gnome.org/GNOME/libxslt.git --branch $LIBXSLT_VER --depth 1 . \ && ./autogen.sh \ --prefix=/usr \ - --with-libxml-src="../libxml2" \ + --with-libxml-src=../libxml2 \ --without-crypto \ --without-plugins \ --without-python \ - && make DESTDIR=$PREFIX install + && make DESTDIR="$DESTDIR" install \ + && mkdir -p "$OUTPUT/usr/lib" \ + && cp -a "$DESTDIR"/usr/lib/*.so* "$OUTPUT/usr/lib" -# Download and build xmlstarlet ADD xmlstarlet-*.patch /tmp WORKDIR /tmp/xmlstarlet RUN git clone git://git.code.sf.net/p/xmlstar/code --branch $XMLSTAR_VER --depth 1 . \ @@ -91,54 +190,82 @@ RUN git clone git://git.code.sf.net/p/xmlstar/code --branch $XMLSTAR_VER --depth && ./configure \ --prefix=/usr \ --disable-build-docs \ - --with-libxml-prefix=$PREFIX/usr \ - --with-libxslt-prefix=$PREFIX/usr \ - && make DESTDIR=$PREFIX install + --with-libxml-prefix="$DESTDIR/usr" \ + --with-libxslt-prefix="$DESTDIR/usr" \ + && make DESTDIR="$DESTDIR" install \ + && install -Dm755 "$DESTDIR/usr/bin/xml" "$OUTPUT/usr/bin/xmlstarlet" + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +FROM builder AS curl + +ARG OPENSSL_VER +ARG CURL_VER +ARG CFLAGS +ARG LDFLAGS +ARG MAKEFLAGS +ARG OUTPUT +ARG DESTDIR + +COPY --from=zlib "$DESTDIR" "$DESTDIR" -# Download and build OpenSSL as a cURL dependency WORKDIR /tmp/openssl RUN curl -sSL https://openssl.org/source/openssl-${OPENSSL_VER}.tar.gz \ | tar xz --strip-components=1 \ - # Install to the default system directories so cURL can find it && ./config \ --prefix=/usr \ --libdir=lib \ - --with-zlib-lib=$PREFIX/usr/lib/ \ - --with-zlib-include=$PREFIX/usr/include \ + --with-zlib-lib="$DESTDIR/usr/lib" \ + --with-zlib-include="$DESTDIR/usr/include" \ shared \ zlib-dynamic \ + no-engine \ no-rc5 \ no-ssl3-method \ && make build_libs \ && make build_programs \ - && make \ + && make DESTDIR="$DESTDIR" \ install_sw \ install_ssldirs \ - && cp libssl*.so* libcrypto*.so* $PREFIX/usr/lib + && mkdir -p "$OUTPUT/usr/lib" \ + && cp -a "$DESTDIR"/usr/lib/*.so* "$OUTPUT/usr/lib" \ + && sed -i "s@prefix=/usr@prefix=$DESTDIR/usr@g" "$DESTDIR"/usr/lib/pkgconfig/*.pc + +# /usr/lib # curl --version +# curl 7.74.0-DEV (x86_64-pc-linux-musl) libcurl/7.73.0-DEV OpenSSL/1.1.1i zlib/1.2.11 nghttp2/1.41.0 +# Protocols: http https +# Features: AsynchDNS HTTP2 HTTPS-proxy IPv6 Largefile libz SSL UnixSockets -# Download and build curl WORKDIR /tmp/curl RUN git clone https://github.com/curl/curl.git --branch $CURL_VER --depth 1 . \ && autoreconf -sif \ && ./configure \ --prefix=/usr \ + --enable-http \ --enable-ipv6 \ + --enable-largefile \ + --enable-proxy \ + --enable-unix-sockets \ + --with-ssl="$DESTDIR/usr" \ + --with-zlib="$DESTDIR/usr" \ --enable-optimize \ --enable-symbol-hiding \ --enable-versioned-symbols \ --enable-threaded-resolver \ - --with-ssl \ - --with-zlib=$PREFIX/usr \ + --disable-cookies \ --disable-crypto-auth \ --disable-curldebug \ --disable-dependency-tracking \ --disable-dict \ + --disable-file \ + --disable-ftp \ --disable-gopher \ --disable-imap \ - --disable-libcurl-option \ --disable-ldap \ --disable-ldaps \ + --disable-libcurl-option \ --disable-manual \ + --disable-mqtt \ --disable-ntlm-wb \ --disable-pop3 \ --disable-rtsp \ @@ -154,40 +281,52 @@ RUN git clone https://github.com/curl/curl.git --branch $CURL_VER --depth 1 . \ --without-libpsl \ --without-librtmp \ --without-winidn \ - && make DESTDIR=$PREFIX install + && make DESTDIR="$DESTDIR" install \ + && install -Dm755 "$DESTDIR/usr/bin/curl" "$OUTPUT/usr/bin/curl" \ + # Cheat and "borrow" libnghttp2 from Alpine + && mkdir -p "$OUTPUT/usr/lib" \ + && cp -a "$DESTDIR"/usr/lib/*.so* /usr/lib/libnghttp2.so* "$OUTPUT/usr/lib" -WORKDIR $PREFIX +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -RUN mkdir -p /output/usr/lib /output/usr/bin /output/etc/ssl/certs \ +FROM builder AS combine + +ARG OUTPUT +WORKDIR $OUTPUT + +COPY --from=plex "$OUTPUT" . +COPY --from=busybox "$OUTPUT" . +COPY --from=zlib "$OUTPUT" . +COPY --from=xml "$OUTPUT" . +COPY --from=curl "$OUTPUT" . + +RUN install -m 1777 -o root -g root -d tmp \ + && ln -sv /usr/lib /usr/bin /usr/sbin . \ # Link Plex ca-certificates as system store so curl and others can use them too - && ln -sv /usr/lib/plexmediaserver/Resources/cacert.pem /output/etc/ssl/certs/ca-certificates.crt \ - && mv usr/lib/*.so* \ - /plex/usr/lib/* \ - /output/usr/lib \ - && mv usr/bin/curl /output/usr/bin \ - && mv usr/bin/xml /output/usr/bin/xmlstarlet - + && mkdir -p etc/ssl/certs \ + && ln -sv /usr/lib/plexmediaserver/Resources/cacert.pem etc/ssl/certs/ca-certificates.crt \ # Strip all unneeded symbols for optimum size -RUN find /output -exec sh -c 'file "{}" | grep -q ELF && strip --strip-debug "{}"' \; \ - # Disable executable stack in all libraries. This should already be the case - # but it seems libgnsdk is not playing along - && apt-get -y update \ - && apt-get -y install execstack \ - && execstack -c /output/usr/lib/*.so* + && find . -type f -exec sh -c 'file "{}" | grep -q ELF && strip --strip-debug "{}"' \; ADD --chmod=755 \ entrypoint \ - *.sh \ - /output/usr/local/bin/ + claim-server.sh \ + gen-config.sh \ + plex-util.sh \ + usr/bin/ -#========================= +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -FROM spritsail/busybox:latest +FROM scratch ARG PLEX_VER -ARG CURL_VER -ARG OPENSSL_VER ARG XMLSTAR_VER +ARG BUSYBOX_VER +ARG SU_EXEC_VER +ARG TINI_VER +ARG OPENSSL_VER +ARG CURL_VER +ARG OUTPUT LABEL maintainer="Spritsail " \ org.label-schema.vendor="Spritsail" \ @@ -196,13 +335,16 @@ LABEL maintainer="Spritsail " \ org.label-schema.description="Tiny Docker image for Plex Media Server, built on busybox" \ org.label-schema.version=${PLEX_VER} \ io.spritsail.version.plex=${PLEX_VER} \ - io.spritsail.version.curl=${CURL_VER} \ + io.spritsail.version.xmlstarlet=${XMLSTAR_VER} \ + io.spritsail.version.busybox=${BUSYBOX_VER} \ + io.spritsail.version.su-exec=${SU_EXEC_VER} \ + io.spritsail.version.tini=${TINI_VER} \ io.spritsail.version.openssl=${OPENSSL_VER} \ - io.spritsail.version.xmlstarlet=${XMLSTAR_VER} + io.spritsail.version.curl=${CURL_VER} WORKDIR /usr/lib/plexmediaserver -COPY --from=builder /output/ / +COPY --from=combine "$OUTPUT" / ENV SUID=900 SGID=900 \ PLEX_MEDIA_SERVER_MAX_PLUGIN_PROCS="6" \ @@ -222,4 +364,4 @@ RUN mkdir -p "$PLEX_MEDIA_SERVER_APPLICATION_SUPPORT_DIR" \ && ln -sfv /config "$PLEX_MEDIA_SERVER_APPLICATION_SUPPORT_DIR/Plex Media Server" ENTRYPOINT ["/sbin/tini", "--"] -CMD ["/usr/local/bin/entrypoint"] +CMD ["/usr/bin/entrypoint"] diff --git a/README.md b/README.md index 1772aa4..503a4dd 100644 --- a/README.md +++ b/README.md @@ -13,14 +13,10 @@ [![Build Status](https://drone.spritsail.io/api/badges/spritsail/plex-media-server/status.svg?branch=pass)][drone] [![Last Build](https://api.spritsail.io/badges/lastbuild/spritsail/plex-media-server:plexpass.svg)][drone] -The _smallest*_ Plex Media Server docker image, built on barebones [spritsail/busybox](https://hub.docker.com/r/spritsail/busybox/) with glibc and libraries built from source. The container hosts a fully featured Plex Media Server, with almost all of the useless crap removed, resulting in the smallest container possible whilst maintaining full functionality. - -You can find out more about the [spritsail/busybox](https://hub.docker.com/r/spritsail/busybox) base image [here](https://github.com/spritsail/busybox) +The _smallest*_ Plex Media Server docker image, built `FROM scratch` with musl provided by Plex and supporting libraries and binaries built from source. The container hosts a fully featured Plex Media Server, with almost all of the useless crap removed, resulting in the smallest container possible whilst maintaining full functionality. _*last we checked_ -**NOTICE:** This build has changed the `/config/Plex Media Server` mountpoint inside the container to now be present at `/config`. If you previously used this container, please update your mountpoint to `/config`. - ## Getting Started Navigate to [plex.tv/claim](https://www.plex.tv/claim) and obtain a token in the form `claim-xxxx...` diff --git a/claim-server.sh b/claim-server.sh index d9cf853..4b9d97d 100755 --- a/claim-server.sh +++ b/claim-server.sh @@ -2,9 +2,9 @@ set -e # Contains getPref/setPref and PREF_FILE vars -source plex-util.sh +. plex-util.sh -opts=`getopt -n "$0" -l save -l token: -l client-id: -l load-client-id -- st:c:l "$@"` || exit 1 +opts=$(getopt -n "$0" -l save -l token: -l client-id: -l load-client-id -- st:c:l "$@") || exit 1 eval set -- "$opts" while true; do case "$1" in @@ -34,15 +34,16 @@ fi >&2 echo "Attempting to obtain server token from claim token" loginInfo="$(curl -X POST \ - -H 'X-Plex-Client-Identifier: '${clientId} \ - -H 'X-Plex-Product: Plex Media Server'\ - -H 'X-Plex-Version: 1.1' \ - -H 'X-Plex-Provides: server' \ - -H 'X-Plex-Platform: Linux' \ - -H 'X-Plex-Platform-Version: 1.0' \ - -H 'X-Plex-Device-Name: PlexMediaServer' \ - -H 'X-Plex-Device: Linux' \ - "https://plex.tv/api/claim/exchange?token=${claimToken}")" + -H "X-Plex-Client-Identifier: ${clientId}" \ + -H "X-Plex-Product: Plex Media Server" \ + -H "X-Plex-Version: 1.1" \ + -H "X-Plex-Provides: server" \ + -H "X-Plex-Platform: Linux" \ + -H "X-Plex-Platform-Version: 1.0" \ + -H "X-Plex-Device-Name: PlexMediaServer" \ + -H "X-Plex-Device: Linux" \ + "https://plex.tv/api/claim/exchange?token=${claimToken}" +)" authtoken="$(echo "$loginInfo" | sed -n 's/.*\(.*\)<\/authentication-token>.*/\1/p')"