Hi all, I've been discussing this with Dave for about a month now. Today I finally present a proposed patch to update pgadmin4's Docker packaging.
Key features of this update: - Main image is based on python:3.6-alpine3.7. Using Alpine linux leads to much smaller image - All build is done with Docker multi-stage build. First of all build the frontend in node:6 image, then build Sphinx documentation in separate Python container and in the end just install all dependencies in a clean python:3.6-alpine3.7 image, so that it does not have any leftovers from the build process and we don't rely on any tools available on the host. - Use Gunicorn (http://gunicorn.org) as lightweight HTTP / WSGI server. Gunicorn supports both HTTP and HTTPS. - Install Alpine postgresql-client package, which includes pg_dump and other tools and config PgAdmin to find these tools by default - Byte-compile all PgAdmin Python code in Dockerfile with optimization (-O) enabled. This way Python does not have to compile modules on each container restart and consume space in overlay fs Please find attached patch from "git format-patch".
From 67e387525a7c832958858c7ec1a1b7076382090e Mon Sep 17 00:00:00 2001 From: Maxim Koltsov <kolma...@gmail.com> Date: Sat, 31 Mar 2018 20:37:51 +0300 Subject: [PATCH] Re-make Docker container packaging Key features of this update: - Main image is based on python:3.6-alpine3.7. Using Alpine linux leads to much smaller image - All build is done with Docker multi-stage build. First of all build the frontend in node:6 image, then build Sphinx documentation in separate Python container and in the end just install all dependencies in a clean python:3.6-alpine3.7 image, so that it does not have any leftovers from the build process and we don't rely on any tools available on the host. - Use Gunicorn (http://gunicorn.org) as lightweight HTTP / WSGI server. Gunicorn supports both HTTP and HTTPS. - Install Alpine postgresql-client package, which includes pg_dump and other tools and config PgAdmin to find these tools by default - Byte-compile all PgAdmin Python code in Dockerfile with optimization (-O) enabled. This way Python does not have to compile modules on each container restart and consume space in overlay fs --- pkg/docker/Dockerfile | 89 ++++++++++++++++++++++++--------------------- pkg/docker/README | 58 ++++++++++++++--------------- pkg/docker/build.sh | 57 +++++------------------------ pkg/docker/config_distro.py | 4 ++ pkg/docker/entry.sh | 29 --------------- pkg/docker/entrypoint.sh | 21 +++++++++++ pkg/docker/pgadmin4.conf.j2 | 43 ---------------------- pkg/docker/run_pgadmin.py | 4 ++ 8 files changed, 112 insertions(+), 193 deletions(-) create mode 100644 pkg/docker/config_distro.py delete mode 100644 pkg/docker/entry.sh create mode 100755 pkg/docker/entrypoint.sh delete mode 100644 pkg/docker/pgadmin4.conf.j2 create mode 100644 pkg/docker/run_pgadmin.py diff --git a/pkg/docker/Dockerfile b/pkg/docker/Dockerfile index 1c1dde27..083dbd60 100644 --- a/pkg/docker/Dockerfile +++ b/pkg/docker/Dockerfile @@ -7,58 +7,63 @@ # ######################################################################### -# Get the basics out of the way -FROM centos:latest +# First of all, build frontend with NodeJS in a separate builder container +# Node-6 with ABI v48 is supported by all needed C++ packages +FROM node:6 AS node-builder -LABEL name="pgAdmin 4" \ - vendor="The pgAdmin Development Team" \ - license="PostgreSQL" +COPY ./pgadmin4/web/ /pgadmin4/web/ +WORKDIR /pgadmin4/web -# We only need the web/ directory, and a few other things -COPY web /var/www/pgadmin -COPY requirements.txt /var/www/pgadmin +RUN yarn install --cache-folder ./ycache --verbose && \ + yarn run bundle && \ + rm -rf ./ycache ./pgadmin/static/js/generated/.cache -# Install everything we need. Use easy_install to get pip, to avoid setting up EPEL -RUN yum install -y python-setuptools python-devel httpd mod_wsgi mod_ssl gcc -RUN easy_install pip -RUN pip install j2cli +# Build Sphinx documentation in separate container +FROM python:3.6-alpine3.7 as docs-builder -# Now install the Python runtime dependencies -RUN pip install -r /var/www/pgadmin/requirements.txt +# Install only dependencies absolutely required for documentation building +RUN apk add --no-cache make +RUN pip install --no-cache-dir \ + sphinx flask_babel flask_security flask_paranoid python-dateutil flask_sqlalchemy \ + flask_gravatar simplejson -# Create required directories for config +COPY ./pgadmin4/ /pgadmin4 +RUN LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 make -C /pgadmin4/docs/en_US -f Makefile.sphinx html -# Create required directories for running -RUN mkdir -p /var/log/pgadmin -RUN chown -R apache /var/log/pgadmin -RUN mkdir -p /var/lib/pgadmin -RUN chown -R apache /var/lib/pgadmin -RUN mkdir -p /certs -RUN chown -R apache /certs -RUN chmod 700 /certs +# Then install backend, copy static files and set up entrypoint +# Need alpine3.7 to get pg_dump and friends in postgresql-client package +FROM python:3.6-alpine3.7 -# Push logs to the container's output streams -RUN ln -sf /proc/self/fd/1 /var/log/httpd/access_log && \ - ln -sf /proc/self/fd/1 /var/log/httpd/ssl_access_log && \ - ln -sf /proc/self/fd/2 /var/log/httpd/error_log && \ - ln -sf /proc/self/fd/2 /var/log/httpd/ssl_error_log +RUN pip --no-cache-dir install gunicorn +RUN apk add --no-cache postgresql-client postgresql-libs -# Apache config time -RUN mkdir -p /templates -COPY pgadmin4.conf.j2 /templates/ -COPY entry.sh / +# Install build-dependencies, build & install C extensions and purge deps in one RUN step +# so that deps do not increase the size of resulting image by remaining in layers +RUN set -ex && \ + apk add --no-cache --virtual build-deps build-base postgresql-dev && \ + pip install --no-cache-dir psycopg2 pycrypto && \ + apk del --no-cache build-deps -# Finally, remove packages we only needed for building -RUN yum -y remove gcc cpp glibc-devel glibc-headers kernel-headers libgomp libmpc mpfr +COPY --from=node-builder /pgadmin4/web/pgadmin/static/js/generated/ /pgadmin4/pgadmin/static/js/generated/ +COPY --from=docs-builder /pgadmin4/docs/en_US/_build/html/ /pgadmin4/docs/ -# Default config options -ENV PGADMIN_DEFAULT_EMAIL contai...@pgadmin.org -ENV PGADMIN_DEFAULT_PASSWORD Conta1ner -ENV PGADMIN_ENABLE_TLS False -ENV PGADMIN_SERVER_NAME pgadmin4 +COPY ./pgadmin4/web /pgadmin4 +COPY ./pgadmin4/requirements.txt /pgadmin4 +COPY ./run_pgadmin.py /pgadmin4 +COPY ./config_distro.py /pgadmin4 -EXPOSE 80 443 +WORKDIR /pgadmin4 +ENV PYTHONPATH=/pgadmin4 -# Start the service -ENTRYPOINT ["/bin/bash", "/entry.sh"] +RUN pip install --no-cache-dir -r requirements.txt + +# Precompile and optimize python code to save time and space on startup +RUN python -O -m compileall /pgadmin4 + +COPY ./entrypoint.sh /entrypoint.sh + +VOLUME /var/lib/pgadmin +EXPOSE 8080 8443 + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/pkg/docker/README b/pkg/docker/README index b2d9595e..9fed51c9 100644 --- a/pkg/docker/README +++ b/pkg/docker/README @@ -4,14 +4,16 @@ Building ======== Whilst you can just use the Dockerfile directly, it requires that various pre-configuration steps are performed, for -example, the pgAdmin web code must be copied to ./web and yarn install/yarn run bundle must be executed. -requirements.txt is also expected to be in this directory, and the pre-built docs must be in web/docs. +example, the pgAdmin web code must be copied to `./web`, Sphinx documentation source must be copied to `./docs` +and `requirements.txt` is also expected to be in this directory. The recommended (and easy) way to build the container is to do: +```console cd $PGADMIN_SRC/ workon pgadmin-venv make docker +``` This will call the build script $PGADMIN_SRC/pkg/docker/build.sh which will prepare a staging directory containing all the required files, then build the container and push it to your repo. @@ -21,57 +23,51 @@ Running The container will accept the following variables at startup: -PGADMIN_DEFAULT_EMAIL ---------------------- - -Default: contai...@pgadmin.org) +PGADMIN_SETUP_EMAIL +------------------- This is the email address used when setting up the initial administrator account to login to pgAdmin. -PGADMIN_DEFAULT_PASSWORD ------------------------- - -Default: Conta1ner +PGADMIN_SETUP_PASSWORD +---------------------- This is the password used when setting up the initial administrator account to login to pgAdmin. PGADMIN_ENABLE_TLS ------------------ -Default: False +Default: unset -If set to the default, False, the container will listen on port 80 for connections in plain text. If set to True, the -container will listen on port 443 for TLS connections. +If not set, the container will listen on port 8080 for connections in insecure HTTP protocol. +If set to any value, the container will listen on port 8443 for TLS connections. -When TLS is enabled, a certificate and key must be provided. Typically these should be stored on the host file system -and mounted from the container. The expected paths are /certs/server.crt and /certs/server.key +When TLS is enabled, a certificate and key must be provided. +Typically these should be stored on the host file system and mounted from the container. +The expected paths are `/certs/server.crt` and `/certs/server.key`. -PGADMIN_SERVER_NAME -------------------- - -Default: pgadmin4 - -This variable allows you to specify the value used for the Apache HTTPD ServerName directive. This is commonly used to -ensure the CN of the TLS certificate matches what the server expects. +You need to explicitly map these ports with `-p` option to some port at your machine. Examples ======== Run a simple container over port 80: -docker run -p 80:80 \ - -e "PGADMIN_DEFAULT_EMAIL=u...@domain.com" \ - -e "PGADMIN_DEFAULT_PASSWORD=SuperSecret" \ +```console +docker run -p 80:8080 \ + -e "PGADMIN_SETUP_EMAIL=u...@domain.com" \ + -e "PGADMIN_SETUP_PASSWORD=SuperSecret" \ -d pgadmin4 +``` Run a TLS secured container using a shared config/storage directory in /private/var/lib/pgadmin on the host: -docker run -p 443:443 \ +```console +docker run -p 443:8443 \ -v "/private/var/lib/pgadmin:/var/lib/pgadmin" \ -v "/path/to/certificate.cert:/certs/server.cert" \ -v "/path/to/certificate.key:/certs/server.key" \ - -e "PGADMIN_DEFAULT_EMAIL=u...@domain.com" \ - -e "PGADMIN_DEFAULT_PASSWORD=SuperSecret" \ - -e "PGADMIN_ENABLE_TLS=True" \ - -e "PGADMIN_SERVER_NAME=pgadmin.domain.com" \ - -d pgadmin4 \ No newline at end of file + -e "PGADMIN_SETUP_EMAIL=u...@domain.com" \ + -e "PGADMIN_SETUP_PASSWORD=SuperSecret" \ + -e "PGADMIN_ENABLE_TLS=1" \ + -d pgadmin4 +``` diff --git a/pkg/docker/build.sh b/pkg/docker/build.sh index 373c99b7..1dda3ca2 100755 --- a/pkg/docker/build.sh +++ b/pkg/docker/build.sh @@ -41,60 +41,21 @@ if [ -d docker-build ]; then rm -rf docker-build fi -mkdir docker-build - -# Create the output directory if not present -if [ ! -d dist ]; then - mkdir dist -fi +mkdir -p docker-build/pgadmin4 # Build the clean tree -for FILE in `git ls-files web` -do - echo Adding $FILE - # We use tar here to preserve the path, as Mac (for example) doesn't support cp --parents - tar cf - $FILE | (cd docker-build; tar xf -) -done - -pushd web - yarn install - yarn run bundle - - rm -rf pgadmin/static/js/generated/.cache - - for FILE in `ls -d pgadmin/static/js/generated/*` - do - echo Adding $FILE - tar cf - $FILE | (cd ../docker-build/web; tar xf -) - done -popd - -# Build the docs -if [ -d docs/en_US/_build/html ]; then - rm -rf docs/en_US/_build/html -fi - -LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 make -C docs/en_US -f Makefile.sphinx html - -mkdir docker-build/web/docs -cp -R docs/en_US/_build/html/* docker-build/web/docs/ - -# Configure pgAdmin -echo "HELP_PATH = '../../docs/'" >> docker-build/web/config_distro.py -echo "DEFAULT_BINARY_PATHS = {" >> docker-build/web/config_distro.py -echo " 'pg': ''," >> docker-build/web/config_distro.py -echo " 'ppas': ''," >> docker-build/web/config_distro.py -echo " 'gpdb': ''" >> docker-build/web/config_distro.py -echo "}" >> docker-build/web/config_distro.py +echo Copying source tree... +git archive HEAD -- docs web requirements.txt | tar xvf - -C docker-build/pgadmin4 # Copy the Docker specific assets into place -cp pkg/docker/Dockerfile docker-build/ -cp pkg/docker/entry.sh docker-build/ -cp pkg/docker/pgadmin4.conf.j2 docker-build/ -cp requirements.txt docker-build/ +cp pkg/docker/Dockerfile \ + pkg/docker/entrypoint.sh \ + pkg/docker/config_distro.py \ + pkg/docker/run_pgadmin.py \ + docker-build/ # Build the container docker build docker-build -t $CONTAINER_NAME \ -t $CONTAINER_NAME:latest \ -t $CONTAINER_NAME:$APP_RELEASE \ - -t $CONTAINER_NAME:$APP_LONG_VERSION \ No newline at end of file + -t $CONTAINER_NAME:$APP_LONG_VERSION diff --git a/pkg/docker/config_distro.py b/pkg/docker/config_distro.py new file mode 100644 index 00000000..71ad3c74 --- /dev/null +++ b/pkg/docker/config_distro.py @@ -0,0 +1,4 @@ +HELP_PATH = '../../docs' +DEFAULT_BINARY_PATHS = { + 'pg': '/usr/bin' +} diff --git a/pkg/docker/entry.sh b/pkg/docker/entry.sh deleted file mode 100644 index b6ba42e9..00000000 --- a/pkg/docker/entry.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash - -######################################################################## -# -# pgAdmin 4 - PostgreSQL Tools -# -# Copyright (C) 2013 - 2018, The pgAdmin Development Team -# This software is released under the PostgreSQL Licence -# -######################################################################### - -export PGADMIN_SETUP_EMAIL=${PGADMIN_DEFAULT_EMAIL} -export PGADMIN_SETUP_PASSWORD=${PGADMIN_DEFAULT_PASSWORD} - -if [ ${PGADMIN_ENABLE_TLS} != "True" ]; then - if [ -f /etc/httpd/conf.d/ssl.conf ]; then - mv /etc/httpd/conf.d/ssl.conf /etc/httpd/conf.d/ssl.conf.disabled - fi -else - if [ -f /etc/httpd/conf.d/ssl.conf.disabled ]; then - mv /etc/httpd/conf.d/ssl.conf.disabled /etc/httpd/conf.d/ssl.conf - fi -fi - -j2 /templates/pgadmin4.conf.j2 > /etc/httpd/conf.d/pgadmin4.conf - -rm -f /run/httpd/httpd.pid - -/usr/sbin/httpd -D FOREGROUND diff --git a/pkg/docker/entrypoint.sh b/pkg/docker/entrypoint.sh new file mode 100755 index 00000000..09fa3ab0 --- /dev/null +++ b/pkg/docker/entrypoint.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +if [ ! -f /var/lib/pgadmin/pgadmin4.db ]; then + if [ -z "${PGADMIN_SETUP_EMAIL}" -o -z "${PGADMIN_SETUP_PASSWORD}" ]; then + echo 'You need to specify PGADMIN_SETUP_EMAIL and PGADMIN_SETUP_PASSWORD environment variables' + exit 1 + fi + + # Initialize DB before starting Gunicorn + # Importing pgadmin4 (from this script) is enough + python run_pgadmin.py +fi + +# NOTE: currently pgadmin can run only with 1 worker due to sessions implementation +# Using --threads to have multi-threaded single-process worker + +if [ ! -z ${PGADMIN_ENABLE_TLS} ]; then + exec gunicorn --bind 0.0.0.0:8443 -w 1 --threads ${GUNICORN_THREADS:-25} --access-logfile - --keyfile /certs/server.key --certfile /certs/server.cert run_pgadmin:app +else + exec gunicorn --bind 0.0.0.0:8080 -w 1 --threads ${GUNICORN_THREADS:-25} --access-logfile - run_pgadmin:app +fi diff --git a/pkg/docker/pgadmin4.conf.j2 b/pkg/docker/pgadmin4.conf.j2 deleted file mode 100644 index d44de2d5..00000000 --- a/pkg/docker/pgadmin4.conf.j2 +++ /dev/null @@ -1,43 +0,0 @@ -######################################################################## -# -# pgAdmin 4 - PostgreSQL Tools -# -# Copyright (C) 2013 - 2018, The pgAdmin Development Team -# This software is released under the PostgreSQL Licence -# -######################################################################### - -ServerName {{ PGADMIN_SERVER_NAME }} -{% if PGADMIN_ENABLE_TLS|default('False') == 'True' %} -LoadModule ssl_module modules/mod_ssl.so - -<VirtualHost *:443> - SSLEngine on - SSLCipherSuite HIGH:!aNULL:!MD5 - SSLCertificateFile "/certs/server.cert" - SSLCertificateKeyFile "/certs/server.key" - - ServerName {{ PGADMIN_SERVER_NAME }} - WSGIDaemonProcess pgadmin processes=1 threads=25 - WSGIScriptAlias / /var/www/pgadmin/pgAdmin4.wsgi - - <Directory /var/www/pgadmin> - WSGIProcessGroup pgadmin - WSGIApplicationGroup %{GLOBAL} - Order deny,allow - Allow from all - </Directory> -</VirtualHost> -{% else %} -<VirtualHost *:80> - WSGIDaemonProcess pgadmin processes=1 threads=25 - WSGIScriptAlias / /var/www/pgadmin/pgAdmin4.wsgi - - <Directory /var/www/pgadmin> - WSGIProcessGroup pgadmin - WSGIApplicationGroup %{GLOBAL} - Order deny,allow - Allow from all - </Directory> -</VirtualHost> -{% endif %} \ No newline at end of file diff --git a/pkg/docker/run_pgadmin.py b/pkg/docker/run_pgadmin.py new file mode 100644 index 00000000..48b17735 --- /dev/null +++ b/pkg/docker/run_pgadmin.py @@ -0,0 +1,4 @@ +import builtins +builtins.SERVER_MODE = True + +from pgAdmin4 import app -- 2.16.2