Git Product home page Git Product logo

docker-mailman's Introduction

permalink
/

GNU Mailman 3 Deployment with Docker

CircleCI

This repository hosts code for two docker images maxking/mailman-core and maxking/mailman-web both of which are meant to deploy GNU Mailman 3 in a production environment.

Docker is a container ecosystem which can run containers on several platforms. It consists of a tool called docker-compose which can be used to run multi-container applications. This repository consists of a docker-compose.yaml file which is a set of configurations that can be used to deploy the Mailman 3 Suite.

Please see release page for the releases and change log.

Release

The tags for the images are assumed to be release versions for images. This is going to be a somewhat common philosophy of distributing Container images where the images with same tags are usually updated with the new functionality.

Releases will follow the following rules:

  • Images tagged like A.B.C will never change. If you want to pin down versions of Images, use these tags.

  • Images tagged with A.B will correspond to the latest A.B.C version released. Releases in A.B series are supposed to be backwards compatible, i.e., any existing installation should not break when upgrading between subversions of A.B.C. So, if you want the latest updates and want to frequently update your installation without having to change the version numbers, you can use this.

  • Any changes in the minor version of Mailman components of the images will cause a bump in the minor version, e.g., A.(B+1) can have one or more updated Mailman components from A.B. Also, significant change in functionality, that might change how Images work or how people interact with the containers can also cause a bump in the minor version.

  • Major versions will change either when there are backwards incompatible changes or when the releases reach a certain set milestone or when there are bugfix releases for the internal components or both.

Container Registries

The container images are available from multiple container registries. Do specify an explicit version tag (e.g. 0.4.5 , MAJOR.MINOR like 0.4 also works as floating tag pointing to latest patch version) as tag latest is not updated anymore.

Mailman Core

  • ghcr.io/maxking/mailman-core
  • docker.io/maxking/mailman-core

Mailman Web

  • ghcr.io/maxking/mailman-web
  • docker.io/maxking/mailman-web

Postorius

  • ghcr.io/maxking/postorius
  • docker.io/maxking/postorius

Rolling Releases

Rolling releases are made up of Mailman Components installed from git source. Note that these releases are made up of un-released software and should be assumed to be beta quality.

Every commit is tested with Mailman's CI infrastructure and is included in rolling releases only if they have passed the complete test suite.

$ docker pull docker.io/maxking/mailman-web:rolling
$ docker pull docker.io/maxking/mailman-core:rolling

Rolling releases are built with every commit and also re-generated nightly. You can inspect the images to get which commit it was built using:

$ docker inspect --format '{{json .Config.Labels }}' mailman-core | python -m json.tool
{
    "version.git_commit": "45a4d7805b2b3d0e7c51679f59682d64ba02f05f",
}

$ docker inspect --format '{{json .Config.Labels }}' mailman-web | python -m json.tool
{
    "version.git_commit": "45a4d7805b2b3d0e7c51679f59682d64ba02f05f",
}
  • version.git_commit : This is the commit hash of the Dockerfile in the Github repo

Dependencies

  • Docker
  • Docker-compose

To install these on Ubuntu/Debian:

$ sudo apt install docker.io docker-compose

For other systems, you can read the official Docker documentation to install Docker from here and docker-compose from here.

Configuration

Most of the common configuration is handled through environment variables in the docker-compose.yaml. However, there is need for some extra configuration that interacts directly with the application. There are two configuration files on the host that interact directly with Mailman's settings. These files exist on the host running the containers and are imported at runtime in the containers.

  • /opt/mailman/core/mailman-extra.cfg : This is the configuration for Mailman Core and anything that you add here will be added to Core's configuration. You need to restart your mailman-core container for the changes in this file to take effect.

  • /opt/mailman/web/settings_local.py : This is the Django configuration that is imported by the existing configuration provided by the mailman-web container. This file is referred to as settings.py in most of the Postorius and Django documentation. To change or override any settings in Django/Postorius, you need to create/edit this file. A useful configuration for troubleshooting is DEBUG = True.

Also, note that if you need any other files to be accessible from the host to inside the container, you can place them at certain directories which are mounted inside the containers.

  • /opt/mailman/core in host maps to /opt/mailman/ in mailman-core container.
  • /opt/mailman/web in host maps to /opt/mailman-web-data in mailman-web container.

Mailman-web

These are the settings that you MUST change in your docker-compose.yaml before deploying:

  • SERVE_FROM_DOMAIN: The domain name from which Django will be served. To be added to ALLOWED_HOSTS in django settings. Default value is not set. This also replaces Django's default example.com SITE and becomes the default SITE (with SITE_ID=1).

  • HYPERKITTY_API_KEY: Hyperkitty's API Key, should be set to the same value as set for the mailman-core. (Not needed in case of Postorius-only version.)

  • MAILMAN_ADMIN_USER: The username for the admin user to be created by default.

  • MAILMAN_ADMIN_EMAIL: The email for the admin user to be created by default.

  • SECRET_KEY: Django's secret key, mainly used for signing cookies and others.

Please note here that if you choose to create the admin user using the environment variables mentioned above (MAILMAN_ADMIN_USER & MAILMAN_ADMIN_EMAIL), no password is set for your admin account. To set a password, plese follow the "Forgot Password" link on the "Sign In" page.

Mailman web is already configured to send emails through $SMTP_HOST as the MTA's address. If you want to modify it, you can set the value in under docker-compose.yaml for mailman-web container. By default, SMTP_HOST points to the gateway of the web container, which is the host itself.

