diff --git a/Dockerfile b/Dockerfile index c3b8a7a1..64ec6704 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,22 +1,7 @@ -FROM python:3 -# Install required system packages -RUN export DEBIAN_FRONTEND=noninteractive \ - && apt-get update \ - && apt-get install -y --no-install-recommends \ - postfix dovecot-imapd dovecot-sqlite dovecot-lmtpd \ - dovecot-sieve dovecot-managesieved \ - dovecot-antispam spamassassin spamc clamav \ - php5-fpm php5-mysql php5-imap php5-sqlite php5-mcrypt \ - supervisor rsyslog nginx sqlite3 \ - && apt-get clean # Install the Webmail from source -ENV ROUNDCUBE_VERSION 1.1.4-complete -RUN curl -L -O https://downloads.sourceforge.net/project/roundcubemail/roundcubemail/1.1.4/roundcubemail-${ROUNDCUBE_VERSION}.tar.gz \ - && tar -xf roundcubemail-${ROUNDCUBE_VERSION}.tar.gz \ - && rm -f roundcubemail-${ROUNDCUBE_VERSION}.tar.gz \ - && mv roundcubemail-* /webmail + # Install the Web admin panel COPY admin /admin diff --git a/README.md b/README.md index 7bc22263..77bfd4b0 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,10 @@ Freeposte.io ============ -Simple yet functional and full-featured mail server as a single Docker image. +Simple yet functional and full-featured mail server as a set of Docker images. The idea behing Freeposte.io is identical to motivations that led to poste.io: -even though it looks like a Docker anti-pattern, single upgradable image -running a full-featured mail server is a truly amazing advantage for hosting -mails on modern cloud services or home-brewed Docker servers. +providing a simple and maintainable mail server that is painless to manage and +does not require more resources than necessary. People from poste.io did an amazing job at accomplishing this ; any company looking for a serious yet simple mail server with professional support should @@ -19,23 +18,32 @@ able to fine-tune some details if needed. How-to run your mail server =========================== -*Please note that this image is still in a very early stage. Do not use for +*Please note that this project is still in a very early stage. Do not use for production!* -The mail server runs as a single Docker container. A volume should be mounted to ``/data`` for persistent storage. Simply setup Docker on your -server then run a container with the ``kaiyou/freeposte.io`` image: +The mail server runs as a set of Docker containers. These containers are managed +through a ``docker-compose.yml`` configuration file that requires Docker Compose +to run. + +First, follow instructions at https://docs.docker.com to setup Docker and Docker +Compose properly for your system. Then download the main configuration file: ``` -docker run --name=freeposte -d \ - -e POSTMASTER_ADDRESS=admin@your.tld \ - -e MAIL_HOSTNAME=mail.your.tld \ - -e SECRET_KEY=yourflasksecretkey \ - -p 25:25 \ - -p 143:143 \ - -p 587:587 \ - -p 80:80 \ - -v /path/to/your/data:/data \ - kaiyou/freeposte.io +wget https://freeposte.io/docker-compose.yml +``` + +This file contains instructions about which containers to run and how they will +interact. You should also create a data directory. Freeposte will use ``/data`` +as a sane default: + +``` +mkdir -p /data +``` + +Finally, you can run your mail server: + +``` +docker-compose up -d ``` General architecture @@ -52,5 +60,3 @@ Additional Web UI : * Roundcube Webmail (can easily be replaced) ; * Administration UI based on Flask. - -All components are monitored by supervisord. diff --git a/admin/Dockerfile b/admin/Dockerfile new file mode 100644 index 00000000..6193aa18 --- /dev/null +++ b/admin/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3 + +RUN mkdir -p /app +WORKDIR /app + +COPY freeposte ./freeposte +COPY initdb.py . +COPY requirements.txt . + +RUN pip install -r requirements.txt + +CMD gunicorn -w 4 -b 0.0.0.0:80 --access-logfile - --error-logfile - freeposte:app diff --git a/admin/freeposte/__init__.py b/admin/freeposte/__init__.py index 89753f79..b4a976b0 100644 --- a/admin/freeposte/__init__.py +++ b/admin/freeposte/__init__.py @@ -10,7 +10,8 @@ app = Flask(__name__) default_config = { 'SQLALCHEMY_DATABASE_URI': 'sqlite:////data/freeposte.db', 'SQLALCHEMY_TRACK_MODIFICATIONS': False, - 'SECRET_KEY': None + 'SECRET_KEY': None, + 'DEBUG': False } # Load configuration from the environment if available @@ -21,4 +22,9 @@ for key, value in default_config.items(): # Create the database db = SQLAlchemy(app) -from freeposte import views +# Import views and models +from freeposte import models, views + +# Manage database upgrades if necessary +db.create_all() +db.session.commit() diff --git a/admin/freeposte/views.py b/admin/freeposte/views.py index 1345fbd9..74c16e8e 100644 --- a/admin/freeposte/views.py +++ b/admin/freeposte/views.py @@ -3,9 +3,11 @@ from flask_admin.contrib import sqla from freeposte import app, db, models +import os # Flask admin -admin = admin.Admin(app, name='Freeposte.io', template_mode='bootstrap3') +app_name = os.environ.get('APP_NAME', 'Freeposte.io') +admin = admin.Admin(app, name=app_name, template_mode='bootstrap3') class BaseModelView(sqla.ModelView): diff --git a/admin/requirements.txt b/admin/requirements.txt index b170d9ee..da2171b1 100644 --- a/admin/requirements.txt +++ b/admin/requirements.txt @@ -1,4 +1,4 @@ Flask Flask-Admin Flask-SQLAlchemy -uwsgi +gunicorn diff --git a/amavis/Dockerfile b/amavis/Dockerfile new file mode 100644 index 00000000..e8f94c49 --- /dev/null +++ b/amavis/Dockerfile @@ -0,0 +1,3 @@ +FROM alpine + +RUN apk add --update amavis spamassassin clamav && rm -rf /var/cache/apk/* diff --git a/config/clamav/empty b/config/clamav/empty deleted file mode 100644 index e69de29b..00000000 diff --git a/config/postfix/master.cf b/config/postfix/master.cf deleted file mode 100644 index 70646553..00000000 --- a/config/postfix/master.cf +++ /dev/null @@ -1,39 +0,0 @@ -# service type private unpriv chroot wakeup maxproc command + args -# (yes) (yes) (yes) (never) (100) - -# Exposed SMTP services -smtp inet n - - - - smtpd - -o content_filter=spamassassin -submission inet n - - - - smtpd - -o smtpd_enforce_tls=yes - -o smtpd_sasl_auth_enable=yes - -o smtpd_client_restrictions=permit_sasl_authenticated,reject -smtps inet n - - - - smtpd - -o smtpd_enforce_tls=yes - -o smtpd_sasl_auth_enable=yes - -o smtpd_client_restrictions=permit_sasl_authenticated,reject - -# Internal postfix services -pickup fifo n - n 60 1 pickup -cleanup unix n - - - 0 cleanup -qmgr unix n - n 300 1 qmgr -tlsmgr unix - - - 1000? 1 tlsmgr -rewrite unix - - - - - trivial-rewrite -bounce unix - - - - 0 bounce -defer unix - - - - 0 bounce -trace unix - - - - 0 bounce -proxymap unix - - - - - proxymap -verify unix - - - - 1 verify -flush unix n - - 1000? 0 flush -smtp unix - - - - - smtp -relay unix - - - - - smtp -error unix - - - - - error -retry unix - - - - - error -discard unix - - - - - discard -lmtp unix - - - - - lmtp -anvil unix - - - - 1 anvil -scache unix - - - - 1 scache - -# Utility services -spamassassin unix - n n - - pipe - user=nobody argv=/usr/bin/spamc -f -e /usr/sbin/sendmail -oi -f ${sender} ${recipient} diff --git a/config/rsyslog.conf b/config/rsyslog.conf deleted file mode 100644 index 489b97f2..00000000 --- a/config/rsyslog.conf +++ /dev/null @@ -1,5 +0,0 @@ -$ModLoad imuxsock -$ModLoad imklog - - -*.* /data/logs/mail.log diff --git a/config/spamassassin/empty b/config/spamassassin/empty deleted file mode 100644 index e69de29b..00000000 diff --git a/config/supervisor/supervisord.conf b/config/supervisor/supervisord.conf deleted file mode 100644 index 36133ee6..00000000 --- a/config/supervisor/supervisord.conf +++ /dev/null @@ -1,28 +0,0 @@ -[supervisord] -nodaemon = true -logfile = /data/logs/supervisord.log - -[program:nginx] -command = nginx -g 'daemon off;' - -[program:rsyslog] -command = rsyslogd -n - -[program:postfix] -command = /usr/lib/postfix/master -d - -[program:dovecot] -command = /usr/sbin/dovecot -c /etc/dovecot/dovecot.conf -F - -[program:spamassassin] -command = /usr/sbin/spamd - -[program:admin] -command = uwsgi --yaml /etc/uwsgi/apps-enabled/freeposte.yml -stdout_logfile = /data/logs/admin.log -stderr_logfile = /data/logs/admin-error.log - -[program:webmail] -command = php5-fpm -F -stdout_logfile = /data/logs/php.log -stderr_logfile = /data/logs/php-error.log diff --git a/config/uwsgi/apps-enabled/freeposte.yml b/config/uwsgi/apps-enabled/freeposte.yml deleted file mode 100644 index 1ea57b28..00000000 --- a/config/uwsgi/apps-enabled/freeposte.yml +++ /dev/null @@ -1,16 +0,0 @@ -uwsgi: - socket: /var/run/freeposte.sock - chown-socket: www-data:www-data - pidfile: /var/run/freeposte.pid - master: true - workers: 2 - - vacuum: true - plugins: python - wsgi-file: /admin/run.py - callable: app - processes: 1 - pythonpath: /usr/lib/python2.7/site-packages - pythonpath: /admin - catch-exceptions: true - post-buffering: 8192 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..22246503 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,48 @@ +http: + build: nginx + ports: + - "80:80" + - "443:443" + links: + - admin + - webmail + volumes: + - /tmp/data/certs:/certs + +imap: + build: dovecot + ports: + - "143:143" + - "993:993" + volumes: + - /tmp/data/freeposte:/data + - /tmp/data/mail:/mail + - /tmp/data/certs:/certs + +smtp: + build: postfix + ports: + - "25:25" + - "465:465" + - "587:587" + links: + - imap + volumes: + - /tmp/data/freeposte:/data + - /tmp/data/logs:/logs + - /tmp/data/certs:/certs + +admin: + build: admin + volumes: + - /tmp/data/freeposte:/data + environment: + - DEBUG=True + - SECRET_KEY=mysecretkey + +webmail: + build: roundcube + links: + - imap + volumes: + - /tmp/data/webmail:/data diff --git a/dovecot/Dockerfile b/dovecot/Dockerfile new file mode 100644 index 00000000..5884c1eb --- /dev/null +++ b/dovecot/Dockerfile @@ -0,0 +1,17 @@ +FROM alpine + +RUN echo "@testing http://nl.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories \ + && echo "@community http://dl-3.alpinelinux.org/alpine/edge/community/" >> /etc/apk/repositories \ + && apk add --update \ + dovecot \ + dovecot-sqlite \ + dovecot-pigeonhole-plugin@community \ + dovecot-antispam-plugin@testing \ + && rm -rf /var/cache/apk/* + +COPY conf /etc/dovecot +COPY sieve /var/lib/dovecot + +COPY start.sh /start.sh + +CMD ["/start.sh"] diff --git a/config/dovecot/after.sieve b/dovecot/conf/after.sieve similarity index 100% rename from config/dovecot/after.sieve rename to dovecot/conf/after.sieve diff --git a/config/dovecot/default.sieve b/dovecot/conf/default.sieve similarity index 100% rename from config/dovecot/default.sieve rename to dovecot/conf/default.sieve diff --git a/config/dovecot/dovecot-sql.conf.ext b/dovecot/conf/dovecot-sql.conf.ext similarity index 100% rename from config/dovecot/dovecot-sql.conf.ext rename to dovecot/conf/dovecot-sql.conf.ext diff --git a/config/dovecot/dovecot.conf b/dovecot/conf/dovecot.conf similarity index 77% rename from config/dovecot/dovecot.conf rename to dovecot/conf/dovecot.conf index 25a658dc..94c8a15b 100644 --- a/config/dovecot/dovecot.conf +++ b/dovecot/conf/dovecot.conf @@ -1,6 +1,7 @@ ############### # General ############### +log_path = /dev/stderr protocols = imap lmtp sieve postmaster_address = %{env:POSTMASTER_ADDRESS} hostname = %{env:MAIL_HOSTNAME} @@ -18,8 +19,8 @@ service dict { ############### first_valid_gid = 8 first_valid_uid = 8 -mail_location = maildir:/data/mail/%u -mail_home = /data/mail/%u +mail_location = maildir:/mail/%u +mail_home = /mail/%u mail_uid = mail mail_gid = mail mail_privileged_group = mail @@ -49,13 +50,13 @@ namespace inbox { # TLS ############### ssl = yes -ssl_cert = /usr/local/etc/php/conf.d/timezone.ini + +RUN cd /tmp \ + && curl -L -O ${ROUNDCUBE_URL} \ + && tar -xf *.tar.gz \ + && rm -f *.tar.gz \ + && rm -rf /var/www/html \ + && mv roundcubemail-* /var/www/html \ + && cd /var/www/html \ + && rm -rf CHANGELOG INSTALL LICENSE README.md UPGRADING composer.json-dist installer \ + && chown -R www-data: logs + +COPY config.inc.php /var/www/html/config/ + +COPY start.sh /start.sh + +CMD ["/start.sh"] diff --git a/config/roundcube.inc.php b/roundcube/config.inc.php similarity index 61% rename from config/roundcube.inc.php rename to roundcube/config.inc.php index c231f472..06ac6737 100644 --- a/config/roundcube.inc.php +++ b/roundcube/config.inc.php @@ -3,7 +3,7 @@ $config = array(); // Generals -$config['db_dsnw'] = 'sqlite:////data/webmail/roundcube.db'; +$config['db_dsnw'] = 'sqlite:////data/roundcube.db'; $config['des_key'] = 'rcmail-!24ByteDESkey*Str'; $config['identities_level'] = 3; $config['reply_all_mode'] = 1; @@ -18,10 +18,24 @@ $config['plugins'] = array( ); // Mail servers -$config['default_host'] = 'localhost'; +$config['default_host'] = 'tls://imap'; $config['default_port'] = 143; -$config['smtp_server'] = 'localhost'; -$config['smtp_port'] = 25; +$config['smtp_server'] = 'tls://smtp'; +$config['smtp_port'] = 587; +$config['smtp_user'] = '%u'; +$config['smtp_pass'] = '%p'; + +// We access the IMAP and SMTP servers locally with internal names, SSL +// will obviously fail but this sounds better than allowing insecure login +// from the outter world +$ssl_no_check = array( + 'ssl' => array( + 'verify_peer' => false, + 'verify_peer_name' => false, + ), +); +$config['imap_conn_options'] = $ssl_no_check; +$config['smtp_conn_options'] = $ssl_no_check; // Password management $config['password_driver'] = 'sql'; diff --git a/roundcube/start.sh b/roundcube/start.sh new file mode 100755 index 00000000..824009f9 --- /dev/null +++ b/roundcube/start.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +# Fix some permissions +chown -R www-data:www-data /data + +# Run apache +exec apache2-foreground