Enhancement: Add Docker Hub image (#965)

* Enhancement: Add Docker Hub image

* Push to ghcr.io

* Add all `org.opencontainers.image.xxx` labels using `--label` instead of `LABEL`

* Change `php-fpm` to listen on `:9000` to expose prometheus metrics via `php-fpm_exporter`

* Include `yarn.lock` to speed up builds

* Reorder

* Use correct sha for pr

* Add logging before overriding values in config

* Add `DEBUG=true` env var for verbose `entrypoint.sh` logs

* Print php exception when snappymail fails to generate data directory and config file on the very first time

* Log snappymail version

* Fix COPY statement

* Add .dockerignore

* Add `USER` and `ENTRYPOINT`

* Update `.dockerignore`

* Add docker image test

* Push only if image test succeeds

* Log when startup is successful

* Use plain `docker build` in `build_and_test.sh`

* Fix test output
This commit is contained in:
leo 2023-11-20 14:01:26 +00:00 committed by GitHub
parent 6552a93989
commit 24dbff999e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 5201 additions and 229 deletions

View file

@ -1,51 +1,116 @@
# syntax=docker/dockerfile:1
FROM alpine:3.15 AS builder
RUN apk add --no-cache php7 php7-json php-phar php-zip
RUN apk add --no-cache npm
RUN npm install -g gulp yarn
WORKDIR /source
COPY package.json yarn.lock ./
RUN yarn install
COPY . .
# Patch release.php with hotfix from: https://github.com/xgbstar1/snappymail-docker/blob/main/Dockerfile, so that release.php doesn't fail with error
RUN sed -i 's_^if.*rename.*snappymail.v.0.0.0.*$_if (!!system("mv snappymail/v/0.0.0 snappymail/v/{$package->version}")) {_' cli/release.php || true
RUN php release.php
RUN set -eux; \
VERSION=$( ls build/dist/releases/webmail ); \
ls -al build/dist/releases/webmail/$VERSION/snappymail-$VERSION.tar.gz; \
mkdir -p /snappymail; \
tar -zxvf build/dist/releases/webmail/$VERSION/snappymail-$VERSION.tar.gz -C /snappymail; \
find /snappymail -type d -exec chmod 550 {} \; ; \
find /snappymail -type f -exec chmod 440 {} \; ; \
find /snappymail/data -type d -exec chmod 750 {} \; ; \
# Remove unneeded files
rm -v /snappymail/README.md /snappymail/_include.php
# Inspired by the original Rainloop dockerfile from youtous on GitLab
FROM php:8.1-fpm-bullseye
FROM php:8.1-fpm-alpine AS final
ARG FILES_ZIP
LABEL org.label-schema.description="SnappyMail webmail client image using nginx, php-fpm based on Debian Buster"
ENV UID=991 GID=991 UPLOAD_MAX_SIZE=25M LOG_TO_STDERR=true MEMORY_LIMIT=128M SECURE_COOKIES=true
ENV fpm.pool.clear_env=false
LABEL org.label-schema.description="SnappyMail webmail client image using nginx, php-fpm on Alpine"
# Install dependencies such as nginx
RUN mkdir -p /usr/share/man/man1/ /usr/share/man/man3/ /usr/share/man/man7/ && \
apt-get update -q --fix-missing && \
apt-get -y upgrade && \
apt-get install --no-install-recommends -y \
apt-transport-https gnupg openssl wget curl ca-certificates nginx supervisor sudo \
unzip libzip-dev libxml2-dev libldb-dev libldap2-dev \
sqlite3 libsqlite3-dev libsqlite3-0 libpq-dev postgresql-client mariadb-client logrotate \
zip mlocate libpcre3-dev libicu-dev \
build-essential chrpath libssl-dev \
libxft-dev libfreetype6 libfreetype6-dev \
libpng-dev libjpeg62-turbo-dev \
libfontconfig1 libfontconfig1-dev \
&& \
rm -rf /var/lib/apt/lists/*
RUN apk add --no-cache ca-certificates nginx supervisor bash
# Install PHP extensions
RUN php -m && \
docker-php-ext-configure ldap --with-libdir=lib/$(uname -m)-linux-gnu/ && \
docker-php-ext-configure intl && \
docker-php-ext-configure gd --with-freetype --with-jpeg && \
docker-php-ext-install ldap opcache pdo_mysql pdo_pgsql zip intl gd && \
php -m
# gd
RUN set -eux; \
apk add --no-cache freetype libjpeg-turbo libpng; \
apk add --no-cache --virtual .deps freetype-dev libjpeg-turbo-dev libpng-dev; \
docker-php-ext-configure gd --with-freetype --with-jpeg; \
docker-php-ext-install gd; \
apk del .deps
# gnupg
RUN set -eux; \
apk add --no-cache gnupg gpgme; \
apk add --no-cache --virtual .deps gpgme-dev; \
apk add --no-cache --virtual .build-dependencies $PHPIZE_DEPS; \
pecl install gnupg; \
docker-php-ext-enable gnupg; \
docker-php-source delete; \
apk del .build-dependencies; \
apk del .deps
# intl
RUN set -eux; \
apk add --no-cache icu-libs; \
apk add --no-cache --virtual .deps icu-dev; \
docker-php-ext-configure intl; \
docker-php-ext-install intl; \
apk del .deps
# ldap
RUN set -eux; \
apk add --no-cache libldap; \
apk add --no-cache --virtual .deps openldap-dev; \
docker-php-ext-configure ldap; \
docker-php-ext-install ldap; \
apk del .deps
# mysql
RUN docker-php-ext-install pdo_mysql
# opcache
RUN docker-php-ext-install opcache
# postgres
RUN set -eux; \
apk add --no-cache postgresql-libs; \
apk add --no-cache --virtual .deps postgresql-dev; \
docker-php-ext-install pdo_pgsql; \
apk del .deps
# redis
RUN set -eux; \
apk add --no-cache liblzf zstd-libs; \
apk add --no-cache --virtual .deps zstd-dev; \
apk add --no-cache --virtual .build-dependencies $PHPIZE_DEPS; \
pecl install igbinary; \
docker-php-ext-enable igbinary; \
pecl install --configureoptions 'enable-redis-igbinary="yes" enable-redis-lzf="yes" enable-redis-zstd="yes"' redis; \
docker-php-ext-enable redis; \
docker-php-source delete; \
apk del .build-dependencies; \
apk del .deps
# zip
RUN set -eux; \
apk add --no-cache libzip; \
apk add --no-cache --virtual .deps libzip-dev; \
docker-php-ext-install zip; \
apk del .deps
# Install snappymail
WORKDIR /tmp
COPY ${FILES_ZIP} .
RUN mkdir /snappymail && \
unzip -q ${FILES_ZIP} -d /snappymail && \
find /snappymail -type d -exec chmod 755 {} \; && \
find /snappymail -type f -exec chmod 644 {} \; && \
rm -rf ${FILES_ZIP}
# The 'www-data' user/group in alpine is 82:82. The 'nginx' user/group in alpine is 101:101, and is part of www-data group
COPY --chown=www-data:www-data --from=builder /snappymail /snappymail
# Use a custom snappymail data folder
RUN mv -v /snappymail/data /var/lib/snappymail;
# Setup configs
COPY --chown=root:root .docker/release/files /
RUN set -eux; \
chown www-data:www-data /snappymail/include.php; \
chmod 440 /snappymail/include.php; \
chmod +x /entrypoint.sh; \
# Disable the built-in php-fpm configs, since we're using our own config
mv -v /usr/local/etc/php-fpm.d/docker.conf /usr/local/etc/php-fpm.d/docker.conf.disabled; \
mv -v /usr/local/etc/php-fpm.d/www.conf /usr/local/etc/php-fpm.d/www.conf.disabled; \
mv -v /usr/local/etc/php-fpm.d/zz-docker.conf /usr/local/etc/php-fpm.d/zz-docker.conf.disabled;
# Install other content
COPY files /
RUN chmod +x /entrypoint.sh && chmod +x /logrotate-loop.sh
VOLUME /snappymail/data
USER root
WORKDIR /snappymail
VOLUME /var/lib/snappymail
EXPOSE 8888
EXPOSE 9000
ENTRYPOINT []
CMD ["/entrypoint.sh"]

92
.docker/release/files/entrypoint.sh Normal file → Executable file
View file

@ -1,23 +1,20 @@
#!/bin/sh
set -eu
# Create not root user
groupadd --gid "$GID" php-cli -f
adduser --uid "$UID" --disabled-password --gid "$GID" --shell /bin/bash --home /home/php-cli php-cli --force --gecos ""
DEBUG=${DEBUG:-}
if [ "$DEBUG" = 'true' ]; then
set -x
fi
UPLOAD_MAX_SIZE=${UPLOAD_MAX_SIZE:-25M}
MEMORY_LIMIT=${MEMORY_LIMIT:-128M}
SECURE_COOKIES=${SECURE_COOKIES:-true}
# Set attachment size limit
sed -i "s/<UPLOAD_MAX_SIZE>/$UPLOAD_MAX_SIZE/g" /usr/local/etc/php-fpm.d/php-fpm.conf /etc/nginx/nginx.conf
sed -i "s/<MEMORY_LIMIT>/$MEMORY_LIMIT/g" /usr/local/etc/php-fpm.d/php-fpm.conf
# Set log output to STDERR if wanted (LOG_TO_STDERR=true)
if [ "$LOG_TO_STDERR" = true ]; then
echo "[INFO] Logging to stderr activated"
sed -i "s/.*error_log.*$/error_log \/dev\/stderr warn;/" /etc/nginx/nginx.conf
sed -i "s/.*error_log.*$/php_admin_value[error_log] = \/dev\/stderr/" /usr/local/etc/php-fpm.d/php-fpm.conf
fi
# Secure cookies
if [ "${SECURE_COOKIES}" = true ]; then
if [ "${SECURE_COOKIES}" = 'true' ]; then
echo "[INFO] Secure cookies activated"
{
echo 'session.cookie_httponly = On';
@ -26,43 +23,58 @@ if [ "${SECURE_COOKIES}" = true ]; then
} > /usr/local/etc/php/conf.d/cookies.ini;
fi
# Copy snappymail default config if absent
SNAPPYMAIL_CONFIG_FILE=/snappymail/data/_data_/_default_/configs/application.ini
echo "[INFO] Snappymail version: $( ls /snappymail/snappymail/v )"
# Set permissions on snappymail data
echo "[INFO] Setting permissions on /var/lib/snappymail"
chown -R www-data:www-data /var/lib/snappymail/
chmod 550 /var/lib/snappymail/
find /var/lib/snappymail/ -type d -exec chmod 750 {} \;
# Create snappymail default config if absent
SNAPPYMAIL_CONFIG_FILE=/var/lib/snappymail/_data_/_default_/configs/application.ini
if [ ! -f "$SNAPPYMAIL_CONFIG_FILE" ]; then
echo "[INFO] Creating default Snappymail configuration"
mkdir -p $(dirname $SNAPPYMAIL_CONFIG_FILE)
cp /usr/local/include/application.ini $SNAPPYMAIL_CONFIG_FILE
echo "[INFO] Creating default Snappymail configuration: $SNAPPYMAIL_CONFIG_FILE"
# Run snappymail and exit. This populates the snappymail data directory and generates the config file
# On error, print php exception and exit
EXITCODE=
su - www-data -s /bin/sh -c 'php /snappymail/index.php' > /tmp/out || EXITCODE=$?
if [ -n "$EXITCODE" ]; then
cat /tmp/out
exit "$EXITCODE"
fi
fi
echo "[INFO] Overriding values in snappymail configuration: $SNAPPYMAIL_CONFIG_FILE"
# Enable output of snappymail logs
if [ "${LOG_TO_STDERR}" = true ]; then
sed -z 's/\; Enable logging\nenable = Off/\; Enable logging\nenable = On/' -i $SNAPPYMAIL_CONFIG_FILE
sed 's/^filename = .*/filename = "errors.log"/' -i $SNAPPYMAIL_CONFIG_FILE
sed 's/^write_on_error_only = .*/write_on_error_only = Off/' -i $SNAPPYMAIL_CONFIG_FILE
sed 's/^write_on_php_error_only = .*/write_on_php_error_only = On/' -i $SNAPPYMAIL_CONFIG_FILE
else
sed -z 's/\; Enable logging\nenable = On/\; Enable logging\nenable = Off/' -i $SNAPPYMAIL_CONFIG_FILE
fi
sed '/^\; Enable logging/{
N
s/enable = Off/enable = On/
}' -i $SNAPPYMAIL_CONFIG_FILE
# Redirect snappymail logs to stderr /stdout
sed 's/^filename = .*/filename = "stderr"/' -i $SNAPPYMAIL_CONFIG_FILE
sed 's/^write_on_error_only = .*/write_on_error_only = Off/' -i $SNAPPYMAIL_CONFIG_FILE
sed 's/^write_on_php_error_only = .*/write_on_php_error_only = On/' -i $SNAPPYMAIL_CONFIG_FILE
# Always enable snappymail Auth logging
sed 's/^auth_logging = .*/auth_logging = On/' -i $SNAPPYMAIL_CONFIG_FILE
sed 's/^auth_logging_filename = .*/auth_logging_filename = "auth.log"/' -i $SNAPPYMAIL_CONFIG_FILE
sed 's/^auth_logging_format = .*/auth_logging_format = "[{date:Y-m-d H:i:s}] Auth failed: ip={request:ip} user={imap:login} host={imap:host} port={imap:port}"/' -i $SNAPPYMAIL_CONFIG_FILE
# Redirect snappymail logs to stderr /stdout
mkdir -p /snappymail/data/_data_/_default_/logs/
# empty logs
cp /dev/null /snappymail/data/_data_/_default_/logs/errors.log
cp /dev/null /snappymail/data/_data_/_default_/logs/auth.log
chown -R php-cli:php-cli /snappymail/data/
sed 's/^auth_syslog = .*/auth_syslog = Off/' -i $SNAPPYMAIL_CONFIG_FILE
# Fix permissions
chown -R $UID:$GID /snappymail/data /var/log /var/lib/nginx
chmod o+w /dev/stdout
chmod o+w /dev/stderr
(
while ! nc -vz -w 1 localhost 8888 > /dev/null 2>&1; do echo "[INFO] Checking whether nginx is alive"; sleep 1; done
while ! nc -vz -w 1 localhost 9000 > /dev/null 2>&1; do echo "[INFO] Checking whether php-fpm is alive"; sleep 1; done
# Create snappymail admin password if absent
SNAPPYMAIL_ADMIN_PASSWORD_FILE=/var/lib/snappymail/_data_/_default_/admin_password.txt
if [ ! -f "$SNAPPYMAIL_ADMIN_PASSWORD_FILE" ]; then
echo "[INFO] Creating Snappymail admin password file: $SNAPPYMAIL_ADMIN_PASSWORD_FILE"
wget -T 1 -qO- 'http://localhost:8888/?/AdminAppData/0/12345/' > /dev/null
echo "[INFO] Snappymail Admin Panel ready at http://localhost:8888/?admin. Login using password in $SNAPPYMAIL_ADMIN_PASSWORD_FILE"
fi
# Touch supervisord PID file in order to fix permissions
touch /run/supervisord.pid
chown php-cli:php-cli /run/supervisord.pid
wget -T 1 -qO- 'http://localhost:8888/' > /dev/null
echo "[INFO] Snappymail ready at http://localhost:8888/"
) &
# RUN !
exec sudo -u php-cli -g php-cli /usr/bin/supervisord -c '/supervisor.conf' --pidfile '/run/supervisord.pid'
exec /usr/bin/supervisord -c /supervisor.conf --pidfile /run/supervisord.pid