You can also use the environment variables SMTP_HOST (defaults to the container's gateway), SMTP_PORT (defaults to 25), SMTP_HOST_USER (defaults to an empty string), SMTP_HOST_PASSWORD (defaults to an empty string), SMTP_USE_TLS (defaults to False) and SMTP_USE_SSL (defaults to False).

This is required in addition to the Setup your MTA section below, which covers email setup for Mailman Core.

For more details on how to configure this image, please look at Mailman-web's Readme

Mailman-Core

These are the variables that you MUST change in your docker-compose.yaml before deploying:

  • HYPERKITTY_API_KEY: Hyperkitty's API Key, should be set to the same value as set for the mailman-web. Skip the variable in case of non-Hyperkitty deployment.

  • DATABASE_URL: URL of the type driver://user:password@hostname:port/databasename for the django to use. If not set, the default is set to sqlite:///opt/mailman-web-data/mailmanweb.db. The standard docker-compose.yaml comes with it set to a postgres database. There is no need to change this if you are happy with PostgreSQL.

  • DATABASE_TYPE: Its value can be one of sqlite, postgres or mysql as these are the only three database types that Mailman 3 supports. Its default value is set to sqlite along with the default database class and default database url above.

  • DATABASE_CLASS: Default value is mailman.database.sqlite.SQLiteDatabase. The values for this can be found in the mailman's documentation here.

  • SMTP_HOST : outgoing host for SMTP connections

  • SMTP_PORT : use this port. 25, 587, whatever your host asks for.

  • SMTP_HOST_USER: authenticate this user

  • SMTP_HOST_PASSWORD: and use this password

  • SMTP_SECURE_MODE: security mode for smtp connection - can be smtp (no encryption), smtps or starttls

  • SMTP_VERIFY_HOSTNAME: defaults to true - verify, that certificate hostname is identical to SMTP_HOST

  • SMTP_VERIFY_CERT: defaults to true - verify, that certificate is valid

For more details on how to configure this image, please look Mailman-core's Readme

While the above configuration will allow you to run the images and possibly view the Web Frontend, it won't be functional until it is fully configured to to send emails.

To configure the mailman-core container to send emails, see the Setting your MTA section below.

Running

To run the containers, simply run:

$ mkdir -p /opt/mailman/core
$ mkdir -p /opt/mailman/web
$ git clone https://github.com/maxking/docker-mailman
$ cd docker-mailman
# Change some configuration variables as mentioned above.
$ docker-compose up -d

Note that the web frontend in the mailman-web container is, by default, only configured to serve dynamic content. Anything static like stylesheets, etc., is expected to be served directly by the web server. The static content exists at /opt/mailman/web/static and should be aliased to /static/ in the web server configuration.

See the nginx configuration as an example.

This command will do several things, most importantly:

  • Run a wsgi server using uwsgi for the Mailman's Django-based web frontend listening on port 8000. It will run 2 worker processes with 4 threads each. You may want to change the setting ALLOWED_HOSTS in the settings before deploying the application in production.

  • Run a PostgreSQL server with a default database, username, and password as mentioned in the docker-compose.yaml. You will have to change configuration files too if you change any of these.

  • Run mailman-core listening on port 8001 for REST API and port 8024 (LMTP server) for messages from your MTA. You will have to configure your MTA to send messages at this address.

Some more details about what the above system achieves is mentioned below. If you are only going to deploy a simple configuration, you don't need to read this. However, these are very easy to understand if you know how docker works.

  • First create a bridge network called mailman in the docker-compose.yaml. It will probably be named something else in your machine. All the containers mentioned (mailman-core, mailman-web, database) will join this network and are assigned static IPs. The host operating system is the default gateway from within these containers.

  • Spin off a mailman-core container attached to the mailman bridge network created above. It has GNU Mailman 3 core running inside it. Mailman core's REST API is available at port 8001 and LMTP server listens at port 8024.

  • Spin off a mailman-web container which has a Django application running with both Mailman's web frontend Postorius and Mailman's web-based Archiver running. Uwsgi server is used to run a web server with the configuration provided in this repository here. You may want to change the setting ALLOWED_HOSTS in the settings before deploying the application in production. You can do that by adding a /opt/mailman/web/settings_local.py which is imported by the Django when running.

  • Spin off a PostgreSQL database container which is used by both mailman-core and mailman-web as their primary database.

  • mailman-core mounts /opt/mailman/core from host OS at /opt/mailman in the container. Mailman's var directory is stored there so that it is accessible from the host operating system. Configuration for Mailman core is generated on every run from the environment variables provided. Extra configuration can also be provided at /opt/mailman/core/mailman-extra.cfg (on host), and will be added to generated configuration file. Mailman also needs another configuration file called mailman-hyperkitty.cfg and is also expected to be at /opt/mailman/core/ on the host OS.

  • mailman-web mounts /opt/mailman/web from the host OS to /opt/mailman-web-data in the container. It consists of the logs and settings_local.py file for Django.

  • database mounts /opt/mailman/database at /var/lib/postgresql/data so that PostgreSQL can persist its data even if the database containers are updated/changed/removed.

Setting up your MTA

The provided docker containers do not have an MTA in-built. You can either run your own MTA inside a container and have them relay emails to the mailman-core container or just install an MTA on the host and have them relay emails.

Exim4

To use Exim4, it should be setup to relay emails from mailman-core and mailman-web. The mailman specific configuration is provided in the repository at core/assets/exim. There are three files

  • 25_mm3_macros to be placed at /etc/exim4/conf.d/main/25_mm3_macros in a typical Debian install of exim4. Please change MY_DOMAIN_NAME to the domain name that will be used to serve mailman. Multi-domain setups will be added later.

  • 455_mm3_router to be placed at /etc/exim4/conf.d/router/455_mm3_router in a typical Debian install of exim4.

  • 55_mm3_transport to be placed at /etc/exim4/conf.d/transport/55_mm3_transport in a typical Debian install of exim4.

Also, the default configuration inside the mailman-core image has the MTA set to Exim, but just for reference, it looks like this:

# mailman.cfg
[mta]
incoming: mailman.mta.exim4.LMTP
outgoing: mailman.mta.deliver.deliver
lmtp_host: $MM_HOSTNAME
lmtp_port: 8024
smtp_host: $SMTP_HOST
smtp_port: $SMTP_PORT
configuration: python:mailman.config.exim4

Postfix

To use Postfix, edit the main.cf configuration file, which is typically at /etc/postfix/main.cf on Debian-based operating systems. Add mailman-core and mailman-web to mynetworks so it will relay emails from the containers and add the following configuration lines:

# main.cf

# Support the default VERP delimiter.
recipient_delimiter = +
unknown_local_recipient_reject_code = 550
owner_request_special = no

transport_maps =
    regexp:/opt/mailman/core/var/data/postfix_lmtp
local_recipient_maps =
    regexp:/opt/mailman/core/var/data/postfix_lmtp
relay_domains =
    regexp:/opt/mailman/core/var/data/postfix_domains

To configure Mailman to use Postfix, add MTA=postfix under mailman-core's environment section in the docker-compose.yaml:

  mailman-core:
    <snip>
    environment:
    - MTA=postfix

This will auto-generate the configuration to talk to Postfix assuming that Postfix is available at the gateway address for the container's bridge network at port 25. The final configuration can be found by executing:

$ docker exec mailman-core cat /etc/mailman.cfg

The postfix configuration that is generated looks like this:

[mta]
incoming: mailman.mta.postfix.LMTP
outgoing: mailman.mta.deliver.deliver
lmtp_host: $MM_HOSTNAME
lmtp_port: 8024
smtp_host: $SMTP_HOST
smtp_port: $SMTP_PORT
configuration: /etc/postfix-mailman.cfg

So, if you need to update the values, you can set SMTP_HOST, SMTP_PORT, MM_HOSTNAME environment variables in mailman-core container.

Please verify the output for [mta] section to ensure that it points to the right smtp_host (address to reach postfix from mailman-core container) and lmtp_host (address to reach mailman-core container from postfix).

The configuration file /etc/postfix-mailman.cfg is also generated automatically inside the mailman-core container and contains the configuration specific for Postfix.

Site Owner

Setup site owner address. By default, mailman is setup with the site_owner set to '[email protected]'. This should be pointing to a valid mailbox. Add the following to the '/opt/mailman/core/mailman-extra.cfg'.

[mailman]
# This address is the "site owner" address.  Certain messages which must be
# delivered to a human, but which can't be delivered to a list owner (e.g. a
# bounce from a list owner), will be sent to this address.  It should point to
# a human.
site_owner: [email protected]

Setting up search indexing

Hyperkitty in mailman-web image support full-text indexing. The current default indexing engine is Whoosh for historical reasons. It is highly recommended that you instead use Xapian for production use cases. The default will change when the next major version bump happens.

To configure your Mailman-web container to use Xapian, add the following to your settings_local.py:

HAYSTACK_CONNECTIONS = {
    'default': {
        'ENGINE': 'xapian_backend.XapianEngine',
        'PATH': "/opt/mailman-web-data/fulltext_index",
    },
}

If you have been using the default search indexing engine, you might have to re-index emails using the following command:

$ docker-compose exec mailman-web ./manage.py rebuild_index

This command can take some time if you a lot of emails, so please be patient!

Setting up your web server

It is advisable to run your Django (interfaced through WSGI server) through an actual webserver in production for better performance.

If you are using v0.1.0, the uwsgi server is configured to listen to requests at port 8000 using the HTTP protocol. Make sure that you preserve the HOST header when you proxy the requests from your Web Server. In Nginx you can do that by adding the following to your configuration:

    # Nginx configuration.
    location /static {
        alias /opt/mailman/web/static;
        autoindex off;
    }

    location / {
		  proxy_pass http://127.0.0.1:8000;
		  include uwsgi_params;
		  uwsgi_read_timeout 300;
		  proxy_set_header Host $host;
		  proxy_set_header X-Forwarded-For $remote_addr;
    }

Make sure you are using proxy_pass for the HTTP protocol.

uwsgi

Starting from v0.1.1, the uwsgi server is configured to listen to requests at port 8000 with the http protocol and port 8080 for the uwsgi protocol.

Please make sure that you are using port 8080 for uwsgi protocol.

It is advised to use the uwsgi protocol as it has better performance. Both Apache and Nginx have native support for the uwsgi protocol through plugins which are generally included in the distro packages.

To move to uwsgi protocol in the above nginx configuration use this

    # Nginx configuration.
    location /static {
        alias /opt/mailman/web/static;
        autoindex off;
    }

    location / {
		  uwsgi_pass localhost:8080;
		  include uwsgi_params;
		  uwsgi_read_timeout 300;
    }

Please make sure that you are using v0.1.1 or greater if you use this configuration.

Serving static files

UWSGI by default doesn't serve static files so, when running mailman-web using the provided docker-compose.yaml file, you won't see any CSS or JS files being served.

To enable serving of static files using UWSGI, add the following environment variable to your docker-compose.yaml file under mailman-web:

UWSGI_STATIC_MAP=/static=/opt/mailman-web-data/static

It is recommended to use web-server to serve static files instead of UWSGI for better performance. You will have to add an alias rule in your web server to serve the static files. See here for instructions on how to configure your web server. The STATIC_ROOT for you would be /opt/mailman/web/static.

SSL certificates

SSL Certificates from Lets Encrypt need to be renewed every 90 days. You can setup a cron job to do the job. I have this small shell script (certbot-renew.sh) that you can put up in /etc/cron.monthly to get the job done.

#! /bin/bash

cd /opt/letsencrypt/
./certbot-auto --config /etc/letsencrypt/renewal/MY_DOMAIN_NAME.conf certonly

if [ $? -ne 0 ]
 then
        ERRORLOG=`tail /var/log/letsencrypt/letsencrypt.log`
        echo -e "The Let's Encrypt cert has not been renewed! \n \n" \
                 $ERRORLOG
 else
        nginx -s reload
fi

exit 0

Please do not forget to make the script executable (chmod +x certbot-renew.sh).

LICENSE

This repository is licensed under the MIT License. Please see the LICENSE file for more details.

docker-mailman's People

Contributors

aiomaster avatar am97 avatar bbcchmc avatar binarious avatar brianemwd avatar cboylan avatar danil-smirnov avatar dependabot[bot] avatar dfukagaw28 avatar encephala avatar gangefors avatar hippu avatar ishitatsuyuki avatar joergmschulz avatar joren485 avatar makinbacon21 avatar marvingreenberg avatar maxking avatar miiichael avatar mm534 avatar mohazza00 avatar pgmillon avatar pini-gh avatar raph-topo avatar richardbrinkman avatar shoetten avatar skoranda avatar svenroederer avatar tbrowder avatar worikgh avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

docker-mailman's Issues

Test the images

I am still trying to find out some way to actually test the image without having to run it manually. There is some code and I don't way people to just trust my bash skills.

Use environment variables for network setup

The current setup uses manually assigned IPs, which need to be set up on docker-compose. This is limiting the ability to deploy mailman on environments where those IPs are already assigned or reserved by a software-defined network.

Changes would be needed at least on docker-compose.yaml, as well as on config files (e.g. settings.py), if we want to be able to setup mailman on networks with dynamic address assignment.

Add support for custom entrypoint hooks

This might be a dangerous thing to do but figure out a right way to let people run custom commands when starting up a container.

One use case that I can think off the top of my head is to load fixtures in the database.

ERROR: Pool overlaps with other one on this address space

Hi

Thanks for this projet ! Installing Mailman3 is tricky and I hope your docker images will help

However I'm getting an error at the early stage:

$ docker-compose up
Creating network "dockermailman_mailman" with driver "bridge"
ERROR: Pool overlaps with other one on this address space

the host is a regular Linux box:

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 16.04.1 LTS
Release:        16.04
Codename:       xenial

$ uname -a
Linux Hammer 4.4.0-59-generic #80-Ubuntu SMP Fri Jan 6 17:47:47 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux

The docker version seems up to date

docker version
Client:
 Version:      17.05.0-ce
 API version:  1.29
 Go version:   go1.7.5
 Git commit:   89658be
 Built:        Thu May  4 22:10:54 2017
 OS/Arch:      linux/amd64

Server:
 Version:      17.05.0-ce
 API version:  1.29 (minimum version 1.12)
 Go version:   go1.7.5
 Git commit:   89658be
 Built:        Thu May  4 22:10:54 2017
 OS/Arch:      linux/amd64
 Experimental: false

Which default address should Mailman Core bind to for LMTP?

Hi, after last updates my Mailman configuration stop working. Now when I up containers I get the following error in mailman-core:

test-mailman-core | Postgres is up - continuing
test-mailman-core | Found configuration file at /opt/mailman/mailman-extra.cfg
test-mailman-core | Traceback (most recent call last):
test-mailman-core |   File "/usr/local/bin/runner", line 11, in <module>
test-mailman-core |     sys.exit(main())
test-mailman-core |   File "/usr/local/lib/python3.6/site-packages/mailman/bin/runner.py", line 194, in main
test-mailman-core |     runner = make_runner(*args.runner, once=args.once)
test-mailman-core |   File "/usr/local/lib/python3.6/site-packages/mailman/bin/runner.py", line 94, in make_runner
test-mailman-core |     return runner_class(name, slice)
test-mailman-core |   File "/usr/local/lib/python3.6/site-packages/mailman/runners/rest.py", line 53, in __init__
test-mailman-core |     self._server = make_server()
test-mailman-core |   File "/usr/local/lib/python3.6/site-packages/mailman/rest/wsgiapp.py", line 234, in make_server
test-mailman-core |     handler_class=AdminWebServiceWSGIRequestHandler)
test-mailman-core |   File "/usr/local/lib/python3.6/wsgiref/simple_server.py", line 153, in make_server
test-mailman-core |     server = server_class((host, port), handler_class)
test-mailman-core |   File "/usr/local/lib/python3.6/socketserver.py", line 453, in __init__
test-mailman-core |     self.server_bind()
test-mailman-core |   File "/usr/local/lib/python3.6/wsgiref/simple_server.py", line 50, in server_bind
test-mailman-core |     HTTPServer.server_bind(self)
test-mailman-core |   File "/usr/local/lib/python3.6/http/server.py", line 136, in server_bind
test-mailman-core |     socketserver.TCPServer.server_bind(self)
test-mailman-core |   File "/usr/local/lib/python3.6/socketserver.py", line 467, in server_bind
test-mailman-core |     self.socket.bind(self.server_address)
test-mailman-core | OSError: [Errno 99] Address not available

