使用 Docker Compose 启动 DNMP 架构

 Technique  2 comments

之前最流行的架构是 LNMP(Linux+Nginx+MySQL+PHP) ,随着容器技术的进步和火热,现在对于中小型网站可以使用 DNMP 架构,就是将 Linux 换为了 Docker 。

总览

使用官方镜像很多时候无法满足使用的需求,比如 NGINX 官方镜像并不支持比如 Brotli 等模块,在 NGINX v1.9.11 引入了 Dynamic Module ,一开可以动态加载模块,节约内存,用不到的模块可以在启动程序时不加载。

且 Docker 官方的容器中不提供基于 Alpine 的 MySQL / MariaDB 的镜像,因此还需要自行构造。

本文所需文件全部在 PlanD - Github 中提供。

过程

NGINX

先构建支持 Brotli 的 NGINX 官方容器,在此使用 Multi-Stage 技术基于官方镜像进行构建。

FROM nginx:alpine-perl AS builder

LABEL maintainer="VVavE Docker Maintainers <waveworkshop@outlook.com>"

# ENV NGINX_VERSION 1.17.5
# ENV NJS_VERSION   0.3.6
# ENV PKG_RELEASE   1

ENV NGX_BROTLI_COMMIT e505dce68acc190cc5a1e780a3b0275e39f160ca
ENV NGX_NCHAN_COMMIT 11e718227aa1a7a448b688f801d380dbc612349a
ENV NGX_HEADERS_COMMIT 552e216a0da95c685d9db4f43e209c3f2a803e49

