4

I'm trying to deploy Craft CMS to zeit/now using Docker. It works locally, but zeit has an image size limit of 100MB. My container is currently 176MB.

It's a Docker image using alpine, nginx and php modules required by Craft, and using a multi stage build for building Composer components, to reduce size of build artifacts.

This is the Dockerfile:

FROM zeit/wait-for:0.2 as wait

# Build dependencies
FROM composer:latest as vendor

COPY composer.json composer.json
COPY composer.lock composer.lock

RUN composer install --ignore-platform-reqs --no-interaction --no-plugins --no-scripts --prefer-dist --no-dev

FROM alpine:3.8

LABEL maintainer="Eivind Mikael Lindbråten <eivindml@icloud.com>"
LABEL description="Minimal Craft CMS Container using nginx."

# install nginx, php, and php extensions for Craft
RUN apk add --no-cache \
    bash \
    nginx \
    php7 \
    php7-fpm \
    php7-opcache \
    php7-phar \
    php7-zlib \
    php7-ctype \
    php7-session \
    php7-fileinfo \
# Required php extensions for Craft
    php7-pdo \
    php7-pdo_mysql \
    php7-gd \
    php7-openssl \
    php7-mbstring \
    php7-json \
    php7-curl \
    php7-zip \
# Optional extensions for Craft
    php7-iconv \
    php7-intl \
    php7-dom

COPY nginx.conf /etc/nginx/nginx.conf
COPY www.conf /etc/php7/php-fpm.d/

# Copy over Craft files
COPY config/ /www/config
COPY modules/ /www/modules
COPY storage/ /www/storage
COPY templates/ /www/templates
COPY storage/ /www/storage
COPY web/ /www/web
COPY .env /www/.env
COPY composer.json /www/composer.json
COPY composer.lock /www/composer.lock

# Copy over vendor files
COPY --from=vendor /app/vendor /www/vendor

# Set permissions
RUN chmod 777 -R /www/config
RUN chmod 777 -R /www/vendor
RUN chmod 777 -R /www/storage
RUN chmod 777 -R /www/web/cpresources
RUN chmod 777 /www/.env
RUN chmod 777 /www/composer.json
RUN chmod 777 /www/composer.lock

# Expose default port
EXPOSE 80

SHELL ["/bin/bash", "-c"]
COPY --from=wait /bin/wait-for /bin/wait-for

CMD php-fpm7 -F & (wait-for /tmp/php7-fpm.sock && nginx) & wait -n

Any ideas how I can reduce this size further?

eivindml
  • 2,197
  • 7
  • 36
  • 68

2 Answers2

5

Edit 2018-09-26: Lots of edits to tidy up my mistakes and include all the dependencies. Much of my previous poking around is irrelevant, now that I've included all the dependencies and have numbers which add up sensibly.


I built an image from the provided repository in order to see the complete list of packages installed, including dependencies. My build did not complete, but it got far enough to produce an image (5ed25a4a3cf1) which shows where the space is. Part way through the build, at the end of the RUN apk add ..., it said this:

OK: 150 MiB in 102 packages

Looking at the image, we see that the packages are 145MB according to docker history (image layer 8138a6c99655 - you probably need to scroll right to see the size column):