The /opt/mailman/mailman-extra.cfg mentioned is as follows:

bash-4.3# cat /opt/mailman/mailman-extra.cfg
[mta]
incoming: mailman.mta.postfix.LMTP
outgoing: mailman.mta.deliver.deliver
lmtp_host: 172.19.199.18
lmtp_port: 8024
smtp_host: 172.19.199.17
smtp_port: 25
configuration: /etc/postfix-mailman.cfg

Docker network is configured as follows:

[root@host1 test]# docker network inspect test_mailman
[
    {
        "Name": "test_mailman",
        "Id": "37c9994c2089d5b2668ef8654600a216850ea292d263d0a5982b7e6dc7c41e7d",
        "Created": "2017-07-22T14:09:43.285147243+03:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.19.199.16/29"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Containers": {
            "1857292d3af9f1779a63837ae548ad457951c770cf10870e6dd92d695f53a837": {
                "Name": "nginx-proxy",
                "EndpointID": "7034d2d6c6d002d639bbf3089d24e59d6c44cadbc177f841e9052a5dde940752",
                "MacAddress": "02:42:ac:13:c7:15",
                "IPv4Address": "172.19.199.21/29",
                "IPv6Address": ""
            },
            "4499c3af9773be03c3395bd1cddeb064ef164f957815e457c6119cd56fe62a0d": {
                "Name": "test-database",
                "EndpointID": "0baa6d19835206abb413176a9028a8b8da669aa90938b19c3d8e5e2100029506",
                "MacAddress": "02:42:ac:13:c7:14",
                "IPv4Address": "172.19.199.20/29",
                "IPv6Address": ""
            },
            "9b6f03ab97f21822e73e7443f516350db42a88011228104814b62bdefc34f21b": {
                "Name": "test-mailman-core",
                "EndpointID": "a38fba9ccb5f333cd6d44f6c0be50991aa26f73aad3a161379d1f71854137fb9",
                "MacAddress": "02:42:ac:13:c7:12",
                "IPv4Address": "172.19.199.18/29",
                "IPv6Address": ""
            },
            "d0acd784d6ff1750f0d4f6e30294ba79bd3d0771ee82e977cced54b9be18dc6b": {
                "Name": "test-mailman-web",
                "EndpointID": "4afc0c28fe22a1dcbb22281a1ecc6cd053e6e54378601c8232c2fe5b6213fbb4",
                "MacAddress": "02:42:ac:13:c7:13",
                "IPv4Address": "172.19.199.19/29",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {}
    }
]

Can we please debug the error?

Connection refused to mailman-web container

When send mail to a list, I see the following error in the postfix logs on the host machine:

Jun 14 08:53:49 host1 postfix/lmtp[4117]: connect to 172.19.199.11[172.19.199.11]:8024: Connection refused
Jun 14 08:53:49 host1 postfix/lmtp[4117]: D3793600638D: to=<[email protected]>, relay=none, delay=0.23, delays=0.21/0.02/0/0, dsn=4.4.1, status=deferred (connect to 172.19.199.11[172.19.199.11]:8024: Connection refused)

I cannot telnet to container from the host neither:

telnet 172.19.199.11 8024
Trying 172.19.199.11...
telnet: connect to address 172.19.199.11: Connection refused

But the connectivity is okay, because I can telnet to 8000 port:

telnet 172.19.199.11 8000
Trying 172.19.199.11...
Connected to 172.19.199.11.
Escape character is '^]'.
quit

The containers showed by ps:

                      Name                                     Command               State    Ports   
-----------------------------------------------------------------------------------------------------
21936b22-cb35-4037-88a8-2644ff5bc30d-database       docker-entrypoint.sh postgres    Up      5432/tcp 
21936b22-cb35-4037-88a8-2644ff5bc30d-mailman-core   /opt/run.sh /usr/local/bin ...   Up      8001/tcp 
21936b22-cb35-4037-88a8-2644ff5bc30d-mailman-web    /opt/run.sh uwsgi --ini /o ...   Up      8000/tcp 

Need help to debug it further...

Check for database without using postgres-client

Remove the additional dependency of postgres-client in the images. It is possible check for it by just using psycopg2 in the python.

I am not sure though if we should add more python code to maintain or just rely on the postgres-client.

Move mailman-web to alpine linux

Alpine linux is the linux for containers, small and perfect for containers.

Core image has already been moved to alpine linux (pending a few tests after which it will be merged in the master branch).

Web image depends on sassc package which is only available from alpine v3.5 and the python images are still using v3.4. As soon as python image switches to v3.5, I can move to using alpine linux with mailman-web image too.

Multi Domain setup/configuration

This is mostly for Exim and not for postfix. One the pending Pull Request that adds regex tables for postfix lands in core, this image should be able to use postfix as an MTA. Postfix already has support for virtual domains and doesn't need any additional setup.
Exim configuration for mailman doesn't yet support multiple domains.

Support for other databases

Ideally, it should be possible for the images to support any database. Right now, the configuration makes it difficult to actually understand how to do that.
It is also more like a documentation problem as technically, it is possible to specify any database that mailman supports and expect everything to run.

Put mailman core config at /etc/mailman.cfg

This would help with a couple of things like configuring [paths.] so that there aren't multiple instances of mailman when trying to run them. Also, it would be generally helpful if people are trying out some commands inside the container to play around later.

Login error

Hi again, it seems I hit another bug.
The web interface working just fine if I login on page /admin/
But if I logout and then try to login again on page
/accounts/login/?next=/postorius/lists/
using the same credentials I've got error code 500 with the following message in the logs:

ERROR 2017-06-15 20:32:23,236 22 django.request Internal Server Error: /accounts/login/
Traceback (most recent call last):
  File "/usr/local/lib/python2.7/site-packages/django/core/handlers/exception.py", line 39, in inner
    response = get_response(request)
  File "/usr/local/lib/python2.7/site-packages/django/core/handlers/base.py", line 249, in _legacy_get_response
    response = self._get_response(request)
  File "/usr/local/lib/python2.7/site-packages/django/core/handlers/base.py", line 187, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "/usr/local/lib/python2.7/site-packages/django/core/handlers/base.py", line 185, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/usr/local/lib/python2.7/site-packages/django/views/generic/base.py", line 68, in view
    return self.dispatch(request, *args, **kwargs)
  File "/usr/local/lib/python2.7/site-packages/django/utils/decorators.py", line 67, in _wrapper
    return bound_func(*args, **kwargs)
  File "/usr/local/lib/python2.7/site-packages/django/views/decorators/debug.py", line 76, in sensitive_post_parameters_wrapper
    return view(request, *args, **kwargs)
  File "/usr/local/lib/python2.7/site-packages/django/utils/decorators.py", line 63, in bound_func
    return func.__get__(self, type(self))(*args2, **kwargs2)
  File "/usr/local/lib/python2.7/site-packages/allauth/account/views.py", line 128, in dispatch
    return super(LoginView, self).dispatch(request, *args, **kwargs)
  File "/usr/local/lib/python2.7/site-packages/allauth/account/views.py", line 74, in dispatch
    **kwargs)
  File "/usr/local/lib/python2.7/site-packages/django/views/generic/base.py", line 88, in dispatch
    return handler(request, *args, **kwargs)
  File "/usr/local/lib/python2.7/site-packages/allauth/account/views.py", line 97, in post
    response = self.form_valid(form)
  File "/usr/local/lib/python2.7/site-packages/allauth/account/views.py", line 141, in form_valid
    return form.login(self.request, redirect_url=success_url)
  File "/usr/local/lib/python2.7/site-packages/allauth/account/forms.py", line 177, in login
    redirect_url=redirect_url)
  File "/usr/local/lib/python2.7/site-packages/allauth/account/utils.py", line 149, in perform_login
    send_email_confirmation(request, user, signup=signup)
  File "/usr/local/lib/python2.7/site-packages/allauth/account/utils.py", line 313, in send_email_confirmation
    signup=signup)
  File "/usr/local/lib/python2.7/site-packages/allauth/account/models.py", line 60, in send_confirmation
    confirmation.send(request, signup=signup)
  File "/usr/local/lib/python2.7/site-packages/allauth/account/models.py", line 171, in send
    get_adapter(request).send_confirmation_mail(request, self, signup)
  File "/usr/local/lib/python2.7/site-packages/allauth/account/adapter.py", line 454, in send_confirmation_mail
    ctx)
  File "/usr/local/lib/python2.7/site-packages/allauth/account/adapter.py", line 142, in send_mail
    msg.send()
  File "/usr/local/lib/python2.7/site-packages/django/core/mail/message.py", line 342, in send
    return self.get_connection(fail_silently).send_messages([self])
  File "/usr/local/lib/python2.7/site-packages/django/core/mail/backends/smtp.py", line 107, in send_messages
    sent = self._send(message)
  File "/usr/local/lib/python2.7/site-packages/django/core/mail/backends/smtp.py", line 123, in _send
    self.connection.sendmail(from_email, recipients, message.as_bytes(linesep='\r\n'))
  File "/usr/local/lib/python2.7/smtplib.py", line 747, in sendmail
    raise SMTPRecipientsRefused(senderrs)
