diff --git a/Dockerfile b/Dockerfile index 3663649c..02633531 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,8 +19,13 @@ FROM phusion/baseimage:0.9.16 # Dockerfile metadata. MAINTAINER Joshua Tauberer (http://razor.occams.info) -EXPOSE 25 53/udp 53/tcp 80 443 587 993 -VOLUME /data +EXPOSE 25 53/udp 53/tcp 80 443 587 993 4190 +VOLUME /home/user-data + +CMD ["/sbin/my_init"] + +# Create the user-data user, so the start script doesn't have to. +RUN useradd -m user-data # Docker has a beautiful way to cache images after each step. The next few # steps of installing system packages are very intensive, so we take care @@ -32,23 +37,17 @@ VOLUME /data RUN apt-get update RUN DEBIAN_FRONTEND=noninteractive apt-get upgrade -y +# We want to use Ubuntu's stock rsyslog rather than syslog-ng +# that the base image provides. +RUN DEBIAN_FRONTEND=noninteractive apt-get install -y rsyslog +RUN rm -rf /etc/service/syslog-ng + # Install packages needed by Mail-in-a-Box. ADD containers/docker/apt_package_list.txt /tmp/mailinabox_apt_package_list.txt RUN DEBIAN_FRONTEND=noninteractive apt-get install -y $(cat /tmp/mailinabox_apt_package_list.txt) -RUN DEBIAN_FRONTEND=noninteractive apt-get install -y rsyslog -RUN rm -f /tmp/mailinabox_apt_package_list.txt -RUN apt-get clean - -# Create the user-data user, so the start script doesn't have to. -RUN useradd -m user-data # Now add Mail-in-a-Box to the system. ADD . /usr/local/mailinabox -# We can't know things like the IP address where the container will eventually -# be deployed until the container is started. We also don't want to create any -# private keys during the creation of the image --- that should wait until the -# container is started too. So our whole setup process is deferred until the -# container is started. -RUN mkdir -p /etc/my_init.d -RUN ln -s /usr/local/mailinabox/containers/docker/init.sh /etc/my_init.d/20-mailinabox.sh +# Cleanup +RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* diff --git a/containers/docker/init.sh b/containers/docker/init.sh index 8f7e377e..f7ec4be7 100755 --- a/containers/docker/init.sh +++ b/containers/docker/init.sh @@ -17,58 +17,21 @@ if [ ! -t 0 ]; then export NONINTERACTIVE=1 fi -# The phusion/baseimage base image we use for a working Ubuntu -# replaces the normal Upstart system service management with -# a ligher-weight service management system called runit that -# requires a different configuration. We need to create service -# run files that do not daemonize. - -# For most of the services, there is a common pattern we can use: -# execute the init.d script that the Ubuntu package installs, and -# then poll for the termination of the daemon. -function make_runit_service { - INITD_NAME=$1 - WAIT_ON_PROCESS_NAME=$2 - mkdir -p /etc/service/$INITD_NAME - cat > /etc/service/$INITD_NAME/run < LSB compatibility + # see http://smarden.org/runit/faq.html#lsb + if [ -f /etc/init.d/$service ]; then + mv /etc/init.d/$service /etc/init.d/$service.lsb + chmod -x /etc/init.d/$service.lsb + fi + ln -s /usr/bin/sv /etc/init.d/$service +done # Start configuration. Using 'source' means an exit from inside # also exits this script and terminates the container. diff --git a/containers/docker/run b/containers/docker/run index e9a310ed..b082e152 100755 --- a/containers/docker/run +++ b/containers/docker/run @@ -22,6 +22,10 @@ # Build or rebuild the image. # Rebuilds are very fast. +HOST_HTTP_PORT=${HOST_HTTP_PORT:-80} +HOST_HTTPS_PORT=${HOST_HTTPS_PORT:-443} +HOST_USERDATA_VOLUME=${HOST_USERDATA_VOLUME:-/home/user-data} + tput setaf 2 echo "Building/updating base image (mailinabox)..." tput setaf 7 @@ -36,8 +40,8 @@ if ! docker ps -a | grep mailinabox-userdata > /dev/null; then docker run -d \ --name mailinabox-userdata \ - -v /home/user-data \ - scratch /bin/does-not-exist-but-thats-ok + -v $HOST_USERDATA_VOLUME:/home/user-data \ + phusion/baseimage:0.9.16 else tput setaf 2 echo @@ -63,15 +67,43 @@ echo echo "Starting new container (mailinabox-services)..." tput setaf 7 +# Run the services container detached. # Notes: # * Passing through SKIP_NETWORK_CHECKS makes it easier to do testing # on a residential network. - +# * --privileged flag cause an issue with bind9/named failing to start in this case +# see docker/docker#7318 docker run \ - --privileged \ -v /dev/urandom:/dev/random \ - -p 25 -p 53/udp -p 53/tcp -p 80 -p 443 -p 587 -p 993 \ + -p 25:25 \ + -p 53:53/udp \ + -p 53:53/tcp \ + -p $HOST_HTTP_PORT:80 \ + -p $HOST_HTTPS_PORT:443 \ + -p 587:587 \ + -p 993:993 \ + -p 4190:4190 \ --name mailinabox-services \ --volumes-from mailinabox-userdata \ -e "SKIP_NETWORK_CHECKS=$SKIP_NETWORK_CHECKS" \ + -d \ + -t \ mailinabox + +tput setaf 2 +echo +echo "Initializing container (mailinabox-services)..." +tput setaf 7 + +# run init.sh +docker exec \ + -it \ + mailinabox-services \ + /usr/local/mailinabox/containers/docker/init.sh + +tput setaf 2 +echo +echo "Restarting container..." +tput setaf 7 + +docker restart mailinabox-services diff --git a/containers/docker/runit/bind9.sh b/containers/docker/runit/bind9.sh new file mode 100644 index 00000000..04090dca --- /dev/null +++ b/containers/docker/runit/bind9.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +# for a chrooted server: "-u bind -t /var/lib/named" +# Don't modify this line, change or create /etc/default/bind9. +OPTIONS="" +RESOLVCONF=no + +test -f /etc/default/bind9 && . /etc/default/bind9 + +# dirs under /var/run can go away on reboots. +mkdir -p /var/run/named +chmod 775 /var/run/named +chown root:bind /var/run/named >/dev/null 2>&1 || true + +exec named -f $OPTIONS 2>&1 diff --git a/containers/docker/runit/dovecot.sh b/containers/docker/runit/dovecot.sh index 5e5c1474..e700e7d7 100755 --- a/containers/docker/runit/dovecot.sh +++ b/containers/docker/runit/dovecot.sh @@ -1,3 +1,3 @@ #!/bin/bash -/usr/sbin/dovecot -F -c /etc/dovecot/dovecot.conf &> /var/log/dovecot.log +exec /usr/sbin/dovecot -F -c /etc/dovecot/dovecot.conf &> /var/log/dovecot.log diff --git a/containers/docker/runit/fail2ban.sh b/containers/docker/runit/fail2ban.sh new file mode 100644 index 00000000..5fad39a9 --- /dev/null +++ b/containers/docker/runit/fail2ban.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +NAME=fail2ban +DAEMON=/usr/bin/$NAME-server + +# Ad-hoc way to parse out socket file name +SOCKFILE=`grep -h '^[^#]*socket *=' /etc/$NAME/$NAME.conf /etc/$NAME/$NAME.local 2>/dev/null \ + | tail -n 1 | sed -e 's/.*socket *= *//g' -e 's/ *$//g'` +[ -z "$SOCKFILE" ] && SOCKFILE='/tmp/fail2ban.sock' + +# Assure that /var/run/fail2ban exists +[ -d /var/run/fail2ban ] || mkdir -p /var/run/fail2ban + +# Run as root by default. +FAIL2BAN_USER=root + +# Read configuration variable file if it is present +[ -r /etc/default/$NAME ] && . /etc/default/$NAME +DAEMON_ARGS="$FAIL2BAN_OPTS" + +exec $DAEMON -f $DAEMON_ARGS 2>&1 \ No newline at end of file diff --git a/containers/docker/runit/mailinabox.sh b/containers/docker/runit/mailinabox.sh new file mode 100644 index 00000000..abeb4331 --- /dev/null +++ b/containers/docker/runit/mailinabox.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +NAME=mailinabox +DAEMON=/usr/local/bin/mailinabox-daemon + +export LANGUAGE=en_US.UTF-8 +export LC_ALL=en_US.UTF-8 +export LANG=en_US.UTF-8 +export LC_TYPE=en_US.UTF-8 + +# Read configuration variable file if it is present +[ -r /etc/default/$NAME ] && . /etc/default/$NAME + +exec $DAEMON 2>&1 \ No newline at end of file diff --git a/containers/docker/runit/memcached.sh b/containers/docker/runit/memcached.sh new file mode 100644 index 00000000..548dd28c --- /dev/null +++ b/containers/docker/runit/memcached.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +exec /sbin/setuser memcache /usr/bin/memcached >>/var/log/memcached.log 2>&1 \ No newline at end of file diff --git a/containers/docker/runit/nginx.sh b/containers/docker/runit/nginx.sh new file mode 100644 index 00000000..cd7a782c --- /dev/null +++ b/containers/docker/runit/nginx.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +exec /usr/sbin/nginx -c /etc/nginx/nginx.conf -g "daemon off;" 2>&1 diff --git a/containers/docker/runit/nsd.sh b/containers/docker/runit/nsd.sh new file mode 100644 index 00000000..81af8804 --- /dev/null +++ b/containers/docker/runit/nsd.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +NAME=nsd +DAEMON=/usr/sbin/$NAME +CONFFILE=/etc/nsd/nsd.conf +DAEMON_ARGS="-d -c $CONFFILE" + +# reconfigure since the ip may have changed +# if it fails runit will retry anyway, but +# don't do this on first start +if [ -f /var/lib/mailinabox/api.key ]; then + /usr/local/mailinabox/tools/dns_update +fi + +exec $DAEMON $DAEMON_ARGS 2>&1 diff --git a/containers/docker/runit/opendkim.sh b/containers/docker/runit/opendkim.sh new file mode 100644 index 00000000..791619df --- /dev/null +++ b/containers/docker/runit/opendkim.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +DAEMON=/usr/sbin/opendkim +NAME=opendkim +USER=opendkim +RUNDIR=/var/run/$NAME +SOCKET=local:$RUNDIR/$NAME.sock + +# Include opendkim defaults if available +if [ -f /etc/default/opendkim ] ; then + . /etc/default/opendkim +fi + +if [ -f /etc/opendkim.conf ]; then + CONFIG_SOCKET=`awk '$1 == "Socket" { print $2 }' /etc/opendkim.conf` +fi + +# This can be set via Socket option in config file, so it's not required +if [ -n "$SOCKET" -a -z "$CONFIG_SOCKET" ]; then + DAEMON_OPTS="-p $SOCKET $DAEMON_OPTS" +fi + +DAEMON_OPTS="-f -x /etc/opendkim.conf -u $USER $DAEMON_OPTS" + +exec $DAEMON $DAEMON_OPTS 2>&1 \ No newline at end of file diff --git a/containers/docker/runit/opendmarc.sh b/containers/docker/runit/opendmarc.sh new file mode 100644 index 00000000..4da90f21 --- /dev/null +++ b/containers/docker/runit/opendmarc.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +DAEMON=/usr/sbin/opendmarc +NAME=opendmarc +USER=opendmarc +RUNDIR=/var/run/$NAME +SOCKET=local:$RUNDIR/$NAME.sock + +# Include opendkim defaults if available +if [ -f /etc/default/opendmarc ] ; then + . /etc/default/opendmarc +fi + +if [ -f /etc/opendmarc.conf ]; then + CONFIG_SOCKET=`awk '$1 == "Socket" { print $2 }' /etc/opendmarc.conf` +fi + +# This can be set via Socket option in config file, so it's not required +if [ -n "$SOCKET" -a -z "$CONFIG_SOCKET" ]; then + DAEMON_OPTS="-p $SOCKET $DAEMON_OPTS" +fi + +DAEMON_OPTS="-f -c /etc/opendmarc.conf -u $USER $DAEMON_OPTS" + +exec $DAEMON $DAEMON_OPTS 2>&1 diff --git a/containers/docker/runit/php5-fpm.sh b/containers/docker/runit/php5-fpm.sh new file mode 100644 index 00000000..c04f5a5b --- /dev/null +++ b/containers/docker/runit/php5-fpm.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +NAME=php5-fpm +DAEMON=/usr/sbin/$NAME +DAEMON_ARGS="--fpm-config /etc/php5/fpm/php-fpm.conf -F" + +# Read configuration variable file if it is present +[ -r /etc/default/$NAME ] && . /etc/default/$NAME + +exec $DAEMON $DAEMON_ARGS 2>&1 \ No newline at end of file diff --git a/containers/docker/runit/postfix.sh b/containers/docker/runit/postfix.sh new file mode 100644 index 00000000..3341e4e3 --- /dev/null +++ b/containers/docker/runit/postfix.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +exec 1>&2 + +test -f /etc/default/postfix && . /etc/default/postfix + +command_directory=`postconf -h command_directory` +daemon_directory=`$command_directory/postconf -h daemon_directory` +# make consistency check +$command_directory/postfix check 2>&1 +# run Postfix +exec $daemon_directory/master 2>&1 \ No newline at end of file diff --git a/containers/docker/runit/postgrey.sh b/containers/docker/runit/postgrey.sh new file mode 100644 index 00000000..fb8c1bce --- /dev/null +++ b/containers/docker/runit/postgrey.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# Read config file if it is present. +if [ -r /etc/default/postgrey ] +then + . /etc/default/postgrey +fi + +exec /usr/sbin/postgrey $POSTGREY_OPTS 2>&1 \ No newline at end of file diff --git a/containers/docker/runit/rsyslog.sh b/containers/docker/runit/rsyslog.sh new file mode 100755 index 00000000..fab06fce --- /dev/null +++ b/containers/docker/runit/rsyslog.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +exec rsyslogd -n 2>&1 diff --git a/containers/docker/runit/rsyslogd.sh b/containers/docker/runit/rsyslogd.sh deleted file mode 100755 index 497d38ad..00000000 --- a/containers/docker/runit/rsyslogd.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -rsyslogd -n diff --git a/containers/docker/runit/spampd.sh b/containers/docker/runit/spampd.sh new file mode 100644 index 00000000..fbbf1c25 --- /dev/null +++ b/containers/docker/runit/spampd.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +NAME='spampd' +PROGRAM=/usr/sbin/spampd + +if [ -f /etc/default/$NAME ]; then + . /etc/default/$NAME +fi + +istrue () { + ANS=$(echo $1 | tr A-Z a-z) + [ "$ANS" = 'yes' -o "$ANS" = 'true' -o "$ANS" = 'enable' -o "$ANS" = '1' ] +} + +# +# Calculate final commandline +# +S_TAGALL='' +S_AWL='' +S_LOCALONLY='' + +istrue "$TAGALL" \ +&& S_TAGALL='--tagall' + +istrue "$AUTOWHITELIST" \ +&& S_AWL='--auto-whitelist' + +istrue "$LOCALONLY" \ +&& S_LOCALONLY='--L' + +istrue "$LOGINET" \ +&& LOGTARGET="inet" \ +|| LOGTARGET="unix" + +ARGS="${S_LOCALONLY} ${S_AWL} ${S_TAGALL} " + +[ -n "${LISTENPORT}" ] && ARGS="${ARGS} --port=${LISTENPORT}" + +[ -n "${LISTENHOST}" ] && ARGS="${ARGS} --host=${LISTENHOST}" + +[ -n "${DESTPORT}" ] && ARGS="${ARGS} --relayport=${DESTPORT}" + +[ -n "${DESTHOST}" ] && ARGS="${ARGS} --relayhost=${DESTHOST}" + +[ -n "${PIDFILE}" ] && ARGS="${ARGS} --pid=${PIDFILE}" + +[ -n "${CHILDREN}" ] && ARGS="${ARGS} --children=${CHILDREN}" + +[ -n "${USERID}" ] && ARGS="${ARGS} --user=${USERID}" + +[ -n "${GRPID}" ] && ARGS="${ARGS} --group=${GRPID}" + +[ -n "${LOGTARGET}" ] && ARGS="${ARGS} --logsock=${LOGTARGET}" + +[ -n "${ADDOPTS}" ] && ARGS="${ARGS} ${ADDOPTS}" + +# Don't daemonize +ARGS="${ARGS} --nodetach" + +exec $PROGRAM $ARGS 2>&1 \ No newline at end of file diff --git a/management/utils.py b/management/utils.py index 24a2a0a7..0e2e2ab0 100644 --- a/management/utils.py +++ b/management/utils.py @@ -162,6 +162,8 @@ def shell(method, cmd_args, env={}, capture_stderr=False, return_bytes=False, tr def create_syslog_handler(): import logging.handlers + # This requires rsyslog to be started. + # Could be enhanced for docker logging with phusion/baseimage. handler = logging.handlers.SysLogHandler(address='/dev/log') handler.setLevel(logging.WARNING) return handler diff --git a/setup/functions.sh b/setup/functions.sh index ca64df8c..d739351a 100644 --- a/setup/functions.sh +++ b/setup/functions.sh @@ -166,17 +166,6 @@ function ufw_allow { function restart_service { # Restart a service quietly. - - if [[ ! -z "$IS_DOCKER" && "$1" == "dovecot" ]]; then - # In Docker, sysvinit takes care of any services with an init.d - # script. The dovecot package provides an Upstart config only, - # and so it won't work this way. We make a new script for it - # elsewhere. We also cant do `sv restart dovecot` because runit - # is not running until after the setup scripts are run. So we - # will have to skip starting dovecot for now. - return 0 - fi - hide_output service $1 restart } diff --git a/setup/management.sh b/setup/management.sh index 5c0c6920..ed425ce7 100755 --- a/setup/management.sh +++ b/setup/management.sh @@ -17,9 +17,22 @@ ln -s `pwd`/management/daemon.py /usr/local/bin/mailinabox-daemon # Create an init script to start the management daemon and keep it # running after a reboot. -rm -f /etc/init.d/mailinabox -ln -s $(pwd)/conf/management-initscript /etc/init.d/mailinabox -hide_output update-rc.d mailinabox defaults +if [ ! -z "$IS_DOCKER" ]; then + # Use runit for docker + mkdir -p /etc/service/mailinabox + cp /usr/local/mailinabox/containers/docker/runit/mailinabox.sh /etc/service/mailinabox/run + chmod +x /etc/service/mailinabox/run + + # runit -> LSB compatibility + # see http://smarden.org/runit/faq.html#lsb + mv /etc/init.d/mailinabox /etc/init.d/mailinabox.lsb + chmod -x /etc/init.d/mailinabox.lsb + ln -s /usr/bin/sv /etc/init.d/mailinabox +else + rm -f /etc/init.d/mailinabox + ln -s $(pwd)/conf/management-initscript /etc/init.d/mailinabox + hide_output update-rc.d mailinabox defaults +fi # Perform a daily backup. cat > /etc/cron.daily/mailinabox-backup << EOF; diff --git a/setup/questions.sh b/setup/questions.sh index 08531fe8..0b10a895 100644 --- a/setup/questions.sh +++ b/setup/questions.sh @@ -4,6 +4,7 @@ if [ -z "$NONINTERACTIVE" ]; then # e.g. if we piped a bootstrapping install script to bash to get started. In that # case, the nifty '[ -t 0 ]' test won't work. But with Vagrant we must suppress so we # use a shell flag instead. Really supress any output from installing dialog. + apt_get_quiet update apt_get_quiet install dialog message_box "Mail-in-a-Box Installation" \ "Hello and thanks for deploying a Mail-in-a-Box! diff --git a/setup/start.sh b/setup/start.sh index 24ec349d..3a859c37 100755 --- a/setup/start.sh +++ b/setup/start.sh @@ -39,7 +39,7 @@ fi # Put a start script in a global location. We tell the user to run 'mailinabox' # in the first dialog prompt, so we should do this before that starts. cat > /usr/local/bin/mailinabox << EOF; -#!/bin/bash + cd `pwd` source setup/start.sh EOF @@ -146,13 +146,6 @@ source setup/owncloud.sh source setup/zpush.sh source setup/management.sh -# In Docker, sysvinit services are started automatically. Runit services -# aren't started until after this setup script finishes. But we need -# Dovecot (which is Upstart-only) running in order to create the first -# mail user. So start dovecot now. -if [ ! -z "$IS_DOCKER" ]; then - /usr/sbin/dovecot -c /etc/dovecot/dovecot.conf -fi # Ping the management daemon to write the DNS and nginx configuration files. while [ ! -f /var/lib/mailinabox/api.key ]; do @@ -190,4 +183,4 @@ openssl x509 -in $STORAGE_ROOT/ssl/ssl_certificate.pem -noout -fingerprint \ | sed "s/SHA1 Fingerprint=//" echo echo Then you can confirm the security exception and continue. -echo \ No newline at end of file +echo