user@host:~/docker-craft-nginx$ sudo docker history 5ed25a4a3cf1
IMAGE               CREATED              CREATED BY                                      SIZE                COMMENT
5ed25a4a3cf1        About a minute ago   /bin/sh -c #(nop) COPY dir:e9a848580d7409c11…   0B
79bba3526427        About a minute ago   /bin/sh -c #(nop) COPY dir:41ddb696977d39ee6…   7.38kB
f4d1e79f00b4        About a minute ago   /bin/sh -c #(nop) COPY dir:e9a848580d7409c11…   21B
ab8ad35f5a93        About a minute ago   /bin/sh -c #(nop) COPY dir:4ff26c2555a73b795…   1.18kB
29a6368b96c5        About a minute ago   /bin/sh -c #(nop) COPY dir:cb92d968d83d14948…   3.43kB
ea429fb6f1fa        About a minute ago   /bin/sh -c #(nop) COPY file:b1cc7638b7536f51…   139B
f0e1dbcec6c5        About a minute ago   /bin/sh -c #(nop) COPY file:e0f1165c2cf43ac3…   1.07kB
8138a6c99655        About a minute ago   /bin/sh -c apk add --no-cache     bash     n…   145MB
b743c478b647        2 minutes ago        /bin/sh -c #(nop)  LABEL description=Minimal…   0B
f3dab9765884        2 minutes ago        /bin/sh -c #(nop)  LABEL maintainer=Eivind M…   0B
196d12cf6ab1        11 days ago          /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B
<missing>           11 days ago          /bin/sh -c #(nop) ADD file:25c10b1d1b41d46a1…   4.41MB