SMTPRecipientsRefused: {u'[email protected]': (451, '4.3.0 <[email protected]>: Temporary lookup failure')}

Add support for postfix

Right now the only supported MTA is exim4. This is because of the way different MTA are made aware about the addresses of the existing lists on the server.

To use postfix, mailman needs access to postmap command, which is supposed to be on the host and not accessible from within the container. A possible workaround for this could be to install postfix inside of the container too and then make it generate transport maps for the postfix installation outside the container.

Exim4 works differently. It can be configured to check for the lists by creating simple files/folders at a predefined location with the names of the lists. Mailman already does thist at var_dir/lists which is what we use here.

Allow chown failures

Both mailman-core and mailman-web bails when I mount the configs ro like this:

volumes:
  - ./mailman-extra.cfg:/opt/mailman/mailman-extra.cfg
chown: /opt/mailman/mailman-extra.cfg: Read-only file system
mailman-core exited with code 1

I prefer mounting them because /opt is hard to manage and doesn't scale.

Figure out a way to sign releases.

pypa/twine#157

It is possible to verify packages from PyPI which are signed optionally. I don't know if it makes sense at all to verify any package if you can't verify all of them. Also if it makes sense to verify all the packages (dependencies are a lot! and all may not have signatures).

Signing images are another story but security needs to come from bottom up.