View file

@ -1,5 +0,0 @@
/snappymail/data/_data_/_default_/logs/* {
size 10M
rotate 0
missingok
}

View file

@ -11,7 +11,7 @@ http {
default_type application/octet-stream;
access_log off;
error_log /tmp/ngx_error.log error;
error_log /dev/stderr error;
sendfile on;
keepalive_timeout 15;
@ -95,7 +95,7 @@ http {
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param HTTP_PROXY "";
fastcgi_index index.php;
fastcgi_pass unix:/tmp/php-fpm.sock;
fastcgi_pass 127.0.0.1:9000;
fastcgi_intercept_errors on;
fastcgi_request_buffering off;
fastcgi_param REMOTE_ADDR $http_x_real_ip;

View file

@ -1,113 +0,0 @@
#!/bin/bash
# ----------------------------------------------------------------------
# Simple script to invoke logrotate at regular intervals
# ----------------------------------------------------------------------
# from https://github.com/misho-kr/docker-appliances/blob/master/nginx-nodejs/logrotate-loop.sh
LOGROTATE_BIN="logrotate"
STATE="$HOME/logrotate.state"
CONF="/etc/logrotate.d/snappymail"
export LOGROTATE_BIN STATE CONF
RUN_INTERVAL="3600" # every hour
# helper functions for logging
export FMT="%a %b %d %Y %H:%M:%S GMT%z (%Z)"
function log_date() {
echo "$(date +"$FMT"): $*"
}
# ----------------------------------------------------------------------
# Main loop of the logrotate service:
#
# while True:
# sleep N seconds
# run logrotate
#
# ----------------------------------------------------------------------
function logrotate_loop() {
trap on_terminate TERM INT
local interval="${1}"
log_date "===================================================="
log_date
log_date "logrotate service starting (pid=$$)"
log_date "logrotate process will run every ${interval} seconds"
while true; do
current_time=$(date "+%s")
next_run_time=$(( current_time + interval ))
while (( current_time < next_run_time ))
do
logrotate_sleep $(( next_run_time - current_time ))
current_time=$(date "+%s")
done
logrotate_run
done
}
# helper function to execute logrotate and pass it the right parameters
function logrotate_run() {
log_date "logrotate will run now"
${LOGROTATE_BIN} -s ${STATE} ${CONF}
}
# ----------------------------------------------------------------------
# Procedure to idle the execution for a number of seconds
#
# There are two requirements:
#
# - export the PID of the sleep command so that it can be terminated
# in case the logrotate service is being shutdown
# - keep this (bash) process responsive to SIGTERM while in sleep
# mode (normally the signal will be masked and will not be delivered
# until the subprocess completes)
# ----------------------------------------------------------------------
proc_sleep_pid=""
function logrotate_sleep() {
local sleep_interval=${1}
log_date "logrotate will sleep for ${sleep_interval} seconds"
( exec -a "logrotate: sleep" sleep ${sleep_interval} )&
proc_sleep_pid=$!
wait ${proc_sleep_pid}
}
# ----------------------------------------------------------------------
# Signal handler for logrotate service to make sure the process exits:
#
# - properly by terminating the sleep process that is used to idle
# the service
# - gracefully by writing a message in the log
# ----------------------------------------------------------------------
function on_terminate() {
log_date "logrotate will terminate"
log_date
kill -TERM ${proc_sleep_pid}
exit 0
}
# ----------------------------------------------------------------------
# main
# ----------------------------------------------------------------------
logrotate_loop ${1:-RUN_INTERVAL}

View file

@ -0,0 +1,3 @@
<?php
define('APP_DATA_FOLDER_PATH', '/var/lib/snappymail/');
?>

View file

@ -1,10 +1,13 @@
[supervisord]
nodaemon=true
user=root
logfile=/dev/null
logfile_maxbytes=0
[program:nginx]
command=nginx -c /etc/nginx/nginx.conf -g 'daemon off;'
process_name=%(program_name)s_%(process_num)02d
user=php-cli
user=root
numprocs=1
autostart=true
autorestart=false
@ -17,7 +20,7 @@ stderr_logfile_maxbytes=0
[program:php-fpm]
command=php-fpm -F
process_name=%(program_name)s_%(process_num)02d
user=php-cli
user=root
numprocs=1
autostart=true
autorestart=false
@ -27,34 +30,11 @@ stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
; reads snappymail logs
[program:snappymail-auth]
command=tail -f /snappymail/data/_data_/_default_/logs/auth.log
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:snappymail-errors]
command=tail -f /snappymail/data/_data_/_default_/logs/errors.log
# everything is an error
stdout_logfile=/dev/stderr
stdout_logfile_maxbytes=0
redirect_stderr=true
[program:logrotate]
command=/logrotate-loop.sh
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[eventlistener:subprocess-stopped]
command=php /listener.php
process_name=%(program_name)s_%(process_num)02d
user=php-cli
user=root
numprocs=1
events=PROCESS_STATE_EXITED,PROCESS_STATE_STOPPED,PROCESS_STATE_FATAL
autostart=true
autorestart=unexpected
autorestart=unexpected

View file

@ -1,15 +1,21 @@
[global]
daemonize = no
error_log = /dev/stderr
log_buffering = no
[default]
listen = /tmp/php-fpm.sock
listen = 9000
user = www-data
listen.owner = www-data
listen.group = www-data
pm = ondemand
pm.max_children = 30
pm.process_idle_timeout = 10s
pm.max_requests = 500
catch_workers_output = yes
decorate_workers_output = no
chdir = /
php_admin_value[error_log] = /tmp/php_error.log
pm.status_path = /status
php_admin_value[log_errors] = On
php_admin_value[expose_php] = Off
php_admin_value[display_errors] = Off

View file

@ -0,0 +1,7 @@
#!/bin/sh
# This script uses docker buildx to build a docker image, and loads it into the docker daemon. Then it executes ./test.sh to test the docker image
# It is useful for testing release builds in development
set -eu
IMAGE=snappymail/snappymail:test
DOCKER_BUILDX=1 docker build -t "$IMAGE" -f .docker/release/Dockerfile .
.docker/release/test/test.sh "$IMAGE"

View file

@ -0,0 +1,29 @@
# See: https://github.com/GoogleContainerTools/container-structure-test
# See: https://github.com/GoogleContainerTools/container-structure-test/issues/78
schemaVersion: 2.0.0
commandTests:
- name: Integration test
command: /bin/sh
args:
- -c
- |
set -eux
DEBUG=true /entrypoint.sh > /tmp/test 2>&1 &
sleep 5
cat /tmp/test
pidof supervisord
pidof nginx
pidof php-fpm
ls -al /var/lib/snappymail/_data_/_default_/configs/application.ini
ls -al /var/lib/snappymail/_data_/_default_/admin_password.txt
nc -vz localhost 8888
nc -vz localhost 9000
wget -S -T 3 -O /dev/null http://localhost:8888
kill `pidof supervisord`
metadataTest:
exposedPorts: ["8888", "9000"]
volumes: ["/var/lib/snappymail"]
entrypoint: []
cmd: ["/entrypoint.sh"]
workdir: /snappymail
user: root

10
.docker/release/test/test.sh Executable file
View file

@ -0,0 +1,10 @@
#!/bin/sh
# This script tests a given docker image using https://github.com/GoogleContainerTools/container-structure-test
set -eu
SCRIPT_DIR=$( cd "$(dirname "$0")" && pwd )
IMAGE=${1:-}
echo "Testing image: $IMAGE"
docker run --rm -i \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-v "$SCRIPT_DIR/config.yaml:/config.yaml:ro" \
gcr.io/gcp-runtimes/container-structure-test:latest test --image "$IMAGE" --config config.yaml