# For latest build deps, see https://github.com/nginxinc/docker-nginx/blob/master/mainline/alpine/Dockerfile
RUN apk add --no-cache --virtual .build-deps \
        gcc \
        libc-dev \
        make \
        openssl-dev \
        pcre-dev \
        zlib-dev \
        linux-headers \
        libxslt-dev \
        gd-dev \
        geoip-dev \
        perl-dev \
        libedit-dev \
        mercurial \
        bash \
        alpine-sdk \
        findutils \
        git \
        curl \
    && mkdir -p /usr/src \
    && cd /usr/src \
    && git clone --recursive https://github.com/google/ngx_brotli.git \
    && git clone --recursive https://github.com/openresty/headers-more-nginx-module.git \
    && git clone --recursive https://github.com/slact/nchan.git \
    && curl -fSL https://nginx.org/download/nginx-$NGINX_VERSION.tar.gz -o nginx.tar.gz \
    && tar -zxC /usr/src -f nginx.tar.gz \
    && NGX_BROTLI_DIR="$(pwd)/ngx_brotli" \
    && NGX_HEADER_DIR="$(pwd)/headers-more-nginx-module" \
    && NCHAN_DIR="$(pwd)/nchan" \
    && cd /usr/src/nginx-$NGINX_VERSION \
    && ./configure --prefix=/etc/nginx \
        --sbin-path=/usr/sbin/nginx \
        --modules-path=/usr/lib/nginx/modules \
        --conf-path=/etc/nginx/nginx.conf \
        --error-log-path=/var/log/nginx/error.log \
        --http-log-path=/var/log/nginx/access.log \
        --pid-path=/var/run/nginx.pid \
        --lock-path=/var/run/nginx.lock \
        --http-client-body-temp-path=/var/cache/nginx/client_temp \
        --http-proxy-temp-path=/var/cache/nginx/proxy_temp \
        --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp \
        --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp \
        --http-scgi-temp-path=/var/cache/nginx/scgi_temp \
        --with-perl_modules_path=/usr/lib/perl5/vendor_perl \
        --user=nginx \
        --group=nginx \
        --with-compat \
        --with-file-aio \
        --with-threads \
        --with-http_addition_module \
        --with-http_auth_request_module \
        --with-http_dav_module \
        --with-http_flv_module \
        --with-http_gunzip_module \
        --with-http_gzip_static_module \
        --with-http_mp4_module \
        --with-http_random_index_module \
        --with-http_realip_module \
        --with-http_secure_link_module \
        --with-http_slice_module \
        --with-http_ssl_module \
        --with-http_stub_status_module \
        --with-http_sub_module \
        --with-http_v2_module --with-mail \
        --with-mail_ssl_module --with-stream \
        --with-stream_realip_module \
        --with-stream_ssl_module \
        --with-stream_ssl_preread_module \
        --add-dynamic-module=$NGX_BROTLI_DIR \
        --add-dynamic-module=$NGX_HEADER_DIR \
        --add-dynamic-module=$NCHAN_DIR \
        --with-cc-opt='-Os -fomit-frame-pointer' \
        --with-ld-opt=-Wl,--as-needed \
    && make -j$(getconf _NPROCESSORS_ONLN) \
    && make install \
    && strip /usr/lib/nginx/modules/*.so \
    && rm -rf /usr/src/

FROM nginx:alpine-perl

# Extract the dynamic module NCHAN from the builder image
COPY --from=builder /usr/lib/nginx/modules/ngx_nchan_module.so /usr/lib/nginx/modules/ngx_nchan_module.so
COPY --from=builder /usr/lib/nginx/modules/ngx_http_brotli_filter_module.so /usr/lib/nginx/modules/ngx_http_brotli_filter_module.so
COPY --from=builder /usr/lib/nginx/modules/ngx_http_brotli_static_module.so /usr/lib/nginx/modules/ngx_http_brotli_static_module.so
COPY --from=builder /usr/lib/nginx/modules/ngx_http_headers_more_filter_module.so /usr/lib/nginx/modules/ngx_http_headers_more_filter_module.so

RUN apk add --no-cache iproute2

EXPOSE 80

STOPSIGNAL SIGTERM

CMD ["nginx", "-g", "daemon off;"]

小贴士:本 Dockerfile 加入了三个额外组件 Nchan Brotli Header ,具体功能和用途请自行查询,本文不再赘述。

配置

使用上述配置构建的容器需要修改默认 NGINX 配置文件以便支持 Brotli 等额外插件

load_module  modules/ngx_http_brotli_filter_module.so;
load_module  modules/ngx_http_brotli_static_module.so;
load_module  modules/ngx_nchan_module.so;
load_module  modules/ngx_http_headers_more_filter_module.so;
...
http {
...
    brotli  on;
    brotli_comp_level  6;
    brotli_types  text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss application/javascript image/svg+xml;
...
}

MariaDB

FROM alpine:3.10

LABEL maintainer="VVavE Docker Maintainers <waveworkshop@outlook.com>"

ENV LC_ALL=en_GB.UTF-8

RUN mkdir /docker-entrypoint-initdb.d && \
    apk -U upgrade && \
    apk add --no-cache mariadb mariadb-client && \
    apk add --no-cache tzdata && \
    # clean up
    rm -rf /var/cache/apk/*

# comment out a few problematic configuration values
RUN sed -Ei 's/^(bind-address|log)/#&/' /etc/my.cnf && \
    sed -i  's/^skip-networking/#&/' /etc/my.cnf.d/mariadb-server.cnf && \
    # don't reverse lookup hostnames, they are usually another container
    sed -i '/^\[mysqld]$/a skip-host-cache\nskip-name-resolve' /etc/my.cnf && \
    # always run as user mysql
    sed -i '/^\[mysqld]$/a user=mysql' /etc/my.cnf && \
    # allow custom configurations
    echo -e '\n!includedir /etc/mysql/conf.d/' >> /etc/my.cnf && \
    mkdir -p /etc/mysql/conf.d/

VOLUME /var/lib/mysql

COPY docker-entrypoint.sh /usr/local/bin/
RUN ln -s usr/local/bin/docker-entrypoint.sh / # backwards compat
ENTRYPOINT ["docker-entrypoint.sh"]

EXPOSE 3306
# Default arguments passed to ENTRYPOINT if no arguments are passed when starting container
CMD ["mysqld"]

还需要额外的启动脚本,可在仓库中进行下载,也可从下面粘贴。

#!/bin/sh

set -eo pipefail
# set -x

# if command starts with an option, prepend mysqld
if [ "${1:0:1}" = '-' ]; then
  set -- mysqld_safe "$@"
fi

# usage: file_env VAR [DEFAULT]
#    ie: file_env 'XYZ_DB_PASSWORD' 'example'
# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of
#  "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature)
file_env() {
  var=$1
  file_var="${var}_FILE"
  var_value=$(printenv $var || true)
  file_var_value=$(printenv $file_var || true)
  default_value=$2

  if [ -n "$var_value" -a -n "$file_var_value" ]; then
    echo >&2 "error: both $var and $file_var are set (but are exclusive)"
    exit 1
  fi

  if [ -z "${var_value}" ]; then
    if [ -z "${file_var_value}" ]; then
      export "${var}"="${default_value}"
    else
      export "${var}"="${file_var_value}"
    fi
  fi

  unset "$file_var"
}

# Fetch value from server config
# We use mysqld --verbose --help instead of my_print_defaults because the
# latter only show values present in config files, and not server defaults
_get_config() {
  conf="$1"
  mysqld --verbose --help --log-bin-index="$(mktemp -u)" 2>/dev/null | awk '$1 == "'"$conf"'" { print $2; exit }'
}

DATA_DIR="$(_get_config 'datadir')"

# Initialize database if necessary
if [ ! -d "$DATA_DIR/mysql" ]; then
  file_env 'MYSQL_ROOT_PASSWORD'
  if [ -z "$MYSQL_ROOT_PASSWORD" -a -z "$MYSQL_ALLOW_EMPTY_PASSWORD" -a -z "$MYSQL_RANDOM_ROOT_PASSWORD" ]; then
    echo >&2 'error: database is uninitialized and password option is not specified '
    echo >&2 '  You need to specify one of MYSQL_ROOT_PASSWORD, MYSQL_ALLOW_EMPTY_PASSWORD and MYSQL_RANDOM_ROOT_PASSWORD'
    exit 1
  fi

  mkdir -p "$DATA_DIR"
  chown mysql: "$DATA_DIR"

  echo 'Initializing database'
  mysql_install_db --user=mysql --datadir="$DATA_DIR" --rpm
  chown -R mysql: "$DATA_DIR"
  echo 'Database initialized'

  # Start mysqld to config it
  mysqld_safe --skip-networking --nowatch

  mysql_options='--protocol=socket -uroot'

  # Execute mysql statement
  # statement can be passed directly or by HEREDOC
  execute() {
    statement="$1"
    if [ -n "$statement" ]; then
      mysql -ss $mysql_options -e "$statement"
    else
      cat /dev/stdin | mysql -ss $mysql_options
   fi
  }

  for i in `seq 30 -1 0`; do
    if execute 'SELECT 1' &> /dev/null; then
      break
    fi
    echo 'MySQL init process in progress...'
    sleep 1
  done
  if [ "$i" = 0 ]; then
    echo >&2 'MySQL init process failed.'
    exit 1
  fi

  if [ -z "$MYSQL_INITDB_SKIP_TZINFO" ]; then
    # sed is for https://bugs.mysql.com/bug.php?id=20545
    mysql_tzinfo_to_sql /usr/share/zoneinfo | \
      sed 's/Local time zone must be set--see zic manual page/FCTY/' | \
      mysql $mysql_options mysql
  fi

  if [ -n "$MYSQL_RANDOM_ROOT_PASSWORD" ]; then
    export MYSQL_ROOT_PASSWORD="$(tr -dc _A-Z-a-z-0-9 < /dev/urandom | head -c10)"
    echo "GENERATED ROOT PASSWORD: $MYSQL_ROOT_PASSWORD"
  fi

  # Create root user, set root password, drop useless table
  # Delete root user except for
  execute <<SQL
    -- What's done in this file shouldn't be replicated
    --  or products like mysql-fabric won't work
    SET @@SESSION.SQL_LOG_BIN=0;

    DELETE FROM mysql.user WHERE user NOT IN ('mysql.sys', 'mysqlxsys', 'root') OR host NOT IN ('localhost') ;
    SET PASSWORD FOR 'root'@'localhost'=PASSWORD('${MYSQL_ROOT_PASSWORD}') ;
    GRANT ALL ON *.* TO 'root'@'localhost' WITH GRANT OPTION ;
    DROP DATABASE IF EXISTS test ;
    FLUSH PRIVILEGES ;
SQL

  # https://mariadb.com/kb/en/library/mariadb-environment-variables/
  export MYSQL_PWD="$MYSQL_ROOT_PASSWORD"

  # Create root user for $MYSQL_ROOT_HOST
  file_env 'MYSQL_ROOT_HOST' '%'
  if [ "$MYSQL_ROOT_HOST" != 'localhost' ]; then
    execute <<SQL
      CREATE USER 'root'@'${MYSQL_ROOT_HOST}' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}' ;
      GRANT ALL ON *.* TO 'root'@'${MYSQL_ROOT_HOST}' WITH GRANT OPTION ;
      FLUSH PRIVILEGES ;
SQL
  fi

  file_env 'MYSQL_DATABASE'
  if [ "$MYSQL_DATABASE" ]; then
    execute "CREATE DATABASE IF NOT EXISTS \`$MYSQL_DATABASE\` ;"
  fi

  file_env 'MYSQL_USER'
  file_env 'MYSQL_PASSWORD'
  if [ "$MYSQL_USER" -a "$MYSQL_PASSWORD" ]; then
    execute "CREATE USER '$MYSQL_USER'@'%' IDENTIFIED BY '$MYSQL_PASSWORD' ;"

    if [ "$MYSQL_DATABASE" ]; then
      execute "GRANT ALL ON \`$MYSQL_DATABASE\`.* TO '$MYSQL_USER'@'%' ;"
    fi

    execute 'FLUSH PRIVILEGES ;'
  fi

  # Database cannot be specified when creating user,
  # otherwise it will fail with "Unknown database"
  if [ "$MYSQL_DATABASE" ]; then
    mysql_options="$mysql_options \"$MYSQL_DATABASE\""
  fi

  echo
  for f in /docker-entrypoint-initdb.d/*; do
    case "$f" in
      *.sh)     echo "$0: running $f"; . "$f" ;;
      *.sql)    echo "$0: running $f"; execute < "$f"; echo ;;
      *.sql.gz) echo "$0: running $f"; gunzip -c "$f" | execute; echo ;;
      *)        echo "$0: ignoring $f" ;;
    esac
    echo
  done

  if ! mysqladmin -uroot --password="$MYSQL_PWD" shutdown; then
    echo >&2 'Shutdown failed'
    exit 1
  fi

  echo
  echo 'MySQL init process done. Ready for start up.'
  echo
fi

chown -R mysql: "$DATA_DIR"
exec "$@"

PHP

FROM php:fpm-alpine

LABEL maintainer="VVavE Docker Maintainers <waveworkshop@outlook.com>"

RUN docker-php-ext-install pdo_mysql pdo opcache mysqli\
    && apk add --no-cache libpng-dev libxml2-dev \
    && docker-php-ext-install gd xml xmlrpc \
    && apk add --no-cache autoconf build-base \
    && pecl install igbinary \
    && docker-php-ext-enable igbinary \
    && pecl install msgpack \
    && docker-php-ext-enable msgpack \
    && pecl install redis \
    && docker-php-ext-enable redis \
    && apk add --no-cache libmemcached-libs \
        libmemcached-dev \
        libevent-dev \
        zlib-dev \
    && pecl install memcached \
    && docker-php-ext-enable memcached \
    && apk del --no-cache autoconf \
        build-base \
        libmemcached-dev \
        libevent-dev \
        zlib-dev

小贴士:从官方镜像为基础进行构建,增加了例如 mysqli pdo opcache 等常用插件,可根据需求进行增加或者删除。

额外

根据仓库中的说明准备好项目文件和所需配置文件即可配置启动

Compose

version: '3.7'
services:
  nginx:
    image: nginx:alpine-brotli
    build:
      context: ./nginx/
    depends_on:
      - php
    ports:
      - 80:80
      - 443:443
    networks:
      - internal
    restart: on-failure
    volumes:
      - /data/mystack/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - /data/mystack/nginx/conf.d:/etc/nginx/conf.d
      - /data/mystack/nginx/logs.d:/etc/nginx/logs.d
      - /data/mystack/nginx/cert.d:/etc/nginx/cert.d
      - /data/mystack/www:/var/www
  php:
    image: php-fpm:alpine
    build:
      context: ./php/
    depends_on:
      - mariadb
    networks:
      - internal
    restart: on-failure
    volumes:
      - /data/mystack/php/php.ini:/usr/local/etc/php/php.ini:ro
      - /data/mystack/www:/var/www
  mariadb:
    image: mariadb:alpine
    build:
      context: ./mariadb/
    depends_on:
      - memcached
    ports:
      - 127.0.0.1:3306:3306
    networks:
      - internal
    restart: on-failure
    volumes:
      - myRdsVol:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: 'cz72sLJtfWYa983nEUIYA' # 示例密码,使用前请替换
    command: ['mysqld', '--character-set-server=utf8mb4', '--collation-server=utf8mb4_unicode_ci']
  memcached:
    image: memcached:alpine
    depends_on:
      - redis
    ports:
      - 127.0.0.1:11211:11211
    networks:
      - internal
    restart: on-failure
  redis:
    image: redis:alpine
    ports:
      - 127.0.0.1:6379:6379
    networks:
      - internal
    restart: on-failure
    volumes:
      - myKvsVol:/data
networks:
  internal:
volumes:
  myRdsVol:
  myKvsVol:

附录

参考链接

回复
  1. 天翼

    博主能指导一下容器的问题么?有偿!

    Quark 3.6.2.122 Android Pie 回复
  2. 王抗压

    有用,mark一下

    Safari 605.1 iOS 13 回复