Optimize containers to decrease memory consumption

It would be great if you can optimize containers images to decrease their memory consumption. Right after creation I see that mailman-core container occupies more than 500M of memory and mailnman-core takes about 200M.

uwsgi_pass doesn't work with http sockets in uwsgi

The v0.1.0 image has uwsgi's default configuration set to run on http which doesn't work if you want to use uwsgi protocol.

Either change the given Nginx configuration to use proxy_pass instead of uwsgi_pass or change the the uwsgi configuration to look something like this:

# Port on which uwsgi will be listening.
socket = :8000

Instead of

# Port on which uwsgi will be listening.
http = :8000

Error: no containers to start

Hello. Installed docker and compose as instructed on centos 7. Left it in its default loop storage mode just for testing. Cloned your GitHub and starting the docker produces:

Starting database ... done
Starting mailman-core ... done
Starting mailman-web ... done
ERROR: No containers to start

What have I missed?

django.template Exception while resolving variable '' in template ''

We, @gandhiano and me, find certain combinations of errors when deploying this set of containers. After invoking mailman aliases and trying to Sign up or Log in, we encountered timeout errors on those routes without understandable tracebacks for us. The following errors appear repeatingly, but are only represented with the most notable examples.

Login example

uwsgi.log reports

[pid: 27|app: 0|req: 30/30] 2.164.20.235 () {56 vars in 1129 bytes} [Fri Jul  7 20:45:03 2017] POST /accounts/login/ => generated 0 bytes in 254529 msecs (HTTP/1.1 500) 0 headers in 0 bytes (0 switches on core 0)

