├── .project ├── PARAMETERS ├── README.md ├── install.sh ├── libs ├── cloudflare_api.sh ├── common.sh └── digitalocean_api.sh └── remote ├── docker-compose.yml ├── postfixadmin.conf └── remote_install.sh /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | mailserver-quicksetup 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /PARAMETERS: -------------------------------------------------------------------------------- 1 | export digitalocean_token=51ed08c5ca1ccc69572c330ec035cf7e0c69c723dd563ca077b51d2cbf6ba066 2 | export digitalocen_droplet_tag=sample_tag 3 | export cloudflare_token=ALxfPq8QMn37aRHPcsPUgNfTxU9sRrxVs58w12 4 | export cloudflare_email=youremail@domain.tld 5 | export postfix_admin_domain=domain.tld 6 | export postfix_admin_email=admin 7 | export mail_server_host=mail 8 | export docker_compose_password=xxxxxxx 9 | export staging_certs=true 10 | export private_key=id_smtp 11 | export additional_domains=aa.tld, www.bb.tld... 12 | 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quick setup a mailserver in ~10 minutes 2 | ## [hardware/mailserver](https://github.com/hardware/mailserver) (github project) + DigitalOcean + CloudFlare 3 | ### A simple and full-featured mail server using Docker 4 | 5 | What's it? 6 | 7 | ``` 8 | A script to automate all steps neded to install 9 | https://github.com/hardware/mailserver 10 | "a simple and full-featured mail server using Docker" 11 | on a digitalocean droplet. 12 | ``` 13 | After preparation it will perform all automated tasks in around 10 minutes. 14 | 15 | After doing a first install you will see how easy it is. 16 | 17 | For this server template, Webmail and Authoritative DNS was removed. 18 | 19 | ### Status from current version 20 | 21 | October-3-2017 22 | - All the installation process is working and the final server was tested. 23 | 24 | ## How to use it 25 | 26 | ### Preparation 27 | 28 | - have a key pair in your ~/.ssh/ folder. e.g. id_smtp and id_smtp.pub 29 | - have a cloudflare zone for the domain that will be used for this email server (optional) 30 | - generate a token from your digitalocean account to be used on api access by the install script 31 | - get cloudflare token from your cloudflare account to be used on api access by the install script (optional) 32 | - git clone https://github.com/rubentrancoso/mailserver-quicksetup.git 33 | - cd mailserver-quicksetup 34 | - change [PARAMETERS](PARAMETERS) file accordingly 35 | 36 | ``` 37 | export digitalocean_token=51ed08c5ca1ccc69572c330ec035cf7e0c69c723dd563ca077b51d2cbf6ba066 38 | export digitalocen_droplet_tag=sandbox_machine 39 | export cloudflare_enabled=true 40 | export cloudflare_token=ALxfPq8QMn37aRHPcsPUgNfTxU9sRrxVs58w12 41 | export cloudflare_email=youremail@domain.tld 42 | export postfix_admin_domain=example.com 43 | export postfix_admin_email=admin 44 | export mail_server_host=mail 45 | export docker_compose_password=123456 46 | export staging_certs=false 47 | export private_key=id_smtp 48 | export additional_domains=aa.tld, www.bb.tld... 49 | ``` 50 | 51 | ### Installation (10min) 52 | first part runs from a mac and uses brew (should be changed to run from another platforms) 53 | ``` 54 | # ./install (will create a droplet if it does not exists or rebuild an existing one) 55 | ``` 56 | folow the next 2 prompts. 57 | 58 | done. 59 | 60 | ### Unable to be automated 61 | 62 | - ask digitalocean to open port 25 63 | 64 | ## Missing steps (and TODOs) 65 | 66 | 1. Enter de generated token to the container. 67 | 68 | 2. Is certificate renew already automated? 69 | 70 | 3. Check why port 80 is not redirecting to ssl on postfixadmin 71 | 72 | 4. generate keys automatically (optional) 73 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | reset 3 | clear 4 | 5 | . PARAMETERS 6 | . libs/common.sh 7 | . libs/digitalocean_api.sh 8 | . libs/cloudflare_api.sh 9 | 10 | # install jq to parse json responses 11 | installjq 12 | # install resty to make rest calls from bash 13 | getresty 14 | 15 | # given a tag get the vm_id from digital ocean 16 | vm_id=`do_getvmidfromtag "$digitalocen_droplet_tag"` 17 | 18 | # if vm was not found exit 19 | if [ -z "$vm_id" ]; 20 | then 21 | log "no vm found. creating droplet..." 22 | # create a droplet with the given tag 23 | key_id=`do_getkeyidbyname "$private_key"` 24 | vm_id=`do_createdroplet "$digitalocen_droplet_tag" "$mail_server_host.$postfix_admin_domain" "$key_id"` 25 | else 26 | log "found vm $vm_id." 27 | # give the machine the right name 28 | do_renamedroplet "$vm_id" "$mail_server_host.$postfix_admin_domain" 29 | 30 | # 'format and reinstall' image to the vm 31 | do_rebuildimage "$vm_id" 32 | fi 33 | 34 | # wait machine to be rebuilt and be available 35 | # the problem here is that even if the machine is available 36 | # that does mean that it`s already acessible 37 | # so we check for the machine every 5 seconds 38 | do_waitmachinetobeready "$vm_id" 39 | 40 | # ask for the ip address so we can access the machine using ssh 41 | ip_address=`do_getvmipaddressfromid "$vm_id"` 42 | echo "export server_ip=$ip_address" > IP_ADDRESS 43 | 44 | # delete the machine from knowhosts so we can add the new finger print for the newly 45 | # created vm 46 | removemachinefromknownhosts "$ip_address" 47 | 48 | # wait until port 22 answer with ssh handshake 49 | waitforssh "$ip_address" 50 | 51 | # copy installation files to the remote host 52 | 53 | # the docker-compose file 54 | log "copying docker-compose to remote host..." 55 | result=`scp -o "StrictHostKeyChecking no" -i "~/.ssh/$private_key" remote/docker-compose.yml root@$ip_address:~/docker-compose.yml.tpl` 56 | 57 | # copy postfixadmin.conf file to the remote host 58 | log "copying postfixadmin.conf file to remote host..." 59 | result=`scp -o "StrictHostKeyChecking no" -i "~/.ssh/$private_key" remote/postfixadmin.conf root@$ip_address:~/postfixadmin.conf` 60 | 61 | # copy the installation script 62 | log "copying remote_install.sh to remote host..." 63 | result=`scp -o "StrictHostKeyChecking no" -i "~/.ssh/$private_key" remote/remote_install.sh root@$ip_address:~/remote_install.sh` 64 | # make the installation script runnable 65 | result=`ssh -i "~/.ssh/$private_key" root@$ip_address chmod +x /root/remote_install.sh` 66 | 67 | # copy PARAMETERS file to the remote host 68 | log "copying PARAMETERS file to remote host..." 69 | result=`scp -o "StrictHostKeyChecking no" -i "~/.ssh/$private_key" PARAMETERS root@$ip_address:~/PARAMETERS` 70 | 71 | # copy IP_ADDRESS to the remote host 72 | log "copying IP_ADDRESS to remote host..." 73 | result=`scp -o "StrictHostKeyChecking no" -i "~/.ssh/$private_key" IP_ADDRESS root@$ip_address:~/IP_ADDRESS` 74 | 75 | # copy libs/apis to the remote host 76 | log "copying libs/apis to remote host..." 77 | result=`scp -o "StrictHostKeyChecking no" -i "~/.ssh/$private_key" -r libs root@$ip_address:~/libs` 78 | 79 | log "preparing to run remote script on first login" 80 | ssh -i "~/.ssh/$private_key" root@$ip_address 'echo -e "~/remote_install.sh\nmv ~/remote_install.sh ~/remote_install.sh.done" >> ~/.bashrc' 81 | 82 | rm -rf IP_ADDRESS 83 | rm -rf resty 84 | 85 | log "-----------------------" 86 | 87 | if [ "$cloudflare_enabled" = "true" ]; 88 | then 89 | cf_update_record "$postfix_admin_domain" "A" "$mail_server_host.$postfix_admin_domain" "$ip_address" 90 | else 91 | log "you server ip address is: "$ip_address"\n" 92 | log "fix/review your dns records before continue.\n" 93 | log "set and A record with value $mail_server_host poinintg to $ip_address.\n" 94 | pause "Going to second stage. Press any key to continue..." 95 | fi 96 | 97 | log "-----------------------" 98 | log "-----------------------" 99 | log "entering remote host..." 100 | log "-----------------------" 101 | log "-----------------------" 102 | ssh -i "~/.ssh/$private_key" root@$ip_address 103 | 104 | -------------------------------------------------------------------------------- /libs/cloudflare_api.sh: -------------------------------------------------------------------------------- 1 | CLOUDFLARE_ENDPOINT="https://api.cloudflare.com/client/v4" 2 | 3 | log(){ 4 | echo -e "$1" >&2 5 | } 6 | 7 | cf_isusable() { 8 | log "testing if cloudflare is configured..." 9 | 10 | result=`curl -X GET "$CLOUDFLARE_ENDPOINT/user" \ 11 | -H "X-Auth-Email: $cloudflare_email" \ 12 | -H "X-Auth-Key: $cloudflare_token" \ 13 | -H "Content-Type: application/json"` 14 | result=`echo -e "$result" | jq '.success'` 15 | echo $result 16 | } 17 | 18 | cf_get_zone_identifier() { 19 | zone=$1 20 | log "getting zone identifier for $zone..." 21 | 22 | result=`curl -X GET "$CLOUDFLARE_ENDPOINT/zones?name=$zone" \ 23 | -H "X-Auth-Email: $cloudflare_email" \ 24 | -H "X-Auth-Key: $cloudflare_token" \ 25 | -H "Content-Type: application/json"` 26 | result=`echo -e "$result" | jq '.result[] | .id'` 27 | result=`removequotesfromstr $result` 28 | echo "$result" 29 | } 30 | 31 | removequotesfromstr(){ 32 | value=$1 33 | temp="${value%\"}" 34 | temp="${temp#\"}" 35 | echo "$temp" 36 | } 37 | 38 | cf_list_zones() { 39 | log "getting zones list..." 40 | 41 | result=`curl -X GET "$CLOUDFLARE_ENDPOINT/zones" \ 42 | -H "X-Auth-Email: $cloudflare_email" \ 43 | -H "X-Auth-Key: $cloudflare_token" \ 44 | -H "Content-Type: application/json"` 45 | result=`echo -e "$result" | jq -r '.result[] | .name'` 46 | echo "$result" 47 | } 48 | 49 | cf_get_record_identifier() { 50 | zone_id=$1 51 | rtype=$2 52 | name=$3 53 | 54 | log "getting dns record identifier for $zone type[$rtype] name[$name]..." 55 | 56 | result=`curl -X GET "$CLOUDFLARE_ENDPOINT/zones/$zone_id/dns_records" \ 57 | -H "X-Auth-Email: $cloudflare_email" \ 58 | -H "X-Auth-Key: $cloudflare_token" \ 59 | -H "Content-Type: application/json"` 60 | 61 | result=`echo "$result" | jq -r ".result[] | select(.type == \"$rtype\" ) | select(.name == \"$name\") | .id"` 62 | echo "$result" 63 | } 64 | 65 | cf_count_record() { 66 | zone_id=$1 67 | rtype=$2 68 | name=$3 69 | 70 | log "counting dns record $zone type[$rtype] name[$name]..." 71 | 72 | result=`curl -X GET "$CLOUDFLARE_ENDPOINT/zones/$zone_id/dns_records" \ 73 | -H "X-Auth-Email: $cloudflare_email" \ 74 | -H "X-Auth-Key: $cloudflare_token" \ 75 | -H "Content-Type: application/json"` 76 | 77 | result=`echo "$result" | jq -r ".result[] | select(.type == \"$rtype\" ) | select(.name == \"$name\") | .id" | wc -l` 78 | echo "$result" 79 | } 80 | 81 | cf_remove_record() { 82 | zone_id=$1 83 | record_id=$2 84 | 85 | log "removing dns record $record_id from $zone_id..." 86 | 87 | result=`curl -X DELETE "$CLOUDFLARE_ENDPOINT/zones/$zone_id/dns_records/$record_id" \ 88 | -H "X-Auth-Email: $cloudflare_email" \ 89 | -H "X-Auth-Key: $cloudflare_token" \ 90 | -H "Content-Type: application/json"` 91 | } 92 | 93 | 94 | cf_update_record_by_id() { 95 | zone_id=$1 96 | record_id=$2 97 | rtype=$3 98 | name=$4 99 | value=$5 100 | 101 | log "updating dns record $record_id from $zone_id..." 102 | 103 | result=`curl -X PUT "$CLOUDFLARE_ENDPOINT/zones/$zone_id/dns_records/$record_id" \ 104 | -H "X-Auth-Email: $cloudflare_email" \ 105 | -H "X-Auth-Key: $cloudflare_token" \ 106 | -H "Content-Type: application/json" \ 107 | -d "{\"type\":\"$rtype\",\"name\":\"$name\",\"content\":\"$value\"}"` 108 | } 109 | 110 | cf_create_record() { 111 | zone_id=$1 112 | rtype=$2 113 | name=$3 114 | value=$4 115 | 116 | log "creating dns record $zone type[$rtype] name[$name] value[$value]..." 117 | 118 | result=`curl -X POST "$CLOUDFLARE_ENDPOINT/zones/$zone_id/dns_records" \ 119 | -H "X-Auth-Email: $cloudflare_email" \ 120 | -H "X-Auth-Key: $cloudflare_token" \ 121 | -H "Content-Type: application/json" \ 122 | -d "{\"type\":\"$rtype\",\"name\":\"$name\",\"content\":\"$value\"}"` 123 | } 124 | 125 | cf_remove_duplicate_record() { 126 | zone_id=$1 127 | rtype=$2 128 | name=$3 129 | 130 | log "removing duplicate dns record $zone type[$rtype] name[$name]..." 131 | 132 | identifier_list=`cf_get_record_identifier "$zone_id" "$rtype" "$name"` 133 | reminder=`echo -n "$identifier_list" | tr '\n' ' ' | cut -d' ' -f1` 134 | to_remove_list=`echo -n "$identifier_list" | tr '\n' ' ' | cut -d' ' -f2-` 135 | 136 | if [ "$to_remove_list" = "$reminder" ]; 137 | then 138 | to_remove_list="" 139 | fi 140 | 141 | for identifier in $to_remove_list 142 | do 143 | cf_remove_record "$zone_id" "$identifier" 144 | done 145 | echo "$reminder" 146 | } 147 | 148 | cf_update_record() { 149 | zone=$1 150 | rtype=$2 151 | name=$3 152 | value=$4 153 | zone_id=`cf_get_zone_identifier "$zone"` 154 | 155 | reminder=`cf_remove_duplicate_record "$zone_id" "$rtype" "$name"` 156 | if [ "$reminder" = "" ]; 157 | then 158 | cf_create_record "$zone_id" "$rtype" "$name" "$value" 159 | else 160 | cf_update_record_by_id "$zone_id" "$reminder" "$rtype" "$name" "$value" 161 | fi 162 | } 163 | 164 | 165 | -------------------------------------------------------------------------------- /libs/common.sh: -------------------------------------------------------------------------------- 1 | 2 | commandseparator() { 3 | echo -e "\n" 4 | echo -e "---\n" 5 | echo -e "\n" 6 | } 7 | 8 | log(){ 9 | echo -e "$1" >&2 10 | } 11 | 12 | pause() { 13 | read -n1 -r -p "$1" key 14 | } 15 | 16 | # return true or false accorfing that jq is installed or not 17 | isjqinstalled(){ 18 | log "verifying if jq is installed..." 19 | if type jq >/dev/null 2>&1; 20 | then 21 | log "jq already installed." 22 | echo 0 23 | else 24 | log "jq installation not found." 25 | echo 1 26 | fi 27 | } 28 | 29 | installcommand() { 30 | OS=`uname` 31 | if [ "$OS" = "Linux" ] 32 | then 33 | var=`which yum` 34 | if [ -n "$var" ]; 35 | then 36 | sudo yum -y install $1 37 | fi 38 | var=`which apt-get` 39 | if [ -n "$var" ]; 40 | then 41 | sudo apt-get -y install $1 42 | fi 43 | fi 44 | if [ "$OS" = "Darwin" ] 45 | then 46 | brew install $1 47 | fi 48 | } 49 | 50 | installjq(){ 51 | # 0 - sucess / 1 - error (not found) 52 | if [ $(isjqinstalled) = 1 ] 53 | then 54 | log "installing jq..." 55 | installcommand jq 56 | fi 57 | } 58 | 59 | getresty(){ 60 | log "getting resty..." 61 | curl -sL https://raw.githubusercontent.com/micha/resty/master/resty > resty 62 | . resty 63 | } 64 | 65 | removequotesfromstr(){ 66 | value=$1 67 | temp="${value%\"}" 68 | temp="${temp#\"}" 69 | echo "$temp" 70 | } 71 | 72 | waitforssh(){ 73 | log "waiting for ssh..." 74 | ip=$1 75 | 76 | 77 | while : 78 | do 79 | if [[ $(nc -w 5 "$ip" 22 <<< "\0" ) =~ "OpenSSH" ]] ; then 80 | result="open ssh is running" 81 | fi 82 | if [ "$result" = "open ssh is running" ]; 83 | then 84 | return 85 | fi 86 | sleep 5 87 | done 88 | } 89 | 90 | removemachinefromknownhosts(){ 91 | log "remove machine from known hosts..." 92 | ip_address=`removequotesfromstr "$ip_address"` 93 | log `pwd` 94 | ssh-keygen -R "$ip_address" 95 | # sed -i '' '^$ip_address/d' ~/.ssh/known_hosts 96 | echo "~/.ssh/known_hosts" 97 | cat ~/.ssh/known_hosts 98 | echo "----------------" 99 | } 100 | 101 | fixssh(){ 102 | eval "$(ssh-agent)" 103 | ssh-add -K "~/.ssh/$private_key" 104 | } 105 | 106 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /libs/digitalocean_api.sh: -------------------------------------------------------------------------------- 1 | DO_ENDPOINT="https://api.digitalocean.com/v2" 2 | 3 | do_getvmidfromtag(){ 4 | tag=$1 5 | log "looking for vm with tag \"$tag\"..." 6 | result=`curl -sX GET -H "Content-Type: application/json" -H "Authorization: Bearer $digitalocean_token" "$DO_ENDPOINT/droplets?tag_name=$tag"` 7 | result=`echo -e "$result" | jq '.droplets[] | .id'` 8 | echo $result 9 | } 10 | 11 | do_getkeyidbyname() { 12 | key_name=$1 13 | result=`curl -X GET -H "Content-Type: application/json" -H "Authorization: Bearer $digitalocean_token" "$DO_ENDPOINT/account/keys"` 14 | result=`echo -e "$result" | jq '.ssh_keys[] | select(.name == "'$key_name.pub'") | .id'` 15 | echo $result 16 | } 17 | 18 | do_createdroplet(){ 19 | tag=$1 20 | droplet_name=$2 21 | sshkey_id=$3 22 | result=`curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer $digitalocean_token" -d '{"name":"'$droplet_name'","region":"nyc1","size":"1gb","image":"debian-9-x64","ssh_keys":['$sshkey_id'],"backups":false,"ipv6":false,"user_data":null,"private_networking":null,"volumes": null,"tags":["'$tag'"]}' "$DO_ENDPOINT/droplets"` 23 | result=`echo -e "$result" | jq '.droplet.id'` 24 | echo $result 25 | } 26 | 27 | do_rebuildimage(){ 28 | log "going to rebuild $1" 29 | vmid="$1" 30 | result=`curl -sX POST -H "Content-Type: application/json" -H "Authorization: Bearer $digitalocean_token" -d '{"type":"rebuild","image":"debian-9-x64"}' "$DO_ENDPOINT/droplets/$vmid/actions"` 31 | } 32 | 33 | do_getvmipaddressfromid(){ 34 | log "get ip_address from vm $1..." 35 | vmid="$1" 36 | result=`curl -sX GET -H "Content-Type: application/json" -H "Authorization: Bearer $digitalocean_token" "$DO_ENDPOINT/droplets/$vmid"` 37 | result=`echo -e "$result" | jq '.droplet.networks.v4[] | .ip_address'` 38 | echo `removequotesfromstr "$result"` 39 | } 40 | 41 | do_waitmachinetobeready(){ 42 | log "waiting machine to finish rebuild..." 43 | vmid="$1" 44 | sleep 10 45 | while : 46 | do 47 | result=`curl -sX GET -H "Content-Type: application/json" -H "Authorization: Bearer $digitalocean_token" "$DO_ENDPOINT/droplets/$vmid"` 48 | result=`echo -e "$result" | jq '.droplet.status'` 49 | if [ "$result" = "off" ]; 50 | then 51 | echo "-" 52 | else 53 | break 54 | fi 55 | sleep 5 56 | done 57 | } 58 | 59 | do_renamedroplet() { 60 | vm_id=$1 61 | new_name=$2 62 | echo -e "\"$new_name\"" 63 | log "changing droplet name to \"$new_name\"" 64 | curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer $digitalocean_token" -d '{"type":"rename","name":"'$new_name'"}' "$DO_ENDPOINT/droplets/$vm_id/actions" 65 | } 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /remote/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.1' 2 | 3 | services: 4 | mailserver: 5 | image: hardware/mailserver:1.1-stable 6 | container_name: mailserver 7 | domainname: ${mail_server_host}.${postfix_admin_domain} # Mail server A/MX/FQDN & reverse PTR = mail.domain.tld. 8 | hostname: mail 9 | restart: always 10 | ports: 11 | - "25:25" # SMTP - Required 12 | - "143:143" # IMAP STARTTLS - Optional - For webmails/desktop clients 13 | - "587:587" # Submission STARTTLS - Optional - For webmails/desktop clients 14 | - "993:993" # IMAPS SSL/TLS - Optional - For webmails/desktop clients 15 | # - "110:110" # POP3 STARTTLS - Optional - For webmails/desktop clients 16 | # - "465:465" # SMTPS SSL/TLS - Optional - Enabled for compatibility reason, otherwise disabled 17 | # - "995:995" # POP3S SSL/TLS - Optional - For webmails/desktop clients 18 | # - "4190:4190" # SIEVE STARTTLS - Optional - Recommended for mail filtering 19 | environment: 20 | - DBPASS=${docker_compose_password} # MariaDB database password (required) 21 | - RSPAMD_PASSWORD=${docker_compose_password} # Rspamd WebUI password (required) 22 | - ADD_DOMAINS=${additional_domains} # Add additional domains separated by commas (needed for dkim keys etc.) 23 | # - ENABLE_POP3=true # Enable POP3 protocol 24 | # - ENABLE_FETCHMAIL=true # Enable fetchmail forwarding 25 | # - DISABLE_CLAMAV=true # Disable virus scanning 26 | # - DISABLE_SIGNING=true # Disable DKIM/ARC signing 27 | # - DISABLE_GREYLISTING=true # Disable greylisting policy 28 | # - DISABLE_RATELIMITING=true # Disable ratelimiting policy 29 | # 30 | # Full list : https://github.com/hardware/mailserver#environment-variables 31 | # 32 | volumes: 33 | - /mnt/docker/mail:/var/mail 34 | - /mnt/docker/nginx/certs:/etc/letsencrypt 35 | depends_on: 36 | - mariadb 37 | - redis 38 | 39 | # Administration interface 40 | # https://github.com/hardware/postfixadmin 41 | # http://postfixadmin.sourceforge.net/ 42 | # Configuration : https://github.com/hardware/mailserver/wiki/Postfixadmin-initial-configuration 43 | postfixadmin: 44 | image: hardware/postfixadmin 45 | container_name: postfixadmin 46 | domainname: ${mail_server_host}.${postfix_admin_domain} 47 | hostname: mail 48 | restart: always 49 | environment: 50 | - DBPASS=${docker_compose_password} 51 | depends_on: 52 | - mailserver 53 | - mariadb 54 | 55 | # Web server 56 | # https://github.com/Wonderfall/dockerfiles/tree/master/boring-nginx 57 | # https://nginx.org/ 58 | # Configuration : https://github.com/hardware/mailserver/wiki/Reverse-proxy-configuration 59 | nginx: 60 | image: wonderfall/boring-nginx 61 | container_name: nginx 62 | restart: always 63 | ports: 64 | - "80:8000" 65 | - "443:4430" 66 | volumes: 67 | - /mnt/docker/nginx/sites-enabled:/sites-enabled 68 | - /mnt/docker/nginx/conf:/conf.d 69 | - /mnt/docker/nginx/log:/var/log/nginx 70 | - /mnt/docker/nginx/certs:/certs 71 | depends_on: 72 | - mailserver 73 | - postfixadmin 74 | 75 | # Database 76 | # https://github.com/docker-library/mariadb 77 | # https://mariadb.org/ 78 | mariadb: 79 | image: mariadb:10.1 80 | container_name: mariadb 81 | restart: always 82 | # Info : These variables are ignored when the volume already exists (databases created before). 83 | environment: 84 | - MYSQL_ROOT_PASSWORD=${docker_compose_password} 85 | - MYSQL_DATABASE=postfix 86 | - MYSQL_USER=postfix 87 | - MYSQL_PASSWORD=${docker_compose_password} 88 | volumes: 89 | - /mnt/docker/mysql/db:/var/lib/mysql 90 | 91 | # Cache Database 92 | # https://github.com/docker-library/redis 93 | # https://redis.io/ 94 | redis: 95 | image: redis:3.2-alpine 96 | container_name: redis 97 | restart: always 98 | command: redis-server --appendonly yes 99 | volumes: 100 | - /mnt/docker/redis/db:/data 101 | 102 | -------------------------------------------------------------------------------- /remote/postfixadmin.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 8000; 3 | server_name ${mail_server_host}.${postfix_admin_domain}; 4 | return 301 https://$host$request_uri; 5 | } 6 | 7 | server { 8 | listen 4430 ssl http2; 9 | server_name ${mail_server_host}.${postfix_admin_domain}; 10 | 11 | ssl_certificate /certs/live/${mail_server_host}.${postfix_admin_domain}/fullchain.pem; 12 | ssl_certificate_key /certs/live/${mail_server_host}.${postfix_admin_domain}/privkey.pem; 13 | 14 | include /etc/nginx/conf/ssl_params; 15 | include /etc/nginx/conf/headers_params; 16 | 17 | location / { 18 | proxy_pass http://postfixadmin:8888; 19 | include /etc/nginx/conf/proxy_params; 20 | } 21 | } -------------------------------------------------------------------------------- /remote/remote_install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -x 2 | 3 | . PARAMETERS 4 | . IP_ADDRESS 5 | . libs/common.sh 6 | . libs/digitalocean_api.sh 7 | . libs/cloudflare_api.sh 8 | 9 | # prepare 10 | 11 | sudo apt-get -y purge exim4* 12 | commandseparator 13 | sudo apt-get -y update 14 | commandseparator 15 | sudo apt-get -y install git-core 16 | commandseparator 17 | sudo apt-get -y install libterm-readline-gnu-perl 18 | commandseparator 19 | 20 | # install curl 21 | 22 | apt-get -y install curl 23 | commandseparator 24 | getresty 25 | commandseparator 26 | 27 | # install jq 28 | installjq 29 | commandseparator 30 | 31 | # install docker 32 | 33 | sudo apt-get -y remove docker docker-engine docker.io 34 | commandseparator 35 | sudo apt-get -y update 36 | commandseparator 37 | sudo apt-get install -y apt-transport-https ca-certificates wget software-properties-common 38 | commandseparator 39 | wget https://download.docker.com/linux/debian/gpg 40 | commandseparator 41 | sudo apt-key add gpg 42 | commandseparator 43 | echo "deb [arch=amd64] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | sudo tee -a /etc/apt/sources.list.d/docker.list 44 | commandseparator 45 | sudo apt-get -y update 46 | commandseparator 47 | sudo apt-cache policy docker-ce 48 | commandseparator 49 | sudo apt-get -y install docker-ce 50 | commandseparator 51 | sudo systemctl enable docker 52 | commandseparator 53 | sudo systemctl start docker 54 | commandseparator 55 | sudo docker run hello-world 56 | commandseparator 57 | 58 | # install docker-compose 59 | 60 | curl -L https://github.com/docker/compose/releases/download/1.14.0/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose 61 | commandseparator 62 | sudo chmod +x /usr/local/bin/docker-compose 63 | commandseparator 64 | ln -s /usr/local/bin/docker-compose /bin/docker-compose 65 | commandseparator 66 | 67 | # install digitalonecan monitoring 68 | 69 | sudo curl -sSL https://agent.digitalocean.com/install.sh | sh 70 | commandseparator 71 | 72 | # get container image 73 | 74 | docker pull hardware/mailserver:1.1-stable 75 | commandseparator 76 | 77 | # open ports 78 | 79 | log "open ports" 80 | commandseparator 81 | 82 | sudo systemctl stop docker 83 | commandseparator 84 | echo iptables-persistent iptables-persistent/autosave_v4 boolean true | sudo debconf-set-selections 85 | echo iptables-persistent iptables-persistent/autosave_v6 boolean true | sudo debconf-set-selections 86 | sudo apt-get -y install iptables-persistent 87 | commandseparator 88 | sudo iptables -A INPUT -p tcp --dport 25 --jump ACCEPT 89 | commandseparator 90 | sudo iptables -A INPUT -p tcp --dport 143 --jump ACCEPT 91 | commandseparator 92 | sudo iptables -A INPUT -p tcp --dport 587 --jump ACCEPT 93 | commandseparator 94 | sudo iptables -A INPUT -p tcp --dport 993 --jump ACCEPT 95 | commandseparator 96 | sudo iptables-save 97 | commandseparator 98 | sudo service netfilter-persistent start 99 | commandseparator 100 | sudo systemctl start docker 101 | commandseparator 102 | 103 | # replace placeholders on docker-compose.yml file 104 | envsubst < "docker-compose.yml.tpl" > "docker-compose.yml" 105 | 106 | # up stack 107 | 108 | apt-get -y install telnet 109 | commandseparator 110 | docker-compose up -d 111 | commandseparator 112 | 113 | # certificates installation 114 | # --verbose 115 | 116 | certbot_staging_arg= 117 | 118 | if [ "$staging_certs" = "true" ]; 119 | then 120 | certbot_staging_arg="--staging" 121 | fi 122 | 123 | docker-compose stop nginx 124 | commandseparator 125 | docker run -it --rm \ 126 | -v /mnt/docker/nginx/certs:/etc/letsencrypt \ 127 | -p 80:80 -p 443:443 \ 128 | xataz/letsencrypt \ 129 | certonly --standalone \ 130 | --non-interactive \ 131 | "$certbot_staging_arg" \ 132 | --rsa-key-size 4096 \ 133 | --agree-tos \ 134 | -m "$postfix_admin_email@$postfix_admin_domain" \ 135 | -d "$mail_server_host.$postfix_admin_domain" 136 | commandseparator 137 | 138 | # replace placeholders on postfixadmin.conf and place file on it`s right location 139 | envsubst < "postfixadmin.conf" > "/mnt/docker/nginx/sites-enabled/postfixadmin.conf" 140 | 141 | docker-compose up -d 142 | commandseparator 143 | 144 | # DKIM verification 145 | 146 | dkim_record_file="/mnt/docker/mail/dkim/$mail_server_host.$postfix_admin_domain/public.key" 147 | 148 | log "waiting for dkim files to be created" 149 | while [ ! -f "$dkim_record_file" ] 150 | do 151 | sleep 3 152 | echo -n "." 153 | done 154 | log 155 | commandseparator 156 | 157 | log "waiting for dkim files to be populated" 158 | while [ ! -s "$dkim_record_file" ] 159 | do 160 | echo -n "." 161 | sleep 1 162 | done 163 | log 164 | commandseparator 165 | 166 | # list all DKIM files 167 | log "DKIM files installed" 168 | cd /mnt/docker/mail/dkim/ 169 | for entry in *; 170 | do 171 | echo "$entry" 172 | done 173 | commandseparator 174 | cd - 175 | commandseparator 176 | 177 | cat "$dkim_record_file" 178 | commandseparator 179 | 180 | cat "$dkim_record_file" > DKIM.record 181 | commandseparator 182 | 183 | curl --insecure -X POST --data "form=setuppw&setup_password=$docker_compose_password&setup_password2=$docker_compose_password&submit=Generate+password+hash" "https://$mail_server_host.$postfix_admin_domain/setup.php" > response.html 184 | postfix_token=`cat response.html | sed -rn "s/.*\['setup_password'\] = '(.*)';<\/pre><\/div>/\1/p"` 185 | commandseparator 186 | 187 | log "your token is: $postfix_token" 188 | log "copy it to the prompt..." 189 | 190 | docker exec -ti postfixadmin setup 191 | 192 | curl --insecure -X POST --data "form=createadmin&setup_password=$docker_compose_password&username=$postfix_admin_email@$postfix_admin_domain&password=$docker_compose_password&password2=$docker_compose_password&submit=Add+Admin" "https://$mail_server_host.$postfix_admin_domain/setup.php" > response.html 193 | cat response.html | sed -rn 's/.*(The admin .*@.* has been added)\!.*/\1/p' 194 | commandseparator 195 | 196 | rm -rf PARAMETERS 197 | rm -rf response.html 198 | 199 | # populate cloudflare 200 | 201 | if [ "$cloudflare_enabled" = "true" ]; 202 | then 203 | log "will update the ip address on cloudflare zone using cloudflare_api (reminder)" 204 | cf_update_record "$postfix_admin_domain" "A" "$mail_server_host.$postfix_admin_domain" "$server_ip" 205 | cf_update_record "$postfix_admin_domain" "MX" "$postfix_admin_domain" "$mail_server_host.$postfix_admin_domain" 206 | cf_update_record "$postfix_admin_domain" "TXT" "_dmarc" "v=DMARC1; p=reject; rua=mailto:postmaster@$postfix_admin_domain; ruf=mailto:$postfix_admin_email@$postfix_admin_domain; fo=0; adkim=s; aspf=s; pct=100; rf=afrf; sp=reject" 207 | cf_update_record "$postfix_admin_domain" "TXT" "$postfix_admin_domain" "v=spf1 a mx -all" 208 | cf_update_record "$postfix_admin_domain" "TXT" "$mail_server_host._domainkey" "v=DKIM1; k=rsa; p=123456" 209 | else 210 | log "will inform the records to update as text" 211 | fi 212 | 213 | # make digitalocean & cloudflare optional 214 | 215 | log 216 | log "open postfixadmin at https://$mail_server_host.$postfix_admin_domain to finish installation" 217 | log "login with $postfix_admin_email@$postfix_admin_domain/$docker_compose_password" 218 | 219 | 220 | 221 | 222 | --------------------------------------------------------------------------------