Given the complete list of packages to install (they're shown during the image build), we can get their individual sizes using apk info -s NAME_OF_PACKAGE

(I've sorted them in order by decreasing size, and am only showing the first 20)

user@host:~$ sudo docker run -it alpine sh -c "apk update; apk info -s ncurses-terminfo-base ncurses-terminfo ncurses-libs readline bash libxau libbsd libxdmcp libxcb libx11 libxext libbz2 expat libpng freetype fontconfig libgcc libgomp lcms2 libltdl libxml2 imagemagick-libs libxrender pixman cairo libffi libintl libuuid libblkid libmount pcre glib dbus-libs avahi-libs gmp nettle p11-kit libtasn1 libunistring gnutls libstdc++ cups-libs jbig2dec libjpeg-turbo tiff ghostscript libxft graphite2 harfbuzz pango libcroco shared-mime-info gdk-pixbuf librsvg libwebp imagemagick nginx php7-common libedit php7 php7-ctype ca-certificates nghttp2-libs libssh2 libcurl php7-curl php7-dom php7-fileinfo php7-fpm libice libsm libxt libxpm php7-gd php7-iconv php7-imagick icu-libs php7-intl php7-json php7-mbstring php7-opcache php7-openssl php7-pdo php7-mysqlnd php7-pdo_mysql php7-phar php7-session libzip php7-zip" | awk '/^[0-9]/{gsub(/[^0-9]+$/,"",$1); print $1,prev,"\r"; next}{prev=$1}' | sort -gr | head -n 20
50036736 ghostscript-9.24-r0
31248384 icu-libs-60.2-r2
7245824 ncurses-terminfo-6.1_p20180818-r1
5070848 php7-fileinfo-7.2.10-r0
4849664 php7-fpm-7.2.10-r0
4775936 php7-7.2.10-r0
4489216 imagemagick-7.0.7.32-r0
3440640 imagemagick-libs-7.0.7.32-r0
3379200 libx11-1.6.5-r1
3010560 glib-2.56.1-r0
2338816 shared-mime-info-1.9-r0
2203648 harfbuzz-1.7.6-r1
1638400 php7-mbstring-7.2.10-r0
1470464 libunistring-0.9.7-r0
1384448 libstdc++-6.4.0-r8
1282048 gnutls-3.6.2-r0
1236992 p11-kit-0.23.10-r0
1224704 libxml2-2.9.8-r0
1187840 libcroco-0.6.12-r1
1175552 nginx-1.14.0-r1

Or a grand total size of all the packages put together:

user@host:~$ sudo docker run -it alpine sh -c "apk update; apk info -s ncurses-terminfo-base ncurses-terminfo ncurses-libs readline bash libxau libbsd libxdmcp libxcb libx11 libxext libbz2 expat libpng freetype fontconfig libgcc libgomp lcms2 libltdl libxml2 imagemagick-libs libxrender pixman cairo libffi libintl libuuid libblkid libmount pcre glib dbus-libs avahi-libs gmp nettle p11-kit libtasn1 libunistring gnutls libstdc++ cups-libs jbig2dec libjpeg-turbo tiff ghostscript libxft graphite2 harfbuzz pango libcroco shared-mime-info gdk-pixbuf librsvg libwebp imagemagick nginx php7-common libedit php7 php7-ctype ca-certificates nghttp2-libs libssh2 libcurl php7-curl php7-dom php7-fileinfo php7-fpm libice libsm libxt libxpm php7-gd php7-iconv php7-imagick icu-libs php7-intl php7-json php7-mbstring php7-opcache php7-openssl php7-pdo php7-mysqlnd php7-pdo_mysql php7-phar php7-session libzip php7-zip" | awk '/^[0-9]+/ {s+=$1} END {printf "%.0f\n", s}'
152952832

At this point, it looks like your problem is a "your stuff is too big" problem, rather than a docker problem. You may or may not have known that part before, but I've learned some stuff about digging in docker, so it's been fun :-)


Edit 2018-09-25: This is now not so useful, after correcting my earlier errors, but perhaps still has some relevant information: It occurs to me that even though my build of the full image failed, I don't care why - we're only interested in poking around in the big layer where the apks are installed. So, I made a fairly minimal dockerfile:

FROM alpine:3.8

# install nginx, php, and php extensions for Craft
RUN apk add --no-cache \
    bash \
    nginx \
    php7 \
    php7-fpm \
    php7-opcache \
    php7-phar \
    php7-zlib \
    php7-ctype \
    php7-session \
    php7-fileinfo \
# Required php extensions for Craft
    php7-pdo \
    php7-pdo_mysql \
    php7-gd \
    php7-openssl \
    php7-mbstring \
    php7-json \
    php7-curl \
    php7-zip \
# Optional extensions for Craft
    php7-iconv \
    php7-intl \
    php7-dom \
# Extra Optional extensions for Craft
    imagemagick \
    php7-imagick

CMD sh

Built it: sudo docker build . and got an image Successfully built e344a23763c9. Running a container from this image with sudo docker run -it e344a23763c9 I got a shell. Installed ncdu (which I could have installed via the dockerfile) with apk add --no-cache ncdu then ran ncdu / - now I can easily see where the big directories are, starting at the root (Note: I have trimmed small files & dirs from the output):

  146.0 MiB [##########] /usr                                                                                                                                  
    3.6 MiB [          ] /lib
    2.0 MiB [          ] /etc
    1.4 MiB [          ] /bin

Navigating to /usr we find:

   84.4 MiB [##########] /lib
   35.6 MiB [####      ] /share
   20.5 MiB [##        ] /bin
    5.5 MiB [          ] /sbin

In /usr/lib:

   25.7 MiB [##########]  libicudata.so.60.2
   15.0 MiB [#####     ]  libgs.so.9.24
    9.0 MiB [###       ] /php7
    3.9 MiB [#         ] /ImageMagick-7.0.7
    2.3 MiB [          ]  libicui18n.so.60.2
    2.2 MiB [          ]  libMagickCore-7.Q16HDRI.so.6.0.0
    1.5 MiB [          ]  libicuuc.so.60.2
    1.4 MiB [          ]  libgio-2.0.so.0.5600.1
    1.4 MiB [          ]  libunistring.so.2.0.0
    1.3 MiB [          ]  libstdc++.so.6.0.22
    1.2 MiB [          ]  libgnutls.so.30.20.2
    1.2 MiB [          ]  libxml2.so.2.9.8
    1.1 MiB [          ]  libX11.so.6.3.0
    1.1 MiB [          ]  libMagickWand-7.Q16HDRI.so.6.0.0
    1.1 MiB [          ]  libp11-kit.so.0.3.0

In /usr/share:

   17.7 MiB [##########] /ghostscript
    6.9 MiB [###       ] /terminfo
    5.6 MiB [###       ] /mime
    2.3 MiB [#         ] /gtk-doc
    2.1 MiB [#         ] /X11

In /usr/bin:

   14.8 MiB [##########]  gs
    4.5 MiB [###       ]  php7

It looks to me like imagemagick is the single biggest contributor (along with its dependency ghostscript). Running a trial build without imagemagick & php7-imagick yields an image layer size of 65.8MB (as reported by docker history).

Removing php7-intl gets the layer down to 32.7MB (mostly by removing libicudata.so.60.2 which is part of "International Components for Unicode")

If you want a smaller container than your original, I think you you either need to do away with image processing and internationalisation, or find smaller ways to achieve them - they're far and away the largest possibly-trimmable components (depending on what you want to achieve).


You can get a tiny reduction in size/layers by combining your chmod calls:

RUN chmod 777 -R \
   /www/config \
   /www/vendor \
   /www/storage \
   /www/web/cpresources \
&& chmod 777 \
   /www/.env \
   /www/composer.json \
   /www/composer.lock

Edit 2018-09-29: I notice that the terminfo database in the image is [probably] excessive - almost 7MB according to the alpine package database entry for ncurses-terminfo. It has terminfos for virtually every possible term, which seems like overkill for a docker container (depending on what you are doing).

I can't see an easy way to not install that package (other things require it to be installed and I couldn't find a sensible way to force apk to not honor the dependency), but you could get the RUN apk add ... line to delete the unused terminfos before the layer is committed - delete most of these files.

KarlMW
  • 301
  • 2
  • 5
  • Hi, thank you very much for this. I have a repo with a minimal working example: https://github.com/eivindml/docker-craft-nginx – eivindml Sep 22 '18 at 06:41
  • I've been trying to figure out how to mount the docker image layer created by the `apk add` (layer `8138a6c99655`) on my host filesystem so I can poke around in it and see if there is anything extraneous in there. I could probably change the docker storage driver to something else (I'm sure one of the other drivers stored layers in directly accessible form), but I'm not quite that committed, yet. – KarlMW Sep 23 '18 at 03:56
  • 1
    Wow. This is really helpful and interesting. Thank you. I guess if the problem is "php is too big", there isn't much to do about it? I tried to not install the optional php packages, but the size didn't change at all. – eivindml Sep 24 '18 at 09:33
  • I got a tips about this site: https://microbadger.com/images/eivindml/docker-craft-nginx It breaks down the size. But here it says 87mb. – eivindml Sep 24 '18 at 20:04
  • 1
    The 87MB reported by microbadger is interesting. I don't have any more ideas today, but am curious to investigate a bit more. After today's experiments with trimming out imagemagick, I notice that my list of packages and sizes (the first big code block in my answer) still isn't right - when I include everything, the numbers do add up to about 150MB. I'll update my post tomorrow. – KarlMW Sep 25 '18 at 07:49
  • 1
    I've updated and tidied my answer - quite a lot of edits, but I think it all makes sense, and I've learned stuff I can use again in the future. – KarlMW Sep 26 '18 at 08:07
  • Wow. This is super useful. :) The only thing now is why https://microbadger.com/images/eivindml/docker-craft-nginx is reporting 87mb. It looks like there is different ways of calculating it? They call it 'download size', so maybe that's another thing then the size of each folder when ssh'ing into it. And maybe that is the value Zeit/now is looking at, which is why it's deployed, even when they have a 100mb limit. – eivindml Sep 26 '18 at 09:42
  • 87MB sounds suspiciously close to half of 176MB. I had a wild theory about 32bit vs 64bit systems, but the standard alpine images on docker hub are all 64bit, so that's not a problem. Wild theories are fun, though - you never know where you might end up. – KarlMW Sep 28 '18 at 07:58
-2

One thing you can certainly do is remove the apk cache after all apk add calls are done:

RUN rm -rf /var/cache/apk
Mureinik
  • 297,002
  • 52
  • 306
  • 350
  • 5
    I'm using the `--no-cache` flag for `apk`. [As of Alpine Linux 3.3 there exists a new --no-cache option for apk. It allows users to install packages with an index that is updated and used on-the-fly and not cached locally. This avoids the need to use --update and remove /var/cache/apk/* when done installing packages.](https://github.com/gliderlabs/docker-alpine/blob/master/docs/usage.md) – eivindml Sep 22 '18 at 06:37