mailmanweb.log and uwsgi-error.log additionally report

DEBUG 2017-07-06 23:34:35,653 50 django.template Exception while resolving variable 'next' in template 'account/login.html'.
Traceback (most recent call last):
  File "/usr/local/lib/python2.7/site-packages/django/template/base.py", line 907, in _resolve_lookup
    (bit, current))  # missing attribute
VariableDoesNotExist: Failed lookup for key [next] in u"[{'False': False, 'None': None, 'True': True}, {u'csrf_token': <SimpleLazyObject: <function _get_val at 0x7f8fcb548578>>, u'site_name': u'example.com', u'LOGOUT_URL': 'account_logout', 'perms': <django.contrib.auth.context_processors.PermWrapper object at 0x7f8fcb587410>, 'messages': <django.contrib.messages.storage.fallback.FallbackStorage object at 0x7f8fcb5873d0>, 'use_mockups': None, u'request': <WSGIRequest: GET '/accounts/login/?next=/postorius/lists/'>, u'TIME_ZONE': 'UTC', u'STATIC_URL': '/static/', u'LANGUAGES': [(u'af', u'Afrikaans'), (u'ar', u'Arabic'), (u'ast', u'Asturian'), (u'az', u'Azerbaijani'), (u'bg', u'Bulgarian'), (u'be', u'Belarusian'), (u'bn', u'Bengali'), (u'br', u'Breton'), (u'bs', u'Bosnian'), (u'ca', u'Catalan'), (u'cs', u'Czech'), (u'cy', u'Welsh'), (u'da', u'Danish'), (u'de', u'German'), (u'dsb', u'Lower Sorbian'), (u'el', u'Greek'), (u'en', u'English'), (u'en-au', u'Australian English'), (u'en-gb', u'British English'), (u'eo', u'Esperanto'), (u'es', u'Spanish'), (u'es-ar', u'Argentinian Spanish'), (u'es-co', u'Colombian Spanish'), (u'es-mx', u'Mexican Spanish'), (u'es-ni', u'Nicaraguan Spanish'), (u'es-ve', u'Venezuelan Spanish'), (u'et', u'Estonian'), (u'eu', u'Basque'), (u'fa', u'Persian'), (u'fi', u'Finnish'), (u'fr', u'French'), (u'fy', u'Frisian'), (u'ga', u'Irish'), (u'gd', u'Scottish Gaelic'), (u'gl', u'Galician'), (u'he', u'Hebrew'), (u'hi', u'Hindi'), (u'hr', u'Croatian'), (u'hsb', u'Upper Sorbian'), (u'hu', u'Hungarian'), (u'ia', u'Interlingua'), (u'id', u'Indonesian'), (u'io', u'Ido'), (u'is', u'Icelandic'), (u'it', u'Italian'), (u'ja', u'Japanese'), (u'ka', u'Georgian'), (u'kk', u'Kazakh'), (u'km', u'Khmer'), (u'kn', u'Kannada'), (u'ko', u'Korean'), (u'lb', u'Luxembourgish'), (u'lt', u'Lithuanian'), (u'lv', u'Latvian'), (u'mk', u'Macedonian'), (u'ml', u'Malayalam'), (u'mn', u'Mongolian'), (u'mr', u'Marathi'), (u'my', u'Burmese'), (u'nb', u'Norwegian Bokm\\xe5l'), (u'ne', u'Nepali'), (u'nl', u'Dutch'), (u'nn', u'Norwegian Nynorsk'), (u'os', u'Ossetic'), (u'pa', u'Punjabi'), (u'pl', u'Polish'), (u'pt', u'Portuguese'), (u'pt-br', u'Brazilian Portuguese'), (u'ro', u'Romanian'), (u'ru', u'Russian'), (u'sk', u'Slovak'), (u'sl', u'Slovenian'), (u'sq', u'Albanian'), (u'sr', u'Serbian'), (u'sr-latn', u'Serbian Latin'), (u'sv', u'Swedish'), (u'sw', u'Swahili'), (u'ta', u'Tamil'), (u'te', u'Telugu'), (u'th', u'Thai'), (u'tr', u'Turkish'), (u'tt', u'Tatar'), (u'udm', u'Udmurt'), (u'uk', u'Ukrainian'), (u'ur', u'Urdu'), (u'vi', u'Vietnamese'), (u'zh-hans', u'Simplified Chinese'), (u'zh-hant', u'Traditional Chinese')], 'user': <SimpleLazyObject: <django.contrib.auth.models.AnonymousUser object at 0x7f8fcb5870d0>>, u'LOGIN_URL': 'account_login', u'LANGUAGE_CODE': 'en', 'DEFAULT_MESSAGE_LEVELS': {'DEBUG': 10, 'INFO': 20, 'WARNING': 30, 'SUCCESS': 25, 'ERROR': 40}, 'HYPERKITTY_VERSION': u'1.1.0', u'INSTALLED_APPS': ('hyperkitty', 'postorius', 'django_mailman3', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', 'django_gravatar', 'paintstore', 'compressor', 'haystack', 'django_extensions', 'django_q', 'allauth', 'allauth.account', 'allauth.socialaccount'), u'LANGUAGE_BIDI': False, u'MEDIA_URL': u''}, {}, {'compressed': {'name': None}, 'form': <LoginForm bound=False, valid=Unknown, fields=(login;password;remember)>, 'redirect_field_value': u'/postorius/lists/', 'redirect_field_name': 'next', 'signup_url': u'/accounts/signup/?next=%2Fpostorius%2Flists%2F', 'site': <Site: example.com>, u'view': <allauth.account.views.LoginView object at 0x7f8fcb640750>}]"

which hints at the account/login.html template and missing data in allauth.account.views.LoginView.

Signup example

uwsgi.og

[pid: 27|app: 0|req: 3/3] 2.164.20.235 () {50 vars in 1119 bytes} [Fri Jul  7 21:24:32 2017] POST /accounts/signup/ => generated 0 bytes in 254632 msecs (HTTP/1.1 500) 0 headers in 0 bytes (0 switches on core 0)

uwsgi-error.log and mailmanweb.log

DEBUG 2017-07-06 23:34:43,676 50 django.template Exception while resolving variable 'next' in template 'account/signup.html'.
Traceback (most recent call last):
  File "/usr/local/lib/python2.7/site-packages/django/template/base.py", line 907, in _resolve_lookup
    (bit, current))  # missing attribute
VariableDoesNotExist: Failed lookup for key [next] in u"[{'False': False, 'None': None, 'True': True}, {u'csrf_token': <SimpleLazyObject: <function _get_val at 0x7f8fcb548668>>, u'site_name': u'example.com', u'LOGOUT_URL': 'account_logout', 'perms': <django.contrib.auth.context_processors.PermWrapper object at 0x7f8fcb495a10>, 'messages': <django.contrib.messages.storage.fallback.FallbackStorage object at 0x7f8fcb495450>, 'use_mockups': None, u'request': <WSGIRequest: GET '/accounts/signup/?next=%2Fpostorius%2Flists%2F'>, u'TIME_ZONE': 'UTC', u'STATIC_URL': '/static/', u'LANGUAGES': [(u'af', u'Afrikaans'), (u'ar', u'Arabic'), (u'ast', u'Asturian'), (u'az', u'Azerbaijani'), (u'bg', u'Bulgarian'), (u'be', u'Belarusian'), (u'bn', u'Bengali'), (u'br', u'Breton'), (u'bs', u'Bosnian'), (u'ca', u'Catalan'), (u'cs', u'Czech'), (u'cy', u'Welsh'), (u'da', u'Danish'), (u'de', u'German'), (u'dsb', u'Lower Sorbian'), (u'el', u'Greek'), (u'en', u'English'), (u'en-au', u'Australian English'), (u'en-gb', u'British English'), (u'eo', u'Esperanto'), (u'es', u'Spanish'), (u'es-ar', u'Argentinian Spanish'), (u'es-co', u'Colombian Spanish'), (u'es-mx', u'Mexican Spanish'), (u'es-ni', u'Nicaraguan Spanish'), (u'es-ve', u'Venezuelan Spanish'), (u'et', u'Estonian'), (u'eu', u'Basque'), (u'fa', u'Persian'), (u'fi', u'Finnish'), (u'fr', u'French'), (u'fy', u'Frisian'), (u'ga', u'Irish'), (u'gd', u'Scottish Gaelic'), (u'gl', u'Galician'), (u'he', u'Hebrew'), (u'hi', u'Hindi'), (u'hr', u'Croatian'), (u'hsb', u'Upper Sorbian'), (u'hu', u'Hungarian'), (u'ia', u'Interlingua'), (u'id', u'Indonesian'), (u'io', u'Ido'), (u'is', u'Icelandic'), (u'it', u'Italian'), (u'ja', u'Japanese'), (u'ka', u'Georgian'), (u'kk', u'Kazakh'), (u'km', u'Khmer'), (u'kn', u'Kannada'), (u'ko', u'Korean'), (u'lb', u'Luxembourgish'), (u'lt', u'Lithuanian'), (u'lv', u'Latvian'), (u'mk', u'Macedonian'), (u'ml', u'Malayalam'), (u'mn', u'Mongolian'), (u'mr', u'Marathi'), (u'my', u'Burmese'), (u'nb', u'Norwegian Bokm\\xe5l'), (u'ne', u'Nepali'), (u'nl', u'Dutch'), (u'nn', u'Norwegian Nynorsk'), (u'os', u'Ossetic'), (u'pa', u'Punjabi'), (u'pl', u'Polish'), (u'pt', u'Portuguese'), (u'pt-br', u'Brazilian Portuguese'), (u'ro', u'Romanian'), (u'ru', u'Russian'), (u'sk', u'Slovak'), (u'sl', u'Slovenian'), (u'sq', u'Albanian'), (u'sr', u'Serbian'), (u'sr-latn', u'Serbian Latin'), (u'sv', u'Swedish'), (u'sw', u'Swahili'), (u'ta', u'Tamil'), (u'te', u'Telugu'), (u'th', u'Thai'), (u'tr', u'Turkish'), (u'tt', u'Tatar'), (u'udm', u'Udmurt'), (u'uk', u'Ukrainian'), (u'ur', u'Urdu'), (u'vi', u'Vietnamese'), (u'zh-hans', u'Simplified Chinese'), (u'zh-hant', u'Traditional Chinese')], 'user': <SimpleLazyObject: <django.contrib.auth.models.AnonymousUser object at 0x7f8fcb495210>>, u'LOGIN_URL': 'account_login', u'LANGUAGE_CODE': 'en', 'DEFAULT_MESSAGE_LEVELS': {'DEBUG': 10, 'INFO': 20, 'WARNING': 30, 'SUCCESS': 25, 'ERROR': 40}, 'HYPERKITTY_VERSION': u'1.1.0', u'INSTALLED_APPS': ('hyperkitty', 'postorius', 'django_mailman3', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', 'django_gravatar', 'paintstore', 'compressor', 'haystack', 'django_extensions', 'django_q', 'allauth', 'allauth.account', 'allauth.socialaccount'), u'LANGUAGE_BIDI': False, u'MEDIA_URL': u''}, {}, {'login_url': u'/accounts/login/?next=%2Fpostorius%2Flists%2F', 'form': <SignupForm bound=False, valid=Unknown, fields=(username;email;password1;password2)>, 'redirect_field_value': u'/postorius/lists/', 'compressed': {'name': None}, 'redirect_field_name': 'next', u'view': <allauth.account.views.SignupView object at 0x7f8fcb495290>}]"

pointing fingers at account/signup.html and the allauth.account.views.SignupView.
A combination of missing keys and related views was made available with

$ grep "in template" mailmanweb.log | awk '{ print $10 " " $13 }' | sort -u                                                                                                                                             
'add_url' 'admin/index.html'.
'auth_params' 'account/login.html'.
'can_change_related' 'admin/related_widget_wrapper.html'.
'can_delete_related' 'admin/related_widget_wrapper.html'.
'errors' 'admin/change_list.html'.
'is_multipart' 'admin/change_list.html'.
'is_popup' 'admin/app_index.html'.
'is_popup' 'admin/index.html'.
'is_popup' 'admin/login.html'.
'mlist' 'account/login.html'.
'mlist' 'account/logout.html'.
'mlist' 'account/signup.html'.
'mlist' 'hyperkitty/index.html'.
'next' 'account/login.html'.
'next' 'account/signup.html'.
'next' 'hyperkitty/index.html'.
'next' 'postorius/index.html'.
'non_field_errors' 'admin/change_list.html'.
'query' 'account/login.html'.
'query' 'account/logout.html'.
'query' 'account/signup.html'.
'query' 'hyperkitty/index.html'.
'query' 'postorius/lists/members.html'.
'scope' 'account/login.html'.
'show' 'admin/change_list.html'.
'user_request_pending' 'postorius/lists/summary.html'.

Which kinds of triaging, debugging and resolution could help here?
For the load balancer, this just looks like a 504 Gateway Time-out:

nginx                | 2017/07/06 15:47:25 [error] 6#6: *52 upstream prematurely closed connection while reading response header from upstream, client: 0.0.0.0, server: mailman.ecobytes.net, request: "POST /accounts/login/ HTTP/1.1", upstream: "http://172.18.0.6:8000/accounts/login/", host: "mailman", referrer: "https://mailman/accounts/login/"

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.