Bladeren bron

Initial commit

dennisro 7 jaren geleden
commit
a5c080b609
94 gewijzigde bestanden met toevoegingen van 6318 en 0 verwijderingen
  1. 7 0
      .gitignore
  2. 74 0
      README.md
  3. 152 0
      data/include/functions/configuration_functions.sh
  4. 175 0
      data/include/functions/helper_functions.sh
  5. 774 0
      data/include/functions/menu_functions.sh
  6. 396 0
      data/include/functions/setup_functions.sh
  7. 56 0
      data/include/init.sh
  8. 75 0
      data/services/bitbucket/bitbucket.sh
  9. 22 0
      data/services/bitbucket/containers.sh
  10. 44 0
      data/services/bitbucket/nginx/bitbucket.conf
  11. 10 0
      data/services/cryptpad/containers.sh
  12. 48 0
      data/services/cryptpad/cryptpad.sh
  13. 68 0
      data/services/cryptpad/nginx/cryptpad.conf
  14. 13 0
      data/services/cs50ide/containers.sh
  15. 94 0
      data/services/cs50ide/cs50ide.sh
  16. 2 0
      data/services/cs50ide/nginx/basic_auth.conf
  17. 56 0
      data/services/cs50ide/nginx/cs50ide.conf
  18. 9 0
      data/services/dillinger/containers.sh
  19. 54 0
      data/services/dillinger/dillinger.sh
  20. 55 0
      data/services/dillinger/nginx/dillinger.conf
  21. 10 0
      data/services/ghost/containers.sh
  22. 61 0
      data/services/ghost/ghost.sh
  23. 39 0
      data/services/ghost/nginx/ghost.conf
  24. 27 0
      data/services/gitea/containers.sh
  25. 238 0
      data/services/gitea/gitea.sh
  26. 14 0
      data/services/gitea/mysql/my.cnf
  27. 48 0
      data/services/gitea/nginx/gitea.conf
  28. 19 0
      data/services/gitlabce/containers.sh
  29. 61 0
      data/services/gitlabce/gitlabce.sh
  30. 50 0
      data/services/gitlabce/nginx/gitlabce.conf
  31. 27 0
      data/services/gogs/containers.sh
  32. 237 0
      data/services/gogs/gogs.sh
  33. 14 0
      data/services/gogs/mysql/my.cnf
  34. 48 0
      data/services/gogs/nginx/gogs.conf
  35. 9 0
      data/services/hastebin/containers.sh
  36. 48 0
      data/services/hastebin/hastebin.sh
  37. 55 0
      data/services/hastebin/nginx/hastebin.conf
  38. 7 0
      data/services/ipsecvpnserver/README.md
  39. 10 0
      data/services/ipsecvpnserver/containers.sh
  40. 94 0
      data/services/ipsecvpnserver/ipsecvpnserver.sh
  41. 9 0
      data/services/kanboard/containers.sh
  42. 62 0
      data/services/kanboard/kanboard.sh
  43. 70 0
      data/services/kanboard/nginx/kanboard.conf
  44. 143 0
      data/services/mailcowdockerized/mailcowdockerized.sh
  45. 64 0
      data/services/mailcowdockerized/nginx/mailcowdockerized.conf
  46. 3 0
      data/services/mailpile/README.md
  47. 9 0
      data/services/mailpile/containers.sh
  48. 94 0
      data/services/mailpile/mailpile.sh
  49. 53 0
      data/services/mailpile/nginx/mailpile.conf
  50. 210 0
      data/services/mastodon/.env-production
  51. 99 0
      data/services/mastodon/containers.sh
  52. 25 0
      data/services/mastodon/make_admin.sh
  53. 199 0
      data/services/mastodon/mastodon.sh
  54. 85 0
      data/services/mastodon/nginx/mastodon.conf
  55. 29 0
      data/services/nextcloud/containers.sh
  56. 98 0
      data/services/nextcloud/nextcloud.sh
  57. 64 0
      data/services/nextcloud/nginx/nextcloud.conf
  58. 3 0
      data/services/nginx/basic_auth.conf
  59. 17 0
      data/services/nginx/containers.sh
  60. 39 0
      data/services/nginx/ghost.chaosbunker.me.conf
  61. 25 0
      data/services/nginx/includes/gzip.conf
  62. 9 0
      data/services/nginx/includes/ssl.conf
  63. 34 0
      data/services/nginx/nginx.conf
  64. 45 0
      data/services/nginx/nginx.sh
  65. 13 0
      data/services/nginx/ssl/dhparam.pem
  66. 11 0
      data/services/openproject/containers.sh
  67. 55 0
      data/services/openproject/nginx/openproject.conf
  68. 77 0
      data/services/openproject/openproject.sh
  69. 23 0
      data/services/piwik/containers.sh
  70. 57 0
      data/services/piwik/nginx/piwik.conf
  71. 74 0
      data/services/piwik/piwik.sh
  72. 6 0
      data/services/seafilepro/README.md
  73. 46 0
      data/services/seafilepro/containers.sh
  74. 92 0
      data/services/seafilepro/nginx/seafilepro.conf
  75. 0 0
      data/services/seafilepro/seafile-license.txt
  76. 121 0
      data/services/seafilepro/seafilepro.sh
  77. 8 0
      data/services/searx/containers.sh
  78. 49 0
      data/services/searx/nginx/searx.conf
  79. 107 0
      data/services/searx/searx.sh
  80. 9 0
      data/services/send/containers.sh
  81. 57 0
      data/services/send/nginx/send.conf
  82. 48 0
      data/services/send/send.sh
  83. 15 0
      data/services/sftpserver/containers.sh
  84. 49 0
      data/services/sftpserver/nginx/sftpserver.conf
  85. 10 0
      data/services/sftpserver/run.sh
  86. 91 0
      data/services/sftpserver/sftpserver.sh
  87. 81 0
      data/services/statichtmlsite/nginx/statichtmlsite.conf
  88. 73 0
      data/services/statichtmlsite/statichtmlsite.sh
  89. 29 0
      data/services/wordpress/containers.sh
  90. 125 0
      data/services/wordpress/nginx/wordpress.conf
  91. 5 0
      data/services/wordpress/php/uploads.ini
  92. 28 0
      data/services/wordpress/wordpress.sh
  93. 171 0
      dockerbunker.sh
  94. BIN
      preview.png

+ 7 - 0
.gitignore

@@ -0,0 +1,7 @@
+.gitmodules
+data/web/*
+data/env/*
+data/conf/*
+data/Dockerfiles/*
+data/docker-compose/*
+

+ 74 - 0
README.md

@@ -0,0 +1,74 @@
+# What is dockerbunker
+
+`dockerbunker` is a tool that helps configure, deploy and manage dockerized web-applications or static sites behind an nginx reverse proxy. The only requirement is docker.
+
+[![asciicast](preview.png "dockerbunker asciicast preview")](https://asciinema.org/a/PGkj249ZRCtYKKSmpgqymBWmh)
+
+##### Currently included:
+
+| A - G        | H - O           | P - Z  |
+| :-------------: |:-------------:| :-----:|
+|[Bitbucket](https://www.atlassian.com/software/bitbucket)|[Hastebin](https://hastebin.com/about.md)|[Piwik](https://github.com/piwik/piwik)|
+|[cryptpad](https://cryptpad.fr/)|[IPsec VPN Server](https://github.com/hwdsl2/docker-ipsec-vpn-server)|[Seafile Pro](https://github.com/haiwen/seafile)|
+|[CS50 IDE](https://manual.cs50.net/ide/offline)|[Kanboard](https://kanboard.net/)|[Searx](https://github.com/asciimoo/searx.git)|
+|[Dillinger](https://dillinger.io/)|[Mailcow Dockerized](https://github.com/mailcow/mailcow-dockerized)| [Mozilla send](https://send.firefox.com/)|
+|[Ghost Blog](https://ghost.org/)|[Mailpile](https://www.mailpile.is/)|[sFTP Server](https://github.com/atmoz/sftp)|
+|[Gitea](https://gitea.io/en-us/)|[Mastodon](https://github.com/tootsuite/mastodon) (+ [Glitch Edition](https://github.com/glitch-soc/mastodon))|[Wordpress](https://wordpress.org/)|
+|[Gitlab CE](https://gitlab.com/)|[Nextcloud](https://github.com/nextcloud/docker)
+|[Gogs](https://gogs.io/)|[Open Project](https://www.openproject.org/)
+
+##### Planned:
+ - Easy backup and restore of volumes and databases
+
+## How to get started:
+
+1. Get docker
+
+    - Most systems can install Docker by running `wget -qO- https://get.docker.com/ | sh`
+
+3. Clone the master branch of this repository and run `./dockerbunker.sh`
+
+    - `git clone https://github.com/chaosbunker/dockerbunker.git && dockerbunker`
+	- `./dockerbunker.sh`
+
+4. Select a service and configure it (Set domain, etc..)
+
+5. Set up the service. This will
+	- Create an internal network if necessary
+	- Create volumes
+	- Pull or buld images
+	- Run containers
+	- Obtain certificate from Let's Encrypt (if chosen during config)
+
+That's it.
+
+Now when selecting the same service again in the dockerbunker menu, there will be more options depending on the current state of the service. For example:
+```
+Nextcloud
+1) Reconfigure service
+2) Reinstall service
+3) Obtain Let's Encrypt certificate (<-- only visible if using self-signed cert)
+4) Restart container(s)
+5) Stop container(s) (<- only visible when containers are running, otherwise offers "Start Containers"
+6) Upgrade Image(s)
+7) Destroy "Nextcloud"
+```
+
+When destroying a service everything related to the service will be removed. Only Let's Encrypt certificates will be retained.
+
+### SSL
+
+When configuring a service, a self-signed certificate is generated and stored in `data/conf/nginx/ssl/${SERVICE_HOSTNAME}`. Please move your own trusted certificate and key in that directory as `cert.pem` and `key.pem` after configuration of the service is complete.
+
+If you choose to use [Let's Encrypt](https://letsencrypt.org/) during setup, certificates will be automatically obtained via a Certbot container. Let's Encrypt data is stored in `data/conf/nginx/ssl/letsencrypt`.
+
+It is possible to add additional domains to the certificate before obtaining the certificate and these domains will also automatically be added to the corresponding nginx configuration.
+
+#### Good to know:
+All credentials that are set by the user or that are automatically generated are stored in data/env/${SERVICE_NAME}.env
+
+Please refer to the documentation of each web-app (regarding default credentials, configuration etc.)
+
+#### Why I made this
+
+I know that it is not really ideal and recommended to do something like this with bash alone. `dockerbunker` is an idea that went a bit out of control. It was inspired by [@DFabric's](https://github.com/DFabric/) [DPlatform-DockerShip](https://github.com/DFabric/DPlatform-DockerShip). You can read more about why I made dockerbunker [here](https://chaosbunker.com/projects/tech/dockerbunker) (tl;dr: I enjoyed the process)

+ 152 - 0
data/include/functions/configuration_functions.sh

@@ -0,0 +1,152 @@
+# All functions used during configuration of a service
+
+# only relevant if the menu shows "Configure service", although the service has already been configured or installed. This should only happen if things are messed up.
+pre_configure_routine() {
+	if [[ "${CONFIGURED_SERVICES[@]}" =~ ${PROPER_NAME} ]] || [[ -f "${ENV_DIR}/${SERVICE_NAME}" ]]|| [[ "${INSTALLED_SERVICES[@]}" =~ ${PROPER_NAME} ]];then
+		prompt_confirm  "Existing configuration found. Destroy containers and reconfigure?" && destroy || echo "Exiting..";exit
+	fi
+}
+
+# Ask the user what fqdn to use for the service
+set_domain() {
+	echo ""
+	[[ $INVALID ]] && echo -e "\nPlease enter a valid domain!\n"
+	INVALID=
+	while [[ -z $fqdn_is_valid ]];do
+		if [[ -z "${SERVICE_DOMAIN[0]}" ]]; then
+		  read -p "${PROPER_NAME} Service Domain (FQDN): " SERVICE_DOMAIN
+		else
+		  previous_domain=${SERVICE_DOMAIN[0]}
+		  unset SERVICE_DOMAIN
+		  read -p "${PROPER_NAME} Service Domain (FQDN): " -ei "${previous_domain}" SERVICE_DOMAIN
+		fi
+		validate_fqdn ${SERVICE_DOMAIN[0]}
+	done
+	INVALID=1
+	echo ""
+	
+	if [[ $PROMPT_SSL ]];then
+		configure_ssl
+	fi
+}
+
+# Ask user for mx info. User has the option to set for the service that is being configured global mx environment variables or service specific. This creates mx.env (if it does not exist yet) or ${SERVICE_NAME}_mx.env respectively
+configure_mx() {
+	echo ""
+	prompt_confirm "Use global SMTP Settings?"
+	if [[ $? == 1 ]];then
+		SERVICE_SPECIFIC_MX="${SERVICE_NAME}_"
+	else
+		unset SERVICE_SPECIFIC_MX
+	fi
+	if [[ ( $SERVICE_SPECIFIC_MX && ! -f "${ENV_DIR}/${SERVICE_SPECIFIC_MX}mx.env" ) || (  -z $SERVICE_SPECIFIC_MX && ! -f "${ENV_DIR}/mx.env" ) ]];then
+		echo -e "# \n# \e[4mSMTP Settings\e[0m"
+		
+		if [ "$MX_EMAIL" ]; then
+		  read -p "SMTP User: " -ei "$MX_EMAIL" MX_EMAIL
+		else
+		  read -p "SMTP User: " -ei "" MX_EMAIL
+		fi
+
+		unset MX_PASSWORD
+		while [[ -z ${MX_PASSWORD} ]];do
+			if [ $VALIDATE ];then
+				echo -e "\n\e[31m  Password cannot be empty.\e[0m"
+			fi
+			stty_orig=`stty -g`
+			stty -echo
+			read -p "SMTP Password: " -ei "" MX_PASSWORD
+			stty $stty_orig
+			echo ""
+			VALIDATE=1
+		done
+		unset VALIDATE
+		
+		invalid_mx=1
+		while [[ $invalid_mx ]];do
+			if [ -z "$MX_HOSTNAME" ]; then
+			  read -p "MX Hostname for email delivery (FQDN): " -ei "smtp.example.com" MX_HOSTNAME
+			else
+			  read -p "MX Hostname for email delivery (FQDN): " -ei "$MX_HOSTNAME" MX_HOSTNAME
+			fi
+			host -t mx ${MX_EMAIL#*@} | grep ${MX_HOSTNAME} >/dev/null \
+			&& unset invalid_mx \
+			|| echo -e "\n\e[31m${MX_HOSTNAME} not a valid mx entry for ${MX_EMAIL#*@}.\e[0m\n"
+		done
+		cat <<-EOF >> "${ENV_DIR}/${SERVICE_SPECIFIC_MX}mx.env"
+		#MX
+		## ------------------------------
+		
+		MX_HOSTNAME=${MX_HOSTNAME}
+		MX_EMAIL=${MX_EMAIL}
+		MX_PASSWORD="${MX_PASSWORD}"
+		
+		## ------------------------------
+		#/MX
+		EOF
+	fi
+}
+
+# Services that need to be connected to a fqdn will have the variable $PROMPT_SSL set. In that case this function asks if the user wants to get an SSL certificate from Let's Encrypt or keep using the self signed certificate that will be generated during configuration
+configure_ssl() {
+	prompt_confirm "Use Letsencrypt instead of a self-signed certificate?"
+	if [[ $? == 0 ]];then
+		SSL_CHOICE="le"
+		if [[ $LE_EMAIL ]]; then
+			prompt_confirm "Use ${LE_EMAIL} for Let's Encrypt?"
+			if [[ $? == 1 ]];then
+				read -p "Enter E-mail Adress for Let's Encrypt: " LE_EMAIL
+				if ! [[ $(grep ${LE_EMAIL} "${ENV_DIR}"/dockerbunker.env) ]];then
+					prompt_confirm "Use this address globally for every future service configured to obtain a Let's Encrypt certificate?"
+					if [[ $? == 0 && ! $(grep ${LE_EMAIL} "${ENV_DIR}"/dockerbunker.env) ]];then
+						sed -i "s/LE_EMAIL=.*/LE_EMAIL="${LE_EMAIL}"/" "${ENV_DIR}"/dockerbunker.env
+					fi
+				fi
+			fi
+		else
+			get_le_email
+		fi
+	else
+		SSL_CHOICE="ss"
+	fi
+}
+
+# Ask what email to use for Let's Encrypt
+get_le_email() {
+	echo ""
+	read -p "Existing E-mail Adress for letsencrypt: " -ei "" LE_EMAIL
+	[[ -f ${SERVICE_ENV} ]] && sed -i "s/LE_EMAIL=.*/LE_EMAIL=${LE_EMAIL}/" ${SERVICE_ENV}
+}
+
+# Update dockerbunker.env to let dockerbunker know that the service is now configured, then offer a menu to choose further steps (setup/reconfigure/destroy)
+post_configure_routine() {
+	if [[ ${STATIC} ]];then
+		if ! [[ "${STATIC_SITES[@]}" =~ "${SERVICE_DOMAIN[0]}" ]];then
+			STATIC_SITES+=( "${SERVICE_DOMAIN[0]}" )
+			sed -i '/STATIC_SITES/d' "${ENV_DIR}/dockerbunker.env"
+			declare -p STATIC_SITES >> "${ENV_DIR}/dockerbunker.env"
+		fi
+	else
+		[[ ${SERVICE_DOMAIN[0]} ]] && ! elementInArray "${PROPER_NAME}" "${!WEB_SERVICES[@]}" && WEB_SERVICES+=( [${PROPER_NAME}]="${SERVICE_DOMAIN[0]}" )
+		! elementInArray "${PROPER_NAME}" "${CONFIGURED_SERVICES[@]}" && CONFIGURED_SERVICES+=( "${PROPER_NAME}" )
+		for containers in ${add_to_network[@]};do
+			[[ ( $container && ! "${CONTAINERS_IN_DOCKERBUNKER_NETWORK[@]}" =~ "${container}" ) ]] && CONTAINERS_IN_DOCKERBUNKER_NETWORK+=( "${container}" )
+		done
+	
+		if [[ -f "${ENV_DIR}/dockerbunker.env" ]];then
+			sed -i '/CONFIGURED_SERVICES/d' "${ENV_DIR}/dockerbunker.env"
+			sed -i '/WEB_SERVICES/d' "${ENV_DIR}/dockerbunker.env"
+			sed -i '/CONTAINERS_IN_DOCKERBUNKER_NETWORK/d' "${ENV_DIR}/dockerbunker.env"
+			sed -i '/INSTALLED_SERVICES/d' "${ENV_DIR}/dockerbunker.env"
+			declare -p CONFIGURED_SERVICES >> "${ENV_DIR}/dockerbunker.env"
+			declare -p INSTALLED_SERVICES >> "${ENV_DIR}/dockerbunker.env"
+			declare -p WEB_SERVICES >> "${ENV_DIR}/dockerbunker.env"
+			declare -p CONTAINERS_IN_DOCKERBUNKER_NETWORK >> "${ENV_DIR}/dockerbunker.env" 2>/dev/null
+			echo ""
+			echo "Please run \"Setup service\" next."
+			bash "${BASE_DIR}/data/services/${SERVICE_NAME}/${SERVICE_NAME}.sh"
+		fi
+	fi
+}
+
+

+ 175 - 0
data/include/functions/helper_functions.sh

@@ -0,0 +1,175 @@
+# Helper functions that are used in multiple other functions
+
+# get user input
+prompt_confirm() {
+	while true; do
+		read -rs -n 1 -p "${1:-Continue?} [y/n]: " REPLY
+		case $REPLY in
+			[yY]) echo -e "\e[32m[Yes]\e[0m"; return 0 ;;
+			[nN]) echo -e "\e[32m[No]\e[0m"; return 1 ;;
+			*) echo -e "\e[31m[Invalid Input]\e[0m"
+		esac
+	done
+}
+
+# Displays green checkmark or red [failed] to indicate if a step successfully ran or failed
+exit_response() {
+	if [[ $? != 0 ]]; then
+		echo -e " \e[31m[failed]\e[0m"
+		return 1
+	else
+		echo -e " \e[32m\xE2\x9C\x94\e[0m"
+		return 0
+	fi
+}
+
+# insert item into array
+# thanks to http://cfajohnson.com/shell/?2013-01-08_bash_array_manipulation
+insert() {
+	local arrayname=${1:?Arrayname required} val=$2 num=${3:-1}
+	local array
+	eval "array=( \"\${$arrayname[@]}\" )"
+	[ $num -lt 0 ] && num=0 #? Should this be an error instead?
+	array=( "${array[@]:0:num}" "$val" "${array[@]:num}" )
+	eval "$arrayname=( \"\${array[@]}\" )"
+}
+
+# check if array contains element, only returning for exact matches
+elementInArray () {
+	local e match="$1"; shift
+	for e; do
+		[[ "$e" == "$match" ]] && return 0
+	done
+	return 1
+}
+
+# make sure the chosen domain is valid. Currently the regex is limited. Subdomains with less than 3 characters are considered invalid. Need to fix.
+validate_fqdn() {
+	# on macOS validation requires gnu grep (brew install grep)
+	[[ `which ggrep` ]] && g=g
+	local domain
+	domain=$1
+	unset fqdn_is_valid invalid_fqdn existing_domains
+	for conf_file in $(ls "${CONF_DIR}"/nginx/conf.d);do
+		existing_domains+=( ${conf_file%.conf*} )
+	done
+	validate_fqdn=$(echo $1 | ${g}grep -P "(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{1,63}(?<!-)\.)+[a-zA-Z]{2,63}$)")
+	if elementInArray "${domain}" "${existing_domains[@]}" && [[ -z $reconfigure ]];then
+		echo -e "\n\e[3m$domain is already in use\e[0m\n"
+	elif [[ $validate_fqdn  ]];then
+		fqdn_is_valid=1
+	else
+		echo -e "\n\e[3m$domain is not a valid domain\e[0m\n"
+	fi
+}
+
+# remove services from relevant arrays in dockerbunker.env after they have been installed/destroyed/configured/started etc..
+remove_from_WEB_SERVICES() {
+	for key in "${!WEB_SERVICES[@]}";do
+		if [[ "$key" =~ "${PROPER_NAME}" ]];then
+			unset WEB_SERVICES["$key"]
+		fi
+	done
+	sed -i '/WEB_SERVICES/d' "${ENV_DIR}/dockerbunker.env"
+	declare -p WEB_SERVICES >> "${ENV_DIR}/dockerbunker.env"
+}
+
+remove_from_CONFIGURED_SERVICES() {
+	CONFIGURED_SERVICES=("${CONFIGURED_SERVICES[@]/"${PROPER_NAME}"}");
+	for key in "${!CONFIGURED_SERVICES[@]}";do
+		if [[ "${CONFIGURED_SERVICES[$key]}" == "" ]];then
+			unset CONFIGURED_SERVICES[$key]
+			if [[ -z "${CONFIGURED_SERVICES[@]}" ]];then
+				unset ${CONFIGURED_SERVICES[@]}
+			fi
+		fi
+	sed -i '/CONFIGURED_SERVICES/d' "${ENV_DIR}/dockerbunker.env"
+	declare -p CONFIGURED_SERVICES >> "${ENV_DIR}/dockerbunker.env"
+	done
+}
+
+remove_from_STATIC_SITES() {
+	STATIC_SITES=("${STATIC_SITES[@]/"${SERVICE_DOMAIN[0]}"}");
+	for key in "${!STATIC_SITES[@]}";do
+		if [[ "${STATIC_SITES[$key]}" == "" ]];then
+			unset STATIC_SITES[$key]
+			if [[ -z "${STATIC_SITES[@]}" ]];then
+				unset ${STATIC_SITES[@]}
+			fi
+		fi
+	sed -i '/STATIC_SITES/d' "${ENV_DIR}/dockerbunker.env"
+	declare -p STATIC_SITES >> "${ENV_DIR}/dockerbunker.env"
+	done
+}
+
+remove_from_INSTALLED_SERVICES() {
+	INSTALLED_SERVICES=("${INSTALLED_SERVICES[@]/"${PROPER_NAME}"}");
+	for key in "${!INSTALLED_SERVICES[@]}";do
+		if [[ "${INSTALLED_SERVICES[$key]}" == "" ]];then
+			unset INSTALLED_SERVICES[$key]
+			if [[ -z "${INSTALLED_SERVICES[@]}" ]];then
+				unset ${INSTALLED_SERVICES[@]}
+			fi
+		fi
+	sed -i '/INSTALLED_SERVICES/d' "${ENV_DIR}/dockerbunker.env"
+	declare -p INSTALLED_SERVICES >> "${ENV_DIR}/dockerbunker.env"
+	done
+}
+
+remove_from_STOPPED_SERVICES() {
+	STOPPED_SERVICES=("${STOPPED_SERVICES[@]/"${PROPER_NAME}"}");
+	for key in "${!STOPPED_SERVICES[@]}";do
+		if [[ "${STOPPED_SERVICES[$key]}" == "" ]];then
+			unset STOPPED_SERVICES[$key]
+			if [[ -z "${STOPPED_SERVICES[@]}" ]];then
+				unset ${STOPPED_SERVICES[@]}
+			fi
+		fi
+	done
+	sed -i '/STOPPED_SERVICES/d' "${ENV_DIR}/dockerbunker.env"
+	declare -p STOPPED_SERVICES >> "${ENV_DIR}/dockerbunker.env"
+}
+
+remove_from_CONTAINERS_IN_DOCKERBUNKER_NETWORK() {
+	for container in ${add_to_network[@]};do
+		CONTAINERS_IN_DOCKERBUNKER_NETWORK=("${CONTAINERS_IN_DOCKERBUNKER_NETWORK[@]/"$container"}");
+		for key in "${!CONTAINERS_IN_DOCKERBUNKER_NETWORK[@]}";do
+			if [[ "${CONTAINERS_IN_DOCKERBUNKER_NETWORK[$key]}" == "" ]];then
+				unset CONTAINERS_IN_DOCKERBUNKER_NETWORK[$key]
+				if [[ -z "${CONTAINERS_IN_DOCKERBUNKER_NETWORK[@]}" ]];then
+					unset ${CONTAINERS_IN_DOCKERBUNKER_NETWORK[@]}
+				fi
+			fi
+		done
+		sed -i '/CONTAINERS_IN_DOCKERBUNKER_NETWORK/d' "${ENV_DIR}/dockerbunker.env"
+		declare -p CONTAINERS_IN_DOCKERBUNKER_NETWORK >> "${ENV_DIR}/dockerbunker.env" 2>/dev/null
+	done
+}
+
+collectAllImageNamesFromDockerComposeFile() {
+	unset IMAGES
+	for i in $(grep "image:" ${SERVICE_HOME}/docker-compose.yml | awk '{print $NF}');do IMAGES+=( \"$i\" );done
+}
+
+collectImageNamesAndCorrespondingSha256() {
+	[[ $DOCKER_COMPOSE ]] && collectAllImageNamesFromDockerComposeFile
+	for image in ${IMAGES[@]};do
+		image=${image//\"}
+		tag=${image#*:}
+		image=${image%%:*}
+		sha256=$(\
+			docker images --no-trunc \
+				| grep $image \
+				| grep $tag \
+				| awk '{for (i=1;i<=NF;i++){if ($i ~/^sha256/) {print $i}}}' \
+				| awk -F":" '{print $NF}'\
+			)
+		declare -gA test IMAGES_AND_SHA256
+		IMAGES_AND_SHA256[$image]+=$sha256
+	done
+}
+
+say_done() {
+	echo -e "\n\e[1mDone.\e[0m"
+}
+

+ 774 - 0
data/include/functions/menu_functions.sh

@@ -0,0 +1,774 @@
+# The options menu and all its associated functions
+# Options are only shown if relevant in that moment (e.g. "setup" only if service is already configured, "configure" only if service has not yet been configured, "reconfigure" if service is already configured, "destroy" only if it has been configured or installed etc...)
+# Services marked orange are configured but not installed.
+# Services marked green are installed and running
+# If containers of a service are currently stopped the services will say (stopped) behind the service name. This only works if the service has been stopped via the dockerbunker menu, because only then the service is marked as stopped in dockerbunker.env
+options_menu() {
+	COLUMNS=12
+	exitmenu=$(printf "\e[1;4;33mExit\e[0m")
+	returntopreviousmenu=$(printf "\e[1;4;33mReturn to previous menu\e[0m")
+	
+	container_check=1
+
+	# if service is marked as installed, make sure all containers exist and offer to run them if necessary
+	if elementInArray "${PROPER_NAME}" "${INSTALLED_SERVICES[@]}";then
+		for container in "${containers[@]}";do
+			RUNNING=$(docker ps -a -q --filter name=^/${container}$)
+			while [[ -z ${RUNNING} ]];do
+				echo -e "\n\e[3m$container container missing\e[0m\n" \
+				&& prompt_confirm "Re-run container?" \
+				&& docker_run ${container//-/_}
+			RUNNING=$(docker ps -a -q --filter name=^/${container}$)
+			done
+		done
+	fi
+	if [[ $RUNNING ]];then
+		menu=( "Reconfigure service" "Reinstall service" "Upgrade Image(s)" "Destroy \"${PROPER_NAME}\"" "$exitmenu" )
+		add_ssl_menuentry menu 2
+		if elementInArray "${PROPER_NAME}" "${STOPPED_SERVICES}";then
+			insert menu "Start container(s)" 3
+		else
+			insert menu "Restart container(s)" 3
+			insert menu "Stop container(s)" 4
+		fi
+	elif [[ $RUNNING == "false" ]];then
+		menu=( "Reconfigure service" "Reinstall service" "Start container(s)" "Destroy \"${PROPER_NAME}\"" "$exitmenu" )
+		add_ssl_menuentry menu 2
+	else
+		if ! elementInArray "${PROPER_NAME}" "${CONFIGURED_SERVICES[@]}" \
+		&& ! elementInArray "${PROPER_NAME}" "${INSTALLED_SERVICES[@]}" \
+		&& [[ ! -f "${ENV_DIR}/${SERVICE_NAME}.env" ]];then
+			[[ ${STATIC} \
+				&& $(ls -A "${ENV_DIR}"/static) ]] \
+				&& menu=( "Configure Site" "Manage Sites" "$exitmenu" ) \
+				|| menu=( "Configure Site" "$exitmenu" )
+		elif ! elementInArray "${PROPER_NAME}" "${CONFIGURED_SERVICES[@]}" \
+		&& ! elementInArray "${PROPER_NAME}" "${INSTALLED_SERVICES[@]}";then
+			menu=( "Destroy \"${PROPER_NAME}\"" "$exitmenu" )
+			error="Environment file found but ${PROPER_NAME} is not marked as configured or installed. Please destroy first!"
+		elif elementInArray "${PROPER_NAME}" "${CONFIGURED_SERVICES[@]}" \
+		&& [[ ! -f "${ENV_DIR}/${SERVICE_NAME}.env" ]];then
+				error="Service marked as configured, but configuration file is missing. Please destroy first."
+				menu=( "Destroy \"${PROPER_NAME}\"" "$exitmenu" )
+		elif elementInArray "${PROPER_NAME}" "${CONFIGURED_SERVICES[@]}" \
+		&& [[ -f "${ENV_DIR}/${SERVICE_NAME}.env" ]];then
+			menu=( "Reconfigure service" "Setup service" "Destroy \"${PROPER_NAME}\"" "$exitmenu" )
+		fi
+	fi
+
+	echo ""
+	echo -e "\e[4m${PROPER_NAME}\e[0m"
+	if [[ $error ]];then
+		echo -e "\n\e[3m$error\e[0m\n"
+	fi
+	select choice in "${menu[@]}"
+	do
+	    case $choice in
+	        "Configure Site")
+				echo -e "\n\e[3m\xe2\x86\x92 Configure ${PROPER_NAME}\e[0m\n"
+				${SERVICES_DIR}/${SERVICE_NAME}/${SERVICE_NAME}.sh configure
+				say_done
+				sleep 0.2
+				break
+				;;
+	        "Configure service")
+				echo -e "\n\e[3m\xe2\x86\x92 Configure ${PROPER_NAME}\e[0m\n"
+				${SERVICES_DIR}/${SERVICE_NAME}/${SERVICE_NAME}.sh configure
+				sleep 0.2
+				break
+				;;
+	        "Manage Sites")
+				echo -e "\n\e[3m\xe2\x86\x92 Manage sites\e[0m"
+				static_menu
+				sleep 0.2
+				break
+				;;
+	        "Reconfigure service")
+				echo -e "\n\e[3m\xe2\x86\x92 Reconfigure ${PROPER_NAME}\e[0m"
+				${SERVICES_DIR}/${SERVICE_NAME}/${SERVICE_NAME}.sh reconfigure
+				break
+				;;
+			"Setup service")
+				# Set up nginx container if not yet present
+				setup_nginx
+				echo -e "\n\e[3m\xe2\x86\x92 Setup ${PROPER_NAME}\e[0m"
+				${SERVICES_DIR}/${SERVICE_NAME}/${SERVICE_NAME}.sh setup
+				sleep 0.2
+				break
+			;;
+	        "Reinstall service")
+				echo -e "\n\e[3m\xe2\x86\x92 Reinstall ${PROPER_NAME}\e[0m"
+				${SERVICES_DIR}/${SERVICE_NAME}/${SERVICE_NAME}.sh reinstall
+				say_done
+				sleep 0.2
+				break
+			;;
+			"Upgrade Image(s)")
+				echo -e "\n\e[3m\xe2\x86\x92 Upgrade ${PROPER_NAME} images\e[0m"
+				${SERVICES_DIR}/${SERVICE_NAME}/${SERVICE_NAME}.sh upgrade
+				say_done
+				sleep 0.2
+				break
+			;;
+			"Generate self-signed certificate")
+				generate_certificate
+				restart_nginx
+				say_done
+				sleep 0.2
+				break
+			;;
+			"Obtain Let's Encrypt certificate")
+				get_le_cert
+				say_done
+				sleep 0.2
+				exit
+			;;
+			"Renew Let's Encrypt certificate")
+				get_le_cert renew
+				say_done
+				sleep 0.2
+				exit
+			;;
+	        "Restart container(s)")
+				echo -e "\n\e[3m\xe2\x86\x92 Restart ${PROPER_NAME} Containers\e[0m"
+				${SERVICES_DIR}/${SERVICE_NAME}/${SERVICE_NAME}.sh restart_containers
+				say_done
+				sleep 0.2
+				${SERVICES_DIR}/${SERVICE_NAME}/${SERVICE_NAME}.sh
+				break
+			;;
+	        "Start container(s)")
+				echo -e "\n\e[3m\xe2\x86\x92 Start ${PROPER_NAME} Containers\e[0m"
+			${SERVICES_DIR}/${SERVICE_NAME}/${SERVICE_NAME}.sh start_containers
+				say_done
+				sleep 0.2
+				${SERVICES_DIR}/${SERVICE_NAME}/${SERVICE_NAME}.sh
+				break
+			;;
+	        "Stop container(s)")
+				echo -e "\n\e[3m\xe2\x86\x92 Stop ${PROPER_NAME} Containers\e[0m"
+				${SERVICES_DIR}/${SERVICE_NAME}/${SERVICE_NAME}.sh stop_containers
+				say_done
+				sleep 0.2
+				${SERVICES_DIR}/${SERVICE_NAME}/${SERVICE_NAME}.sh
+				break
+			;;
+	        "Destroy \"${PROPER_NAME}\"")
+				echo -e "\n\e[3m\xe2\x86\x92 Destroy ${PROPER_NAME}\e[0m"
+				echo ""
+				echo "The following will be removed:"
+				echo ""
+				for container in ${containers[@]};do
+					[[ $(docker ps -a -q --filter name=^/${container}$) ]]  \
+					&& containers_found+=( $container )
+				done
+				[[ ${containers_found[0]} ]] \
+					&& echo "- ${PROPER_NAME} container(s)"
+
+				for volume in ${volumes[@]};do
+					[[ $(docker volume ls -q --filter name=^${volume}$) ]] \
+						&& volumes_found+=( $volume )
+				done
+				[[ ${volumes_found[0]} ]] && echo "- ${PROPER_NAME} volume(s)"
+
+				[[ -f "${ENV_DIR}"/static/${SERVICE_DOMAIN[0]}.env \
+					|| -f "${ENV_DIR}"/${SERVICE_NAME}.env ]] \
+					&& echo "- ${PROPER_NAME} environment file(s)"
+				[[ -f "${CONF_DIR}"/nginx/conf.d/${SERVICE_DOMAIN[0]}.conf \
+					|| -f "${CONF_DIR}"/nginx/conf.inactive.d/${SERVICE_DOMAIN[0]}.conf ]] \
+					&& echo "- nginx configuration of ${SERVICE_DOMAIN[0]}"
+				[[ ${SERVICE_DOMAIN[0]} && -d "${CONF_DIR}"/nginx/ssl/${SERVICE_DOMAIN[0]} ]] \
+					&& echo "- self-signed certificate for ${SERVICE_DOMAIN[0]}"
+				echo ""
+				prompt_confirm "Continue?" && prompt_confirm "Are you sure?" && . "${SERVICES_DIR}"/${SERVICE_NAME}/${SERVICE_NAME}.sh destroy_service
+				say_done
+				sleep 0.2
+				${BASE_DIR}/dockerbunker.sh
+			;;
+	        "$exitmenu")
+				exit 0
+			;;
+			*)
+				echo "Invalid option."
+				;;
+		esac
+	done
+}
+
+get_le_cert() {
+	if ! [[ $1 == "renew" ]];then
+		echo -e "\n\e[3m\xe2\x86\x92 Obtain Let's Encrypt certificate\e[0m"
+		[[ -z ${LE_EMAIL} ]] && get_le_email
+		if [[ ${STATIC} ]];then
+			sed -i "s/SSL_CHOICE=.*/SSL_CHOICE=le/" "${ENV_DIR}"/static/${SERVICE_DOMAIN[0]}.env
+			sed -i "s/LE_EMAIL=.*/LE_EMAIL="${LE_EMAIL}"/" "${ENV_DIR}"/static/${SERVICE_DOMAIN[0]}.env
+		else
+			sed -i "s/SSL_CHOICE=.*/SSL_CHOICE=le/" "${SERVICE_ENV}"
+			sed -i "s/LE_EMAIL=.*/LE_EMAIL="${LE_EMAIL}"/" "${SERVICE_ENV}"
+		fi
+		elementInArray "${PROPER_NAME}" "${STOPPED_SERVICES[@]}" && "${SERVICES_DIR}"/${SERVICE_NAME}/${SERVICE_NAME}.sh start_containers
+		if [[ ${SERVICE_DOMAIN[0]} && -d "${CONF_DIR}"/nginx/ssl/letsencrypt/live/${SERVICE_DOMAIN[0]} && ! -L "${CONF_DIR}"/nginx/ssl/${SERVICE_DOMAIN[0]}/cert.pem ]];then
+			# Back up self-signed certificate
+			mv "${CONF_DIR}"/nginx/ssl/${SERVICE_DOMAIN[0]}/cert.{pem,pem.backup}
+			mv "${CONF_DIR}"/nginx/ssl/${SERVICE_DOMAIN[0]}/key.{pem,pem.backup}
+			# Symlink letsencrypt certificate
+			ln -sf "/etc/nginx/ssl/letsencrypt/live/${SERVICE_DOMAIN[0]}/fullchain.pem" "${CONF_DIR}"/nginx/ssl/${SERVICE_DOMAIN[0]}/cert.pem
+			ln -sf "/etc/nginx/ssl/letsencrypt/live/${SERVICE_DOMAIN[0]}/privkey.pem" "${CONF_DIR}"/nginx/ssl/${SERVICE_DOMAIN[0]}/key.pem
+			restart_nginx
+		else
+			letsencrypt issue
+		fi
+	else
+		echo -e "\n\e[3m\xe2\x86\x92 Renew Let's Encrypt certificate\e[0m"
+		export prevent_nginx_restart=1
+		[[ -z ${STATIC} ]] && "${SERVICES_DIR}"/${SERVICE_NAME}/${SERVICE_NAME}.sh start_containers
+		bash "${SERVICES_DIR}"/${SERVICE_NAME}/${SERVICE_NAME}.sh letsencrypt issue
+	fi
+}
+
+# start/stop/restart nginx container
+restart_nginx() {
+	echo -en "\n\e[1mRestarting nginx container\e[0m"
+	docker exec -it nginx-dockerbunker nginx -t >/dev/null \
+		&& docker restart ${NGINX_CONTAINER} >/dev/null
+	exit_response
+	if [[ $? == 1 ]];then
+		echo ""
+		docker exec -it nginx-dockerbunker nginx -t
+		echo -e "\n\e[3m\xe2\x86\x92 \e[3m\`nginx -t\` failed. Trying to add missing containers to dockerbunker-network.\e[0m"
+		for container in ${CONTAINERS_IN_DOCKERBUNKER_NETWORK[@]};do
+			connect_containers_to_network ${container}
+		done
+		echo -en "\n\e[1mRestarting nginx container\e[0m"
+		docker exec -it nginx-dockerbunker nginx -t >/dev/null \
+			&& docker restart ${NGINX_CONTAINER} >/dev/null
+		exit_response
+		if [[ $? == 1 ]];then
+			echo ""
+			docker exec -it nginx-dockerbunker nginx -t
+			echo -e "\n\`nginx -t\` failed again. Please resolve issue and try again."
+		fi
+	fi
+}
+start_nginx() {
+	echo -en "\n\e[1mStarting nginx container\e[0m"
+	docker start ${NGINX_CONTAINER} >/dev/null
+	exit_response
+}
+stop_nginx() {
+	echo -en "\n\e[1mStopping nginx container\e[0m"
+	docker stop ${NGINX_CONTAINER} >/dev/null
+	exit_response
+}
+
+# all functions starting/stopping/restarting containers of individual services. This is offered in every service specific menu. 
+deactivate_nginx_conf() {
+	if [[ ${SERVICE_NAME} == "nginx" ]] \
+	|| [[ -f "${CONF_DIR}"/nginx/conf.inactive.d/${SERVICE_DOMAIN[0]}.conf ]] \
+	|| elementInArray "${PROPER_NAME}" "${STOPPED_SERVICES[@]}" \
+	|| [[ ${FUNCNAME[2]} == "destroy_service" ]];then \
+		return
+	fi
+
+	! [[ -f "${CONF_DIR}"/nginx/conf.d/${SERVICE_DOMAIN[0]}.conf ]] \
+		&& [[ -z $reconfigure ]] \
+		&& echo -e "\n\e[31mNginx configuration for ${PROPER_NAME} is not active or missing.\nPlease make sure ${PROPER_NAME} is properly configured.\e[0m\n" \
+		&& return
+
+	! [[ -d "${CONF_DIR}"/nginx/conf.inactive.d ]] \
+		&& mkdir "${CONF_DIR}"/nginx/conf.inactive.d
+	
+	if [[ -f "${CONF_DIR}"/nginx/conf.d/${SERVICE_DOMAIN[0]}.conf ]];then
+	echo -en "\n\e[1mDeactivating nginx configuration\e[0m"
+		[[ -d "${CONF_DIR}"/nginx/conf.d/${SERVICE_DOMAIN[0]} ]] \
+			&& mv "${CONF_DIR}"/nginx/conf.d/${SERVICE_DOMAIN[0]} "${CONF_DIR}"/nginx/conf.inactive.d/
+		mv "${CONF_DIR}"/nginx/conf.d/${SERVICE_DOMAIN[0]}.conf "${CONF_DIR}"/nginx/conf.inactive.d/
+		exit_response
+	fi
+	
+	if ! elementInArray "${PROPER_NAME}" "${STOPPED_SERVICES[@]}";then
+		STOPPED_SERVICES+=( "${PROPER_NAME}" )
+		sed -i '/STOPPED_SERVICES/d' "${ENV_DIR}"/dockerbunker.env
+		declare -p STOPPED_SERVICES >> "${ENV_DIR}"/dockerbunker.env
+	fi
+	
+	[[ -z $prevent_nginx_restart ]] && restart_nginx
+}
+
+activate_nginx_conf() {
+	[[ ${SERVICE_NAME} == "nginx" ]] && return
+	[[ ${FUNCNAME[1]} != "setup" ]] \
+		&& elementInArray "${PROPER_NAME}" "${STOPPED_SERVICES[@]}" \
+		&& ! [[ -f "${CONF_DIR}"/nginx/conf.inactive.d/${SERVICE_DOMAIN[0]}.conf ]] \
+		&& echo -e "\n\e[31mNginx configuration for ${PROPER_NAME} is not inactive or missing. Please make sure ${PROPER_NAME} is properly configured.\e[0m\n" \
+		&& return
+	# activate nginx config
+	[[ -d "${CONF_DIR}"/nginx/conf.inactive.d/${SERVICE_DOMAIN[0]} ]] \
+		&& mv "${CONF_DIR}"/nginx/conf.inactive.d/${SERVICE_DOMAIN[0]} "${CONF_DIR}"/nginx/conf.d/
+	[[ -f "${CONF_DIR}"/nginx/conf.inactive.d/${SERVICE_DOMAIN[0]}.conf ]] \
+		&& mv "${CONF_DIR}"/nginx/conf.inactive.d/${SERVICE_DOMAIN[0]}.conf "${CONF_DIR}"/nginx/conf.d/
+}
+
+start_containers() {
+	RUNNING=$(docker inspect --format="{{.State.Running}}" ${NGINX_CONTAINER} 2> /dev/null)
+	[[ $RUNNING == "false" ]] || [[ -z $RUNNING ]] && bash -c "${SERVICES_DIR}"/nginx/nginx.sh setup
+	echo -e "\n\e[1mStarting containers\e[0m"
+	for container in "${containers[@]}";do
+		[[ $(docker ps -q --filter "status=exited" --filter name=^/${container}$) ]] \
+			&& echo -en "- $container" \
+			&& docker start $container >/dev/null 2>&1 \
+			&& exit_response
+	done
+	[[ $? == 1 ]]  && echo "  Nothing to do. Please configure & setup ${PROPER_NAME} first."
+	remove_from_STOPPED_SERVICES
+	activate_nginx_conf
+	[[ -z $prevent_nginx_restart ]] && restart_nginx
+}
+
+stop_containers() {
+	deactivate_nginx_conf
+	echo -e "\n\e[1mStopping containers\e[0m"
+	for container in "${containers[@]}";do
+		[[ $(docker ps -q --filter name=^/${container}$) ]] \
+			&& echo -en "- $container" \
+			&& docker stop $container >/dev/null 2>&1 \
+			&& exit_response \
+			|| echo "- $container (not running)"
+	done
+}
+
+remove_containers() {
+	for container in ${containers[@]};do
+		[[ $(docker ps -a -q --filter name=^/${container}$) ]]  \
+		&& containers_found+=( $container )
+	done
+	if [[ -z ${containers_found[0]} ]];then
+		echo -e "\n\e[1mRemoving containers\e[0m"
+		for container in "${containers[@]}";do
+			[[ $(docker ps -q --filter "status=exited" --filter name=^/${container}$) || $(docker ps -q --filter "status=restarting" --filter name=^/${container}$) ]] \
+				&& echo -en "- $container" \
+				&& docker rm -f $container >/dev/null 2>&1 \
+				&& exit_response \
+				|| echo "- $container (not found)"
+		done
+	elif [[ ${#containers_found[@]} > 0 ]];then
+		echo -e "\n\e[1mRemoving containers\e[0m"
+		for container in "${containers[@]}";do
+			echo -en "- $container"
+			docker rm -f $container >/dev/null 2>&1
+			exit_response
+		done
+	else
+		return
+	fi
+}
+
+remove_volumes() {
+	if [[ ${volumes[0]} && $remove_volumes ]] || [[ $destroy_all ]];then
+		echo -e "\n\e[1mRemoving volumes\e[0m"
+		for volume in "${volumes[@]}";do
+			[[ $(docker volume ls -q --filter name=^${volume}$) ]] \
+				&& echo -en "- $volume" \
+				&& docker volume rm $volume >/dev/null \
+				&& exit_response \
+				|| echo "- $volume (not found)"
+		done
+	fi
+}
+
+remove_networks() {
+	if [[ ${networks[0]} ]];then
+		echo -e "\n\e[1mRemoving networks\e[0m"
+		for network in "${networks[@]}";do
+			[[ $(docker network ls -q --filter name=^${network}$) ]] \
+				&& echo -en "- $network" \
+				&& docker network rm $network >/dev/null \
+				&& exit_response \
+				|| echo "- $network (not found)"
+		done
+	fi
+}
+
+restart_containers() {
+	echo -e "\n\e[1mRestarting containers\e[0m"
+	[[ $(docker ps -a -q --filter name=^/${NGINX_CONTAINER}$ 2> /dev/null) ]] \
+		|| bash -c "${SERVICES_DIR}"/nginx/nginx.sh setup
+	for container in "${containers[@]}";do
+		[[ $(docker ps -q --filter name=^/${container}$) ]] && ( echo -en "- $container";docker restart $container >/dev/null 2>&1;exit_response )
+	done
+	[[ $? == 1 ]]  && echo "  Nothing to do. Please configure & setup ${PROPER_NAME} first."
+	[[ -z $prevent_nginx_restart ]] && restart_nginx
+}
+
+remove_images() {
+	if [[ ${IMAGES[0]} ]];then
+		prompt_confirm "Remove all images?"
+		if [[ $? == 0 ]];then
+			echo -e "\n\e[1mRemoving images\e[0m"
+			for image in "${IMAGES[@]}";do
+				if ! [[ $(docker container ls | awk '{print $2}' | grep "\<${image}\>") ]];then
+					echo -en "- $image"
+					docker rmi $image >/dev/null
+					exit_response
+				fi
+			done
+		fi
+	fi
+}
+
+remove_service_conf() {
+	[[ -d "${CONF_DIR}/${SERVICE_NAME}" ]] \
+		&& rm -r "${CONF_DIR}/${SERVICE_NAME}/*" \
+
+}
+
+remove_environment_files() {
+	[[ -f "${ENV_DIR}/${SERVICE_NAME}.env" ]] \
+		&& echo -en "\n\e[1mRemoving ${SERVICE_NAME}.env\e[0m" \
+		&& rm "${ENV_DIR}/${SERVICE_NAME}.env" \
+		&& exit_response
+
+	[[ ${SERVICE_SPECIFIC_MX} ]] && [[ -f "${ENV_DIR}/${SERVICE_SPECIFIC_MX}mx.env" ]] \
+		&& rm "${ENV_DIR}/${SERVICE_SPECIFIC_MX}mx.env"
+
+	[[ ${STATIC} && -f "${ENV_DIR}/static/${SERVICE_DOMAIN[0]}.env" ]] \
+		&& echo -en "\n\e[1mRemoving ${SERVICE_DOMAIN[0]}.env\e[0m" \
+		&& rm "${ENV_DIR}/static/${SERVICE_DOMAIN[0]}.env" \
+		&& exit_response
+}
+
+remove_ssl_certificate() {
+	if [[ ${SERVICE_DOMAIN[0]} ]] && [[ -d "${CONF_DIR}"/nginx/ssl/${SERVICE_DOMAIN[0]} ]];then
+		echo -en "\n\e[1mRemoving SSL Certificate\e[0m"
+		rm -r "${CONF_DIR}"/nginx/ssl/${SERVICE_DOMAIN[0]}
+		exit_response
+	fi
+}
+
+destroy_service() {
+	export remove_volumes=1
+	if [[ -z ${STATIC} ]];then
+		disconnect_from_dockerbunker_network
+		stop_containers
+		remove_containers
+		remove_volumes
+		remove_networks
+		remove_images
+
+		echo -en "\n\e[1mRemoving ${PROPER_NAME} from dockerbunker.env\e[0m"
+		remove_from_WEB_SERVICES
+		remove_from_CONFIGURED_SERVICES
+		remove_from_INSTALLED_SERVICES
+		remove_from_STOPPED_SERVICES
+		remove_from_CONTAINERS_IN_DOCKERBUNKER_NETWORK
+	else
+		[[ -d ${STATIC_HOME} ]] \
+			&& prompt_confirm "Remove HTML directory [${STATIC_HOME}]" \
+			&& echo -en "\n\e[1mRemoving ${STATIC_HOME}\e[0m" \
+			&& rm -r ${STATIC_HOME} >/dev/null \
+			&& exit_response
+		echo -en "\n\e[1mRemoving "${SERVICE_DOMAIN[0]}" from dockerbunker.env\e[0m"
+		remove_from_STATIC_SITES
+	fi
+	exit_response
+
+	remove_nginx_conf
+	remove_environment_files
+	remove_service_conf
+	remove_ssl_certificate
+
+	[[ -z $destroy_all ]] \
+		&& [[ -z ${INSTALLED_SERVICES[@]} ]] \
+		&& [[ $(docker ps -q --filter name=^/${NGINX_CONTAINER}) ]] \
+		&& echo -e "\nNo remaining services running.\n" \
+		&& prompt_confirm  "Destroy nginx as well and completely reset dockerbunker?" \
+		&& bash "${SERVICES_DIR}"/nginx/nginx.sh destroy_service \
+		&& return
+
+	[[ -z $prevent_nginx_restart ]] \
+		&& [[ ${SERVICE_NAME} != "nginx" ]] \
+		&& restart_nginx
+}
+
+
+# minimal setup routine. if more is needed add custom setup() in data/services/${SERVICE_NAME}/${SERVICE_NAME}.sh
+setup() {
+	initial_setup_routine
+
+	SUBSTITUTE=( "\${SERVICE_DOMAIN}" )
+	basic_nginx
+
+	docker_run_all
+
+	post_setup_routine
+}
+
+# minimal upgrade routine. if more is needed add custom upgrade() in data/services/${SERVICE_NAME}/${SERVICE_NAME}.sh
+upgrade() {
+	pull_and_compare
+
+	stop_containers
+	remove_containers
+
+	docker_run_all
+
+	delete_old_images
+}
+
+reinstall() {
+	echo ""
+	prompt_confirm "Keep volumes?" && export keep_volumes=1 || export remove_volumes=1
+
+	disconnect_from_dockerbunker_network
+
+	stop_containers
+	remove_containers
+	remove_volumes
+	remove_networks
+
+	export reinstall=1
+	setup
+}
+
+remove_nginx_conf() {
+	if [[ ${SERVICE_DOMAIN[0]} ]];then
+		if [[ -f "${CONF_DIR}"/nginx/conf.d/${SERVICE_DOMAIN[0]}.conf || -f "${CONF_DIR}"/nginx/conf.inactive.d/${SERVICE_DOMAIN[0]}.conf ]];then
+			echo -e "\n\e[1mRemoving nginx configuration\e[0m"
+			echo -n "- ${SERVICE_DOMAIN[0]}.conf"
+			[[ -d "${CONF_DIR}"/nginx/conf.inactive.d/${SERVICE_DOMAIN[0]} ]] \
+				&& rm -r "${CONF_DIR}"/nginx/conf.inactive.d/${SERVICE_DOMAIN[0]} \
+				|| true
+			[[ -d "${CONF_DIR}"/nginx/conf.d/${SERVICE_DOMAIN[0]} ]] \
+				&& rm -r "${CONF_DIR}"/nginx/conf.d/${SERVICE_DOMAIN[0]} \
+				|| true
+			[[ -f "${CONF_DIR}"/nginx/conf.d/${SERVICE_DOMAIN[0]}.conf ]] \
+				&& rm "${CONF_DIR}"/nginx/conf.d/${SERVICE_DOMAIN[0]}.conf \
+				|| true
+			[[ -f "${CONF_DIR}"/nginx/conf.inactive.d/${SERVICE_DOMAIN[0]}.conf ]] \
+				&& rm "${CONF_DIR}"/nginx/conf.inactive.d/${SERVICE_DOMAIN[0]}.conf \
+				|| true
+			exit_response
+		fi
+	fi
+}
+
+disconnect_from_dockerbunker_network() {
+	for container in ${add_to_network[@]};do
+		[[ $container && $(docker ps -q --filter name=^/${container}$) ]] \
+			&&  docker network disconnect ${NETWORK} $container >/dev/null
+	done
+}
+
+reconfigure() {
+	reconfigure=1
+	if [[ $safe_to_keep_volumes_when_reconfiguring ]];then
+		echo ""
+		prompt_confirm "Keep volumes?" || remove_volumes=1
+	else
+		echo ""
+		prompt_confirm "All volumes will be removed. Continue?" && remove_volumes=1 || exit 0
+	fi
+
+	disconnect_from_dockerbunker_network
+
+	stop_containers
+	remove_containers
+	remove_volumes
+	remove_networks
+
+	remove_nginx_conf
+	remove_ssl_certificate
+
+	remove_environment_files
+	remove_service_conf
+
+	[[ $(grep "${PROPER_NAME}" "${ENV_DIR}/dockerbunker.env") ]] && echo -en "\n\e[1mRemoving ${PROPER_NAME} from dockerbunker.env\e[0m"
+	remove_from_WEB_SERVICES
+	remove_from_CONFIGURED_SERVICES
+	remove_from_INSTALLED_SERVICES
+	remove_from_STOPPED_SERVICES
+	remove_from_CONTAINERS_IN_DOCKERBUNKER_NETWORK
+
+	exit_response
+	echo ""
+	configure
+}
+
+# all functions that manipulate all containers
+start_all() {
+	start_nginx
+	for PROPER_NAME in "${STOPPED_SERVICES[@]}";do
+		SERVICE_NAME="$(echo -e "${service,,}" | tr -d '[:space:]')"
+		source "${ENV_DIR}/${SERVICE_NAME}.env"
+		source "${SERVICES_DIR}"/${SERVICE_NAME}/${SERVICE_NAME}.sh start_containers
+	done
+	restart_nginx
+}
+
+restart_all() {
+	for service in "${INSTALLED_SERVICES[@]}";do
+		service="$(echo -e "${service,,}" | tr -d '[:space:]')"
+		source "${SERVICE_ENV}"
+		source "${SERVICES_DIR}"/${service}/${service}.sh restart_containers
+	done
+	restart_nginx
+}
+stop_all() {
+	for service in "${INSTALLED_SERVICES[@]}";do
+		service="$(echo -e "${service,,}" | tr -d '[:space:]')"
+		if ! elementInArray "$service" "${STOPPED_SERVICES[@]}";then
+			source "${SERVICE_ENV}"
+			source "${SERVICES_DIR}"/${service}/${service}.sh stop_containers
+		fi
+	done
+	stop_nginx
+	export prevent_nginx_restart=1
+}
+
+destroy_all() {
+	# destroy_service() is calling restart_nginx, we don't want this happening after each service is destroyed
+	export prevent_nginx_restart=1
+	export destroy_all=1
+	all_services=( "${INSTALLED_SERVICES[@]}" "${CONFIGURED_SERVICES[@]}" "nginx" )
+	echo -e "\nDestroying ${all_services[@]}\n"
+		printf "The following Services will be removed: \
+$(for i in "${all_services[@]}";do \
+if [[ "$i" == ${all_services[-1]} ]];then \
+(printf "\"\e[33m%s\e[0m\" " "$i" )
+			else
+(printf "\"\e[33m%s\e[0m\", " "$i" )
+			fi
+		done) \n"
+	prompt_confirm "Are you sure?"
+	[[ $? == 1 ]] && echo "Exiting..." && exit 0
+	for service in "${all_services[@]}";do
+		SERVICE_NAME="$(echo -e "${service,,}" | tr -d '[:space:]')"
+		echo -e "\n\e[3m\xe2\x86\x92 Destroying $service\e[0m"
+		[[ -f "${SERVICES_DIR}"/${SERVICE_NAME}/${SERVICE_NAME}.sh ]] \
+			&& "${SERVICES_DIR}"/${SERVICE_NAME}/${SERVICE_NAME}.sh destroy_service
+	done
+
+	[[ $(ls -A "${CONF_DIR}"/nginx/conf.inactive.d) ]] \
+		&& rm -r "${CONF_DIR}"/nginx/conf.inactive.d/*
+	[[ $(ls -A "${CONF_DIR}"/nginx/conf.d) ]] \
+		&& rm -r "${CONF_DIR}"/nginx/conf.d/*
+
+	for cert_dir in $(ls "${CONF_DIR}"/nginx/ssl/);do
+		[[ $cert_dir != "letsencrypt" ]] \
+			&& rm -r "${CONF_DIR}"/nginx/ssl/$cert_dir
+	done
+	
+	[[ $(ls -A "${ENV_DIR}"/static) ]] \
+		&& rm "${ENV_DIR}"/static/*
+}
+
+add_ssl_menuentry() {
+	if [[ $SSL_CHOICE == "le" ]] && [[ -d "${CONF_DIR}"/nginx/ssl/letsencrypt/live/${SERVICE_DOMAIN[0]} ]];then
+		# in this case le cert has been obtained previously and everything is as expected
+		insert $1 "Renew Let's Encrypt certificate" $2
+	elif ! [[ -d "${CONF_DIR}"/nginx/ssl/letsencrypt/live/${SERVICE_DOMAIN[0]} ]] && [[ -L "${CONF_DIR}"/nginx/ssl/${SERVICE_DOMAIN[0]}/cert.pem ]];then
+		# in this case neither a self-signed nor a le cert could be found. nginx container will refuse to restart until it can find a certificate in /etc/nginx/ssl/${SERVICE_DOMAIN} - so offer to put one there either via LE or generate new self-signed
+		insert $1 "Generate self-signed certificate" $2
+		insert $1 "Obtain Let's Encrypt certificate" $2
+	elif [[ -f "${CONF_DIR}"/nginx/ssl/${SERVICE_DOMAIN[0]}/cert.pem ]];then
+		# in this case only a self-signed cert is found and a previous cert for the domain might be present in the le directories (if so it will be used and linked to)
+		insert $1 "Obtain Let's Encrypt certificate" $2
+	else
+		# not sure when this should be the case, but if it does happen, bot options are available
+		insert $1 "Generate self-signed certificate" $2
+		insert $1 "Obtain Let's Encrypt certificate" $2
+	fi
+}
+
+static_menu() {
+	[[ -z ${STATIC_SITES[0]} ]] \
+		&& echo -e "\n\e[1mNo existing sites found\e[0m\n" \
+		&& return
+
+	# Display all static sites in a menu
+	
+	# Option menu from directory listing, based on terdon's answer in https://askubuntu.com/a/682146
+	## Collect all sites in the array $staticsites
+	staticsites=( "${BASE_DIR}"/data/env/static/* )
+	# strip path from directory names
+	staticsites=( "${staticsites[@]##*/}" )
+	staticsites=( "${staticsites[@]%.*}" )
+	## Enable extended globbing. This lets us use @(foo|bar) to
+	## match either 'foo' or 'bar'.
+	shopt -s extglob
+	
+	## Start building the string to match against.
+	string="@(${staticsites[0]}"
+	## Add the rest of the site names to the string
+	for((i=1;i<${#staticsites[@]};i++))
+	do
+	    string+="|${staticsites[$i]}"
+	done
+	## Close the parenthesis. $string is now @(site1|site2|...|siteN)
+	string+=")"
+	echo ""
+	
+	## Show the menu. This will list all Static Sites that have an active environment file
+	select static in "${staticsites[@]}" "$returntopreviousmenu"
+	do
+	    case $static in
+	    $string)
+			if [[ -f "${BASE_DIR}"/data/env/static/${static}.env ]];then
+				source "${BASE_DIR}"/data/env/static/${static}.env
+			else
+				echo "No environment file found for $static. Exiting."
+				exit 1
+			fi
+			echo ""
+			static_choices=( "Remove site" "$returntopreviousmenu" )
+			add_ssl_menuentry static_choices 1
+			select static_choice in "${static_choices[@]}"
+				do
+					case $static_choice in
+						"Remove site")
+							echo -e "\n\e[4mRemove site\e[0m"
+							prompt_confirm "Remove $static" && prompt_confirm "Are you sure?" && destroy_service
+							say_done
+							sleep 0.2
+							break
+							;;
+						"Generate self-signed certificate")
+							generate_certificate
+							restart_nginx
+							say_done
+							sleep 0.2
+							break
+						;;
+						"Obtain Let's Encrypt certificate")
+							get_le_cert
+							say_done
+							sleep 0.2
+							break
+						;;
+						"Renew Let's Encrypt certificate")
+							get_le_cert renew
+							say_done
+							sleep 0.2
+							break
+						;;
+						"$returntopreviousmenu")
+							static_menu
+						;;
+						*)
+							echo "Invalid option."
+							;;
+					esac
+				done
+
+			break;
+			;;
+	
+		"$returntopreviousmenu")
+			exec "${SERVICES_DIR}"/statichtmlsite/statichtmlsite.sh options_menu;;
+		*)
+			static=""
+			echo "Please choose a number from 1 to $((${#staticsites[@]}+1))";;
+		esac
+	done
+}
+

+ 396 - 0
data/include/functions/setup_functions.sh

@@ -0,0 +1,396 @@
+# All functions used during setup of a service
+
+docker_build() {
+	if [[ -z $1 ]];then
+		for key in ${!BUILD_IMAGES[@]};do
+			if [[ -f "${BUILD_IMAGES[$key]}/Dockerfile" ]];then
+				if [[ $(docker images | grep "\<${key}\>") ]];then
+					echo ""
+					prompt_confirm "Existing image found for ${key}. Rebuild image?" || return
+				fi
+				echo -e "\n\e[1mBuilding image ${1}\e[0m"
+				docker build -t $key "${BUILD_IMAGES[$key]}"
+			fi
+		done
+	else
+		if [[ -f "${BUILD_IMAGES[$1]}/Dockerfile" ]];then
+				if [[ $(docker images | grep "\<${1}\>") ]];then
+					prompt_confirm "Existing image found for ${1}. Rebuild image?" || return
+				fi
+				echo -e "\n\e[1mBuilding image ${1}\e[0m"
+				docker build -t $1 "${BUILD_IMAGES[$1]}"
+		fi
+	fi
+}
+
+docker_pull() {
+	for image in ${IMAGES[@]};do
+		[[ "$image" != "dockerbunker/${SERVICE_NAME}" ]] \
+			&& echo -e "\n\e[1mPulling $image\e[0m" \
+			&& docker pull $image
+	done
+}
+
+docker_run() {
+	$1
+}
+
+docker_run_all() {
+	echo -e "\n\e[1mStarting up containers\e[0m"
+	for container in "${containers[@]}";do
+		! [[ $(docker ps -q --filter name="^/${container}$") ]] \
+			&& echo -en "- $container" \
+			&& ${container//-/_} \
+			&& exit_response \
+			|| echo "- $container (already running)"
+	done
+
+
+	if elementInArray "${PROPER_NAME}" "${STOPPED_SERVICES[@]}";then
+		remove_from_STOPPED_SERVICES
+	fi
+
+	connect_containers_to_network
+
+	activate_nginx_conf
+
+	restart_nginx
+}
+
+get_current_images_sha256() {
+	# get current images' sha256
+	if [[ -z ${CURRENT_IMAGES_SHA256[@]} ]];then
+		collectImageNamesAndCorrespondingSha256
+		declare -A CURRENT_IMAGES_SHA256
+		for key in "${!IMAGES_AND_SHA256[@]}";do
+			CURRENT_IMAGES_SHA256[$key]+=${IMAGES_AND_SHA256[$key]}
+		done
+	fi
+	declare -p CURRENT_IMAGES_SHA256 >> "${BASE_DIR}"/.image_shas.tmp
+	unset IMAGES_AND_SHA256
+}
+
+pull_and_compare() {
+	[[ -f "${BASE_DIR}"/.image_shas.tmp ]] \
+		&& rm "${BASE_DIR}"/.image_shas.tmp
+
+	get_current_images_sha256
+
+	if [[ ${DOCKER_COMPOSE} ]];then
+		pushd "${SERVICE_HOME}" >/dev/null
+		echo ""
+		echo -e "\e[1mPulling new images\e[0m"
+		echo ""
+		docker-compose pull
+	else
+		docker_build
+		docker_pull
+	fi
+
+	if [[ -f "${BASE_DIR}"/.image_shas.tmp ]];then
+		source "${BASE_DIR}"/.image_shas.tmp
+	else
+		echo -e "\n\e[31mCould not find digests of current images.\nExiting.\e[0m"
+		exit 1
+	fi
+	# compare sha256 and delete old unused images
+	collectImageNamesAndCorrespondingSha256
+	declare -A NEW_IMAGES_SHA256
+	for key in "${!IMAGES_AND_SHA256[@]}";do
+		NEW_IMAGES_SHA256[$key]+=${IMAGES_AND_SHA256[$key]}
+	done
+
+	for key in "${!CURRENT_IMAGES_SHA256[@]}";do
+		if [[ ${CURRENT_IMAGES_SHA256[$key]} != ${NEW_IMAGES_SHA256[$key]} ]];then
+			old_images_to_delete+=( ${CURRENT_IMAGES_SHA256[$key]} )
+		else
+			unchanged_images_to_keep+=( ${CURRENT_IMAGES_SHA256[$key]} )
+		fi
+	done
+		
+	if [[ ${DOCKER_COMPOSE} ]] \
+	&& [[ ${old_images_to_delete[0]} ]];then
+		pushd "${SERVICE_HOME}" >/dev/null
+		echo -e "\n\e[1mTaking down ${PROPER_NAME}\e[0m"
+		docker-compose down
+		echo -e "\n\e[1mBringing ${PROPER_NAME} back up\e[0m"
+		docker-compose up -d
+		connect_containers_to_network
+		popd >/dev/null
+	fi
+
+	[[ ${old_images_to_delete[0]} ]] \
+		&& declare -p old_images_to_delete >> "${BASE_DIR}"/.image_shas.tmp
+	[[ ${unchanged_images_to_keep[0]} ]] \
+		&& declare -p unchanged_images_to_keep >> "${BASE_DIR}"/.image_shas.tmp
+}
+
+delete_old_images() {
+	if [[ -f "${BASE_DIR}"/.image_shas.tmp ]];then
+		source "${BASE_DIR}"/.image_shas.tmp
+	else
+		echo -en "\n\e[31mCould not find digests of current images.\nExiting.\e[0m"
+		return
+	fi
+
+	[[ -z ${old_images_to_delete[0]} ]] \
+		&& echo -e "\n\e[1mImages did not change.\e[0m" \
+		&& rm "${BASE_DIR}"/.image_shas.tmp \
+		&& return
+
+	prompt_confirm "Delete all old images?"
+	if [[ $? == 0 ]];then
+		echo ""
+		for image in "${old_images_to_delete[@]}";do
+				echo -en "\e[1m[DELETING]\e[0m $image"
+				docker rmi $image >/dev/null
+				exit_response
+		done
+		for image in ${unchanged_images_to_keep[@]};do
+			echo -en "\e[1m[KEEPING]\e[0m $image (did not change)"
+		done
+		echo ""
+	fi
+	rm "${BASE_DIR}"/.image_shas.tmp
+}
+setup_nginx() {
+	[[ ! $(docker ps -q --filter name=^/${NGINX_CONTAINER}$) ]] && bash "${SERVICES_DIR}"/nginx/nginx.sh setup
+}
+
+# Build image if necessary, set up nginx container if necessary, create or use existing volumes, create networks if necessary, pull images if necessary
+initial_setup_routine() {
+	[[ ${STATIC} ]] && return
+
+	setup_nginx
+
+	for container in "${containers[@]}";do
+		[[ ( $(docker inspect $container 2> /dev/null) &&  $? == 0 ) ]] && docker rm $container
+	done
+
+	docker_build
+	docker_pull
+	
+	if [[ ${volumes[0]} ]];then
+		echo -e "\n\e[1mCreating volumes\e[0m"
+		for volume in "${volumes[@]}";do
+			[[ ! $(docker volume ls -q --filter name=^${volume}$) ]] \
+				&& echo -en "- $volume" \
+				&& docker volume create $volume >/dev/null \
+				&& exit_response \
+				|| echo "- $volume (already exists)"
+		done
+	fi
+	create_networks
+}
+
+create_networks() {
+	for network in "${networks[@]}";do
+		[[ $(docker network ls -q --filter name=^${network}$) ]] \
+			&& docker network rm $network >/dev/null
+		[[ ! $(docker network ls -q --filter name=^${network}$) ]] \
+			&& docker network create $network >/dev/null
+	done
+}
+
+generate_certificate() {
+	[[ -L "${CONF_DIR}"/nginx/ssl/${SERVICE_DOMAIN[0]}/cert.pem ]] && rm "${CONF_DIR}"/nginx/ssl/${SERVICE_DOMAIN[0]}/cert.pem
+	[[ -L "${CONF_DIR}"/nginx/ssl/${SERVICE_DOMAIN[0]}/key.pem ]] && rm "${CONF_DIR}"/nginx/ssl/${SERVICE_DOMAIN[0]}/key.pem
+	echo -en "\n\e[1mGenerating self-signed certificate for ${SERVICE_DOMAIN[0]}\e[0m"
+	openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout "${CONF_DIR}"/nginx/ssl/${SERVICE_DOMAIN[0]}/key.pem -out "${CONF_DIR}"/nginx/ssl/${SERVICE_DOMAIN[0]}/cert.pem -subj "/C=XY/ST=hidden/L=nowhere/O=${PROPER_NAME}/OU=IT Department/CN=${SERVICE_DOMAIN[0]}" >/dev/null 2>&1
+	exit_response
+}
+# this generates the nginx configuration for the service that is being set up and puts it into data/services/ngix/conf.d
+basic_nginx() {
+	if [[ -z $reinstall ]];then
+		[[ ! -d "${CONF_DIR}"/nginx/ssl/${SERVICE_DOMAIN[0]} ]] && \
+			mkdir -p "${CONF_DIR}"/nginx/ssl/${SERVICE_DOMAIN[0]}
+	
+		if [[ ! -f "${CONF_DIR}"/nginx/ssl/${SERVICE_DOMAIN[0]}/cert.pem ]];then
+			generate_certificate
+		fi
+	
+		[[ ! -d "${CONF_DIR}"/nginx/conf.d ]] && \
+			mkdir -p "${CONF_DIR}"/nginx/conf.d
+		if [[ ! -f "${CONF_DIR}"/nginx/conf.d/${SERVICE_DOMAIN[0]}.conf ]];then
+			cp "${SERVICES_DIR}"/${SERVICE_NAME}/nginx/${SERVICE_NAME}.conf "${SERVICES_DIR}"/${SERVICE_NAME}/nginx/${SERVICE_DOMAIN[0]}.conf
+			for variable in "${SUBSTITUTE[@]}";do
+				subst="\\${variable}"
+				variable=`eval echo "$variable"`
+				sed -i "s@${subst}@${variable}@g;" \
+				"${SERVICES_DIR}"/${SERVICE_NAME}/nginx/${SERVICE_DOMAIN[0]}.conf
+			done
+		fi
+	
+		echo -en "\n\e[1mMoving nginx configuration in place\e[0m"
+		if [[ -f "${SERVICES_DIR}"/${SERVICE_NAME}/nginx/${SERVICE_DOMAIN[0]}.conf ]];then
+			mv "${SERVICES_DIR}"/${SERVICE_NAME}/nginx/${SERVICE_DOMAIN[0]}.conf "${CONF_DIR}"/nginx/conf.d
+			# add basic_auth
+			if [[ ${BASIC_AUTH} == "yes" ]];then
+				[[ ! -d "${CONF_DIR}"/nginx/conf.d/${SERVICE_DOMAIN} ]] && \
+					mkdir -p "${CONF_DIR}"/nginx/conf.d/${SERVICE_DOMAIN}
+				SALT="$(openssl rand 3)"
+				SHA1="$(printf "%s%s" "${HTPASSWD}" "$SALT" | openssl dgst -binary -sha1)"
+				printf "${HTUSER}:{SSHA}%s\n" "$(printf "%s%s" "$SHA1" "$SALT" | base64)" > "${CONF_DIR}"/nginx/conf.d/${SERVICE_DOMAIN}/.htpasswd
+
+				cp "${SERVICES_DIR}"/nginx/basic_auth.conf \
+					"${CONF_DIR}"/nginx/conf.d/${SERVICE_DOMAIN}/basic_auth.conf
+				for variable in "${SUBSTITUTE[@]}";do
+					subst="\\${variable}"
+					variable=`eval echo "$variable"`
+					sed -i "s@${subst}@${variable}@g;" \
+					"${CONF_DIR}"/nginx/conf.d/${SERVICE_DOMAIN}/basic_auth.conf
+				done
+			fi
+			exit_response
+
+		else
+			! [[ -f "${CONF_DIR}"/nginx/conf.d/${SERVICE_DOMAIN[0]}.conf ]] && echo "Nginx configuration file could not be found. Exiting." && exit 1
+		fi
+	fi
+}
+
+# called in docker_run_all if container is found in ${add_to_network}
+connect_containers_to_network() {
+	[[ $1 ]] \
+		&& [[ $(docker ps -q --filter name=^/"${1}"$) ]] \
+		&& ! [[ $(docker network inspect dockerbunker-network | grep $1) ]] \
+		&& docker network connect ${NETWORK} ${1} >/dev/null \
+		&& return
+	for container in ${add_to_network};do
+		[[ $(docker ps -q --filter name=^/"${container}"$) ]] \
+			&& docker network connect ${NETWORK} ${container} >/dev/null
+	done
+}
+
+
+wait_for_db() {
+	if ! docker exec ${FUNCNAME[1]//_/-} mysqladmin ping -h"127.0.0.1" --silent;then
+		while ! docker exec ${FUNCNAME[1]//_/-} mysqladmin ping -h"127.0.0.1" --silent;do
+			sleep 1
+		done
+	fi
+}
+
+post_setup_routine() {
+
+	if [[ $SSL_CHOICE == "le" ]] && [[ ! -d "${CONF_DIR}"/nginx/ssl/letsencrypt/${SERVICE_DOMAIN[0]} ]];then
+		letsencrypt issue
+	fi
+
+	remove_from_CONFIGURED_SERVICES
+
+	! elementInArray "${PROPER_NAME}" "${INSTALLED_SERVICES[@]}" && INSTALLED_SERVICES+=( "${PROPER_NAME}" )
+	for container in ${add_to_network[@]};do
+		! elementInArray "${container}" "${CONTAINERS_IN_DOCKERBUNKER_NETWORK[@]}" && CONTAINERS_IN_DOCKERBUNKER_NETWORK+=( "${container}" )
+	done
+	
+	[[ -f "${ENV_DIR}/dockerbunker.env" ]] && ( sed -i '/CONFIGURED_SERVICES/d' "${ENV_DIR}/dockerbunker.env"; sed -i '/WEB_SERVICES/d' "${ENV_DIR}/dockerbunker.env"; sed -i '/CONTAINERS_IN_DOCKERBUNKER_NETWORK/d' "${ENV_DIR}/dockerbunker.env"; sed -i '/INSTALLED_SERVICES/d' "${ENV_DIR}/dockerbunker.env" )
+	declare -p CONFIGURED_SERVICES >> "${ENV_DIR}/dockerbunker.env"
+	declare -p INSTALLED_SERVICES >> "${ENV_DIR}/dockerbunker.env"
+	declare -p WEB_SERVICES >> "${ENV_DIR}/dockerbunker.env"
+	declare -p CONTAINERS_IN_DOCKERBUNKER_NETWORK >> "${ENV_DIR}/dockerbunker.env" 2>/dev/null
+}
+
+# This function issues a Let's Encrypt certificate if the choice has been made earlier and finally updates the arrays in dockerbunker.env so dockerbunker knows that the service now is installed
+letsencrypt() {
+	echo ""
+	add_domains() {
+		prompt_confirm "Include other domains in certificate beside ${SERVICE_DOMAIN[*]}?"
+		if [[ $? == 0 ]];then
+			unset fqdn_is_valid
+			unset domains
+			while [[ -z $fqdn_is_valid ]];do
+				if [[ $invalid ]];then
+					echo -e "\nPlease enter a valid domain!\n"
+				fi
+				unset invalid
+				read -p "Enter domains, separated by spaces: ${SERVICE_DOMAIN[*]} " -ei "" domains
+				if [[ -z $domains ]];then
+					domains=( ${SERVICE_DOMAIN[*]} )
+					break
+				else
+					domains=( ${SERVICE_DOMAIN[*]} $domains )
+					for i in "${domains[@]}";do
+						# don't check if main domain is valid, otherwise it will complain that the domain already exists because an nginx configuration is already in place
+						[[ $i != ${SERVICE_DOMAIN[0]} ]] && validate_fqdn $i
+					done
+					invalid=1
+				fi
+				invalid=1
+			done
+
+			if [[ ${STATIC} ]];then
+				[[ ${domains[1]} ]] && sed -i "s/server_name.*/server_name ${domains[*]}\;/" "data/conf/nginx/conf.d/${SERVICE_DOMAIN[0]}.conf" && sed -i "s/^SERVICE\_DOMAIN.*/SERVICE\_DOMAIN\=\(\ ${domains[*]}\ \)/" "data/env/static/${SERVICE_DOMAIN[0]}.env"
+			else
+				[[ ${domains[1]} ]] && sed -i "s/server_name.*/server_name ${domains[*]}\;/" "data/conf/nginx/conf.d/${SERVICE_DOMAIN[0]}.conf" && sed -i "s/^SERVICE\_DOMAIN.*/SERVICE\_DOMAIN\=\(\ ${domains[*]}\ \)/" "data/env/${SERVICE_NAME}.env"
+			fi
+			expand="--expand "
+		else
+			domains=( ${SERVICE_DOMAIN[@]} )
+		fi
+	}
+	issue() {
+		[[ -z $1 ]] && ! [[ $(docker ps -q --filter name=^/${SERVICE_NAME}-service-dockerbunker$) ]] && echo "${PROPER_NAME} container not running. Exiting." && exit 1
+			for value in $*;do
+				[[ ( $value == "letsencrypt" || $value == "issue" || $value == "static" ) ]] || domains+=( "$value" )
+			done
+			for domain in ${domains[@]};do
+				[[ ${domain} != ${SERVICE_DOMAIN[0]} ]] && validate_fqdn $domain || fqdn_is_valid=1
+				if [[ $fqdn_is_valid ]];then
+					[[ ! "${le_domains[@]}" =~ $domain ]] && le_domains+=( "-d $domain" )
+				else
+					exit
+				fi
+			done
+		[[ ( "${domains[@]}" =~ ${SERVICE_DOMAIN[0]} && ! "${domains[0]}" =~ "${SERVICE_DOMAIN[0]}" ) ]] && ( echo "Please list ${SERVICE_DOMAIN[0]} first.";exit 1 )
+			[[ "${domains[@]}" =~ ${SERVICE_DOMAIN[0]} ]] || ( echo -e "Please include your chosen ${PROPER_NAME} domain ${SERVICE_DOMAIN[0]}";exit 1 )
+			[[ ! -d "${CONF_DIR}"/nginx/ssl/letsencrypt ]] && mkdir "${CONF_DIR}"/nginx/ssl/letsencrypt
+		[[ ( "${domains[@]}" =~ "${SERVICE_DOMAIN[0]}" && "${domains[0]}" =~ "${SERVICE_DOMAIN[0]}" ) ]] \
+			&& echo "" \
+			&& docker run --rm -it --name=certbot \
+				--network ${NETWORK} \
+				-v "${CONF_DIR}"/nginx/ssl/letsencrypt:/etc/letsencrypt \
+				-v "${BASE_DIR}"/data/web:/var/www/html:rw \
+				certbot/certbot \
+				certonly --noninteractive \
+				--webroot -w /var/www/html \
+				${le_domains[@]} \
+				--email ${LE_EMAIL} ${expand}\
+				--agree-tos
+		if [[ $? == 0 ]];then
+			if ! [[ -L "data/conf/nginx/ssl/${SERVICE_DOMAIN[0]}/cert.pem" ]];then
+				echo -en "\n\e[1mBacking up self-signed certificate\e[0m"
+				mv "${CONF_DIR}"/nginx/ssl/${SERVICE_DOMAIN[0]}/cert.{pem,pem.backup} && \
+					mv "${CONF_DIR}"/nginx/ssl/${SERVICE_DOMAIN[0]}/key.{pem,pem.backup} && exit_response || exit_response
+				echo -en "\n\e[1mSymlinking letsencrypt certificate\e[0m"
+				ln -sf "/etc/nginx/ssl/letsencrypt/live/${SERVICE_DOMAIN[0]}/fullchain.pem" "${CONF_DIR}"/nginx/ssl/${SERVICE_DOMAIN[0]}/cert.pem && \
+					ln -sf "/etc/nginx/ssl/letsencrypt/live/${SERVICE_DOMAIN[0]}/privkey.pem" "${CONF_DIR}"/nginx/ssl/${SERVICE_DOMAIN[0]}/key.pem && exit_response || exit_response
+			fi
+			restart_nginx
+		fi
+	}
+
+	renew() {
+		docker run --rm -it --name=certbot \
+		--network ${NETWORK} \
+		-v "${CONF_DIR}"/nginx/ssl/letsencrypt:/etc/letsencrypt \
+		-v "${BASE_DIR}"/data/web:/var/www/html:rw \
+		certbot/certbot \
+		renew
+	
+		restart_nginx
+	}
+
+	echo -e "\e[1mObtain certificate from Let's Encrypt\e[0m"
+	if [[ ( "$1" == "issue" ) ]] || [[ ( "$1" == "letsencrypt" && "$2" == "issue" ) ]];then
+			add_domains
+			issue ${domains[@]}
+	elif [[ ( "$1" == "letsencrypt" && "$2" == "renew" ) ]] || [[ "$1" == "renew" ]];then
+		renew
+	else
+		echo "Usage: issue example.org www.example.org | renew"
+		exit 0
+	fi
+
+}
+

+ 56 - 0
data/include/init.sh

@@ -0,0 +1,56 @@
+# Find base dir
+while true;do ls | grep -q dockerbunker.sh;if [[ $? == 0 ]];then BASE_DIR=$PWD;break;else cd ../;fi;done
+
+# On first run, generate the basic environment file. This file will collect and hold all information regarding dockerbunker.
+# It keeps track of which web-apps are configured, installed, which services' containers are stopped etc.
+init_dockerbunker() {
+	! [[ -d ${BASE_DIR}/data/conf/nginx/conf.d ]] && mkdir -p ${BASE_DIR}/data/conf/nginx/conf.d
+	! [[ -d ${BASE_DIR}/data/env/static ]] && mkdir ${BASE_DIR}/data/env/static
+
+	if [[ ! -f "${BASE_DIR}/data/env/dockerbunker.env" ]];then
+		[[ ! -d "${BASE_DIR}/data/env/" ]] && mkdir -p "${BASE_DIR}/data/env/"
+		cat <<-EOF >> "${BASE_DIR}/data/env/dockerbunker.env"
+			BASE_DIR="${BASE_DIR}"
+			SERVICES_DIR="${BASE_DIR}/data/services"
+			SERVICE_DIR="\${SERVICES_DIR}/\${SERVICE_NAME}"
+			CONF_DIR="${BASE_DIR}/data/conf"
+			ENV_DIR="${BASE_DIR}/data/env"
+			SERVICE_ENV="\${ENV_DIR}/\${SERVICE_NAME}.env"
+			DOCKERFILES="${BASE_DIR}/data/Dockerfiles"
+			CONTAINERS=\${SERVICE_DIR}/containers.sh
+
+			LE_EMAIL=
+
+			NETWORK=dockerbunker-network
+			NGINX_CONTAINER=( "nginx-dockerbunker" )
+
+			declare -A WEB_SERVICES=()
+			declare -a CONFIGURED_SERVICES=()
+			declare -a INSTALLED_SERVICES=()
+			declare -a STATIC_SITES=()
+		EOF
+	fi
+}
+
+[[ -f "${BASE_DIR}"/data/env/dockerbunker.env ]] && source "${BASE_DIR}"/data/env/dockerbunker.env
+
+for file in "${BASE_DIR}"/data/include/functions/*; do
+  source $file
+done
+
+if [[ ${STATIC} && ${SERVICE_DOMAIN[0]} ]];then
+	[[ -f "${ENV_DIR}"/static/${SERVICE_DOMAIN[0]}.env ]] \
+	&& source "${ENV_DIR}"/static/${SERVICE_DOMAIN[0]}.env
+else
+	if [[ ${SERVICE_NAME} ]];then
+		[[ -f ${SERVICE_ENV} ]] \
+			&& source ${SERVICE_ENV}
+		[[ -f ${CONTAINERS} ]] \
+			&& source ${CONTAINERS}
+		[[ -f "${ENV_DIR}"/mx.env ]] \
+			&& source "${ENV_DIR}"/mx.env
+		[[ -f "${ENV_DIR}"/${SERVICE_NAME}_mx.env ]] \
+			&& source "${ENV_DIR}"/${SERVICE_NAME}_mx.env
+	fi
+fi
+

+ 75 - 0
data/services/bitbucket/bitbucket.sh

@@ -0,0 +1,75 @@
+#!/usr/bin/env bash
+
+while true;do ls | grep -q dockerbunker.sh;if [[ $? == 0 ]];then BASE_DIR=$PWD;break;else cd ../;fi;done
+
+PROPER_NAME="Bitbucket"
+SERVICE_NAME="$(echo -e "${PROPER_NAME,,}" | tr -d '[:space:]')"
+PROMPT_SSL=1
+
+declare -a environment=( "data/env/dockerbunker.env" "data/include/init.sh" )
+
+for env in "${environment[@]}";do
+	[[ -f "${BASE_DIR}"/$env ]] && source "${BASE_DIR}"/$env
+done
+
+declare -A WEB_SERVICES
+declare -a containers=( "bitbucket-postgres-dockerbunker" "${SERVICE_NAME}-service-dockerbunker" )
+declare -a add_to_network=( "bitbucket-service-dockerbunker"  )
+declare -a networks=( )
+declare -A IMAGES=( [service]="dockerbunker/${SERVICE_NAME}" )
+declare -a volumes=( "${SERVICE_NAME}-data-vol-1" "${SERVICE_NAME}-db-vol-1" )
+declare -a networks=( "dockerbunker-bitbucket" )
+declare -A IMAGES=( [postgres]="postgres" [service]="atlassian/bitbucket-server:5" )
+
+[[ -z $1 ]] && options_menu
+
+configure() {
+	pre_configure_routine
+
+	echo -e "# \e[4mBitbucket Settings\e[0m"
+
+	set_domain
+
+	# avoid tr illegal byte sequence in macOS when generating random strings
+	if [[ $OSTYPE =~ "darwin" ]];then
+		if [[ $LC_ALL ]];then
+			oldLC_ALL=$LC_ALL
+			export LC_ALL=C
+		else
+			export LC_ALL=C
+		fi
+	fi
+	cat <<-EOF >> "${SERVICE_ENV}"
+	# ------------------------------
+	# General Settings
+	# ------------------------------
+	
+	SERVER_SECURE=true
+	SERVER_SCHEME=https
+	SERVER_PROXY_PORT=443
+	SERVER_PROXY_NAME=${SERVICE_DOMAIN}
+	SERVICE_DOMAIN=${SERVICE_DOMAIN}
+
+	# ------------------------------
+	# SQL database configuration
+	# ------------------------------
+
+	DBUSER=bitbucket
+	
+	# Please use long, random alphanumeric strings (A-Za-z0-9)
+	DBPASS=$(tr -dc 'a-zA-Z0-9' < /dev/urandom | head -c 28)
+	EOF
+
+	if [[ $OSTYPE =~ "darwin" ]];then
+		[[ $oldLC_ALL ]] && export LC_ALL=$oldLC_ALL || unset LC_ALL
+	fi
+
+	post_configure_routine
+}
+
+if [[ $1 == "letsencrypt" ]];then
+	$1 $*
+else
+	$1
+fi
+

+ 22 - 0
data/services/bitbucket/containers.sh

@@ -0,0 +1,22 @@
+bitbucket_postgres_dockerbunker() {
+	docker run -d \
+		--name=${FUNCNAME[0]//_/-} \
+		--restart=always \
+		--network=dockerbunker-${SERVICE_NAME} --net-alias=db \
+		-v ${SERVICE_NAME}-db-vol-1:/var/lib/postgresql/data \
+		--env-file=${SERVICE_ENV} \
+		-e POSTGRES_PASSWORD=${DBPASS} \
+		-e POSTGRES_USER=${DBUSER} \
+	${IMAGES[postgres]}
+}
+
+bitbucket_service_dockerbunker() {
+	docker run -d \
+		--name=${FUNCNAME[0]//_/-} \
+		--restart=always \
+		--network=${NETWORK} \
+		--network=dockerbunker-${SERVICE_NAME} \
+		-v ${SERVICE_NAME}-data-vol-1:/var/atlassian/application-data/bitbucket \
+		--env-file=${SERVICE_ENV} \
+	${IMAGES[service]}
+}

+ 44 - 0
data/services/bitbucket/nginx/bitbucket.conf

@@ -0,0 +1,44 @@
+upstream bitbucket {
+	server bitbucket-service-dockerbunker:7990;
+}
+
+server {
+	listen 80;
+	server_name ${SERVICE_DOMAIN};
+	return 301 https://$host$request_uri;
+	add_header X-Content-Type-Options "nosniff" always;
+	add_header X-XSS-Protection "1; mode=block" always;
+	add_header X-Frame-Options "DENY" always;
+	add_header Referrer-Policy "strict-origin" always;
+	add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
+	server_tokens off;
+}
+
+server {
+	listen 443;
+	server_name ${SERVICE_DOMAIN};
+	ssl on;
+	ssl_certificate /etc/nginx/ssl/${SERVICE_DOMAIN}/cert.pem;
+	ssl_certificate_key /etc/nginx/ssl/${SERVICE_DOMAIN}/key.pem;
+	include /etc/nginx/includes/ssl.conf;
+
+	add_header X-Content-Type-Options "nosniff" always;
+	add_header X-XSS-Protection "1; mode=block" always;
+	add_header X-Frame-Options "DENY" always;
+	add_header Referrer-Policy "strict-origin" always;
+	add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
+	server_tokens off;
+
+	include /etc/nginx/includes/gzip.conf;
+
+	location / {
+		proxy_pass http://bitbucket/;
+	}
+
+	location ~ /.well-known {
+		allow all;
+		root /var/www/html;
+	}
+}
+
+

+ 10 - 0
data/services/cryptpad/containers.sh

@@ -0,0 +1,10 @@
+cryptpad_service_dockerbunker() {
+	docker run -d \
+		--name=${FUNCNAME[0]//_/-} \
+		--restart=always \
+		--network ${NETWORK} \
+		--env-file ${SERVICE_ENV} \
+		-v ${SERVICE_NAME}-data-vol-2:/cryptpad/datastore \
+		-v ${SERVICE_NAME}-data-vol-1:/cryptpad/customize \
+	${IMAGES[service]} >/dev/null
+}

+ 48 - 0
data/services/cryptpad/cryptpad.sh

@@ -0,0 +1,48 @@
+#!/usr/bin/env bash
+
+while true;do ls | grep -q dockerbunker.sh;if [[ $? == 0 ]];then BASE_DIR=$PWD;break;else cd ../;fi;done
+
+PROPER_NAME="Cryptpad"
+SERVICE_NAME="$(echo -e "${PROPER_NAME,,}" | tr -d '[:space:]')"
+PROMPT_SSL=1
+
+declare -a environment=( "data/env/dockerbunker.env" "data/include/init.sh" )
+
+for env in "${environment[@]}";do
+	[[ -f "${BASE_DIR}"/$env ]] && source "${BASE_DIR}"/$env
+done
+
+declare -A WEB_SERVICES
+declare -a containers=( "${SERVICE_NAME}-service-dockerbunker" )
+declare -a add_to_network=( "${SERVICE_NAME}-service-dockerbunker" )
+declare -A IMAGES=( [service]="dockerbunker/${SERVICE_NAME}" )
+declare -A BUILD_IMAGES=( [dockerbunker/${SERVICE_NAME}]="${DOCKERFILES}/${SERVICE_NAME}" )
+declare -a volumes=( "${SERVICE_NAME}-data-vol-1" "${SERVICE_NAME}-data-vol-2" )
+declare -a networks=( )
+
+[[ -z $1 ]] && options_menu
+
+configure() {
+	pre_configure_routine
+
+	echo -e "# \e[4mCryptpad Settings\e[0m"
+
+	set_domain
+	
+	cat <<-EOF >> ${SERVICE_ENV}
+	PROPER_NAME=${PROPER_NAME}
+	SERVICE_NAME=${SERVICE_NAME}
+	SSL_CHOICE=${SSL_CHOICE}
+	LE_EMAIL=${LE_EMAIL}
+
+	SERVICE_DOMAIN=${SERVICE_DOMAIN}
+	EOF
+
+	post_configure_routine
+}
+
+if [[ $1 == "letsencrypt" ]];then
+	$1 $*
+else
+	$1
+fi

+ 68 - 0
data/services/cryptpad/nginx/cryptpad.conf

@@ -0,0 +1,68 @@
+##
+# You should look at the following URL's in order to grasp a solid understanding
+# of Nginx configuration files in order to fully unleash the power of Nginx.
+# http://wiki.nginx.org/Pitfalls
+# http://wiki.nginx.org/QuickStart
+# http://wiki.nginx.org/Configuration
+#
+# Generally, you will want to move this file somewhere, and start with a clean
+# file but keep this around for reference. Or just disable in sites-enabled.
+#
+# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples.
+##
+
+# Default server configuration
+#
+
+map $http_upgrade $connection_upgrade {
+        default upgrade;
+        '' close;
+}
+
+upstream cryptpad {
+ server cryptpad-service-dockerbunker:3000;
+}
+
+server {
+    listen 80;
+	server_name ${SERVICE_DOMAIN};
+    return 301 https://$host$request_uri;
+}
+
+server {
+    listen 443;
+	server_name ${SERVICE_DOMAIN};
+    ssl on;
+	ssl_certificate /etc/nginx/ssl/${SERVICE_DOMAIN}/cert.pem;
+	ssl_certificate_key /etc/nginx/ssl/${SERVICE_DOMAIN}/key.pem;
+	include /etc/nginx/includes/ssl.conf;
+
+    add_header Strict-Transport-Security "max-age=15768000; includeSubDomains";
+	add_header X-Frame-Options SAMEORIGIN;
+	add_header X-Content-Type-Options nosniff;
+
+	include /etc/nginx/includes/gzip.conf;
+
+    location / {
+        proxy_pass http://cryptpad;
+		proxy_set_header  Host              $http_host;   # required for docker client's sake
+		proxy_set_header  X-Real-IP         $remote_addr; # pass on real client's IP
+		proxy_set_header  X-Forwarded-For   $proxy_add_x_forwarded_for;
+		proxy_set_header  X-Forwarded-Proto $scheme;
+		proxy_read_timeout                  900;
+    }
+
+  location /cryptpad_websocket {
+        proxy_pass http://cryptpad/cryptpad_websocket;
+        proxy_http_version 1.1;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection $connection_upgrade;
+  }
+
+	location ~ /.well-known {
+        allow all;
+		root /var/www/html;
+	}
+}
+
+

+ 13 - 0
data/services/cs50ide/containers.sh

@@ -0,0 +1,13 @@
+cs50ide_service_dockerbunker() {
+	docker run -d \
+		--name=${FUNCNAME[0]//_/-} \
+		--restart=always \
+		--network ${NETWORK} \
+		--env-file ${SERVICE_ENV}\
+		--cap-add=SYS_PTRACE \
+		-p 5050:5050 \
+		-e "OFFLINE_PORT=5050" \
+		-e "OFFLINE_IP=127.0.0.1" \
+		-v ${SERVICE_NAME}-data-vol-1:/home/ubuntu/workspace \
+	${IMAGES[service]} >/dev/null
+}

+ 94 - 0
data/services/cs50ide/cs50ide.sh

@@ -0,0 +1,94 @@
+#!/usr/bin/env bash
+
+while true;do ls | grep -q dockerbunker.sh;if [[ $? == 0 ]];then BASE_DIR=$PWD;break;else cd ../;fi;done
+
+PROPER_NAME="CS50 IDE"
+SERVICE_NAME="$(echo -e "${PROPER_NAME,,}" | tr -d '[:space:]')"
+PROMPT_SSL=1
+safe_to_keep_volumes_when_reconfiguring=1
+
+declare -a environment=( "data/env/dockerbunker.env" "data/include/init.sh" )
+
+for env in "${environment[@]}";do
+	[[ -f "${BASE_DIR}/$env" ]] && source "${BASE_DIR}/$env"
+done
+
+declare -A WEB_SERVICES
+declare -a containers=( "${SERVICE_NAME}-service-dockerbunker" )
+declare -a volumes=( "${SERVICE_NAME}-data-vol-1")
+declare -a add_to_network=( "${SERVICE_NAME}-service-dockerbunker" )
+declare -a networks=( )
+declare -A IMAGES=( [service]="cs50/ide50-offline" )
+
+[[ -z $1 ]] && options_menu
+
+configure() {
+	pre_configure_routine
+	
+	echo -e "# \e[4mCS50 IDE Settings\e[0m"
+
+	set_domain
+
+	echo -e "\nCS50 IDE should not be run anywhere but locally. If you want to run it on a server that is accessible by anyone, it is recommended to protect https://${SERVICE_DOMAIN} with basic authentication.\n"
+
+	prompt_confirm "Use Basic Authentication to limit access to https://${SERVICE_DOMAIN}?" choice
+	if [[ $? == 0 ]];then
+		BASIC_AUTH="yes"
+		if [[ -z $HTUSER ]]; then
+			while [[ -z ${HTUSER} ]];do
+				read -p "Basic Auth Username: " -ei "" HTUSER
+			done
+		else
+			read -p "Basic Auth Username: " -ei "${HTUSER}" HTUSER
+		fi
+		unset HTPASSWD
+		while [[ "${#HTPASSWD}" -le 6 || "$HTPASSWD" != *[A-Z]* || "$HTPASSWD" != *[a-z]* || "$HTPASSWD" != *[0-9]* ]];do
+			if [ $VALIDATE ];then
+				echo -e "\n\e[31m  Password does not meet requirements\e[0m"
+			fi
+				stty_orig=$(stty -g)
+				stty -echo
+		  		read -p " $(printf "\n   \e[4mPassword requirements\e[0m\n   Minimum Length 6, Uppercase, Lowercase, Integer\n\n   Enter Password:") " -ei "" HTPASSWD
+				stty "$stty_orig"
+				echo ""
+			VALIDATE=1
+		done
+		unset VALIDATE
+		echo ""
+	else
+		AUTH_SWITCH="#"
+		BASIC_AUTH="no"
+	fi
+
+	cat <<-EOF >> "${SERVICE_ENV}"
+	PROPER_NAME="${PROPER_NAME}"
+	SERVICE_NAME=${SERVICE_NAME}
+	LE_EMAIL=${LE_EMAIL}
+	SSL_CHOICE=${SSL_CHOICE}
+	BASIC_AUTH=${BASIC_AUTH}
+	AUTH_SWITCH=${AUTH_SWITCH}
+	HTUSER=${HTUSER}
+	HTPASSWD=${HTPASSWD}
+
+	SERVICE_DOMAIN=${SERVICE_DOMAIN}
+	EOF
+
+	post_configure_routine
+}
+
+setup() {
+	initial_setup_routine
+
+	SUBSTITUTE=( "\${SERVICE_DOMAIN}" "\${AUTH_SWITCH}" )
+	basic_nginx
+
+	docker_run_all
+
+	post_setup_routine
+}
+
+if [[ $1 == "letsencrypt" ]];then
+	$1 $*
+else
+	$1
+fi

+ 2 - 0
data/services/cs50ide/nginx/basic_auth.conf

@@ -0,0 +1,2 @@
+	auth_basic            "Restricted Area";
+	auth_basic_user_file  /etc/nginx/conf.d/${WORDPRESS_DOMAIN}/.htpasswd;

+ 56 - 0
data/services/cs50ide/nginx/cs50ide.conf

@@ -0,0 +1,56 @@
+##
+# You should look at the following URL's in order to grasp a solid understanding
+# of Nginx configuration files in order to fully unleash the power of Nginx.
+# http://wiki.nginx.org/Pitfalls
+# http://wiki.nginx.org/QuickStart
+# http://wiki.nginx.org/Configuration
+#
+# Generally, you will want to move this file somewhere, and start with a clean
+# file but keep this around for reference. Or just disable in sites-enabled.
+#
+# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples.
+##
+
+# Default server configuration
+#
+upstream cs50ide {
+	server cs50ide-service-dockerbunker:5050;
+}
+
+server {
+	listen 80;
+	server_name ${SERVICE_DOMAIN};
+	return 301 https://$host$request_uri;
+}
+
+server {
+	listen 443;
+	server_name ${SERVICE_DOMAIN};
+	ssl on;
+	ssl_certificate /etc/nginx/ssl/${SERVICE_DOMAIN}/cert.pem;
+	ssl_certificate_key /etc/nginx/ssl/${SERVICE_DOMAIN}/key.pem;
+	include /etc/nginx/includes/ssl.conf;
+
+	add_header Strict-Transport-Security "max-age=15768000; includeSubDomains";
+	add_header X-Frame-Options DENY;
+	add_header X-Content-Type-Options nosniff;
+
+	include /etc/nginx/includes/gzip.conf;
+
+	location / {
+		proxy_pass				http://cs50ide/;
+${AUTH_SWITCH}	include /etc/nginx/conf.d/${SERVICE_DOMAIN}/basic_auth.conf;
+		proxy_set_header		Host				$http_host;   # required for docker client's sake
+		proxy_set_header		X-Real-IP			$remote_addr; # pass on real client's IP
+		proxy_set_header		X-Forwarded-For		$proxy_add_x_forwarded_for;
+		proxy_set_header		X-Forwarded-Proto	$scheme;
+		proxy_read_timeout		900;
+	}
+
+	location ~ /.well-known {
+		allow all;
+		root /var/www/html;
+	}
+}
+
+

+ 9 - 0
data/services/dillinger/containers.sh

@@ -0,0 +1,9 @@
+dillinger_service_dockerbunker() {
+	docker run -d \
+		--name=${FUNCNAME[0]//_/-} \
+		--restart=always \
+		--network ${NETWORK} \
+		--env-file "${SERVICE_ENV}" \
+		-v ${SERVICE_NAME}-data-vol-1:/dillinger/data \
+	${IMAGES[service]} >/dev/null
+}

+ 54 - 0
data/services/dillinger/dillinger.sh

@@ -0,0 +1,54 @@
+#!/usr/bin/env bash
+
+while true;do ls | grep -q dockerbunker.sh;if [[ $? == 0 ]];then BASE_DIR=$PWD;break;else cd ../;fi;done
+
+PROPER_NAME="Dillinger"
+SERVICE_NAME="$(echo -e "${PROPER_NAME,,}" | tr -d '[:space:]')"
+PROMPT_SSL=1
+safe_to_keep_volumes_when_reconfiguring=1
+
+declare -a environment=( "data/env/dockerbunker.env" "data/include/init.sh" )
+
+for env in "${environment[@]}";do
+	[[ -f "${BASE_DIR}"/$env ]] && source "${BASE_DIR}"/$env
+done
+
+declare -A WEB_SERVICES
+declare -a containers=( "${SERVICE_NAME}-service-dockerbunker" )
+declare -a volumes=( "${SERVICE_NAME}-data-vol-1")
+declare -a add_to_network=( "${SERVICE_NAME}-service-dockerbunker" )
+declare -a networks=( )
+declare -A IMAGES=( [service]="dockerbunker/dillinger" )
+declare -A BUILD_IMAGES=( [dockerbunker/${SERVICE_NAME}]="${DOCKERFILES}/${SERVICE_NAME}" )
+
+[[ -z $1 ]] && options_menu
+
+configure() {
+	pre_configure_routine
+	
+	! [[ -d "${BASE_DIR}"/data/Dockerfiles/dillinger ]] \
+	&& echo -n "Cloning Dillinger repository into data/Dockerfiles/dillinger" \
+	&& git submodule add -f https://github.com/joemccann/dillinger.git data/Dockerfiles/dillinger >/dev/null \
+	&& exit_response
+	
+	echo -e "# \e[4mDillinger Settings\e[0m"
+
+	set_domain
+	
+	cat <<-EOF >> "${SERVICE_ENV}"
+	PROPER_NAME=${PROPER_NAME}
+	SERVICE_NAME=${SERVICE_NAME}
+	SSL_CHOICE=${SSL_CHOICE}
+	LE_EMAIL=${LE_EMAIL}
+
+	SERVICE_DOMAIN=${SERVICE_DOMAIN}
+	EOF
+
+	post_configure_routine
+}
+
+if [[ $1 == "letsencrypt" ]];then
+	$1 $*
+else
+	$1
+fi

+ 55 - 0
data/services/dillinger/nginx/dillinger.conf

@@ -0,0 +1,55 @@
+##
+# You should look at the following URL's in order to grasp a solid understanding
+# of Nginx configuration files in order to fully unleash the power of Nginx.
+# http://wiki.nginx.org/Pitfalls
+# http://wiki.nginx.org/QuickStart
+# http://wiki.nginx.org/Configuration
+#
+# Generally, you will want to move this file somewhere, and start with a clean
+# file but keep this around for reference. Or just disable in sites-enabled.
+#
+# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples.
+##
+
+# Default server configuration
+#
+upstream dillinger {
+ server dillinger-service-dockerbunker:8080;
+}
+
+server {
+    listen 80;
+	server_name ${SERVICE_DOMAIN};
+    return 301 https://$host$request_uri;
+}
+
+server {
+    listen 443;
+	server_name ${SERVICE_DOMAIN};
+    ssl on;
+	ssl_certificate /etc/nginx/ssl/${SERVICE_DOMAIN}/cert.pem;
+	ssl_certificate_key /etc/nginx/ssl/${SERVICE_DOMAIN}/key.pem;
+	include /etc/nginx/includes/ssl.conf;
+
+    add_header Strict-Transport-Security "max-age=15768000; includeSubDomains";
+	add_header X-Frame-Options DENY;
+	add_header X-Content-Type-Options nosniff;
+
+	include /etc/nginx/includes/gzip.conf;
+
+    location / {
+        proxy_pass http://dillinger/;
+		proxy_set_header  Host              $http_host;   # required for docker client's sake
+		proxy_set_header  X-Real-IP         $remote_addr; # pass on real client's IP
+		proxy_set_header  X-Forwarded-For   $proxy_add_x_forwarded_for;
+		proxy_set_header  X-Forwarded-Proto $scheme;
+		proxy_read_timeout                  900;
+    }
+
+	location ~ /.well-known {
+        allow all;
+		root /var/www/html;
+	}
+}
+
+

+ 10 - 0
data/services/ghost/containers.sh

@@ -0,0 +1,10 @@
+ghost_service_dockerbunker() {
+	docker run -d \
+		--name=${FUNCNAME[0]//_/-} \
+		--restart=always \
+		--env-file ${SERVICE_ENV} \
+		--env NODE_ENV=production \
+		--env url=https://${SERVICE_DOMAIN[0]} \
+		-v ${SERVICE_NAME}-data-vol-1:/var/lib/ghost/content \
+		${IMAGES[service]} >/dev/null
+}

+ 61 - 0
data/services/ghost/ghost.sh

@@ -0,0 +1,61 @@
+#!/usr/bin/env bash
+
+while true;do ls | grep -q dockerbunker.sh;if [[ $? == 0 ]];then BASE_DIR=$PWD;break;else cd ../;fi;done
+
+PROPER_NAME="Ghost"
+SERVICE_NAME="$(echo -e "${PROPER_NAME,,}" | tr -d '[:space:]')"
+PROMPT_SSL=1
+safe_to_keep_volumes_when_reconfiguring=1
+
+declare -a environment=( "data/env/dockerbunker.env" "data/include/init.sh" )
+
+for env in "${environment[@]}";do
+	[[ -f "${BASE_DIR}"/$env ]] && source "${BASE_DIR}"/$env
+done
+
+declare -A WEB_SERVICES
+declare -a containers=( "${SERVICE_NAME}-service-dockerbunker" )
+declare -a volumes=( "${SERVICE_NAME}-data-vol-1" )
+declare -a add_to_network=( "${SERVICE_NAME}-service-dockerbunker" )
+declare -a networks=( )
+declare -A IMAGES=( [service]="ghost:1-alpine" )
+
+[[ -z $1 ]] && options_menu
+
+configure() {
+	pre_configure_routine
+
+	echo -e "# \e[4mGhost Settings\e[0m"
+
+	set_domain
+
+	cat <<-EOF >> "${SERVICE_ENV}"
+	PROPER_NAME=${PROPER_NAME}
+	SERVICE_NAME=${SERVICE_NAME}
+	SSL_CHOICE=${SSL_CHOICE}
+	LE_EMAIL=${LE_EMAIL}
+	
+	# ------------------------------
+	# General Settings
+	# ------------------------------
+	
+	SERVICE_DOMAIN=${SERVICE_DOMAIN}
+	
+	# ------------------------------
+	# Ghost Settings
+	# ------------------------------
+	
+	url=https://${SERVICE_DOMAIN}
+	NODE_ENV=production
+	EOF
+
+	post_configure_routine
+}
+
+if [[ $1 == "letsencrypt" ]];then
+	$1 $*
+else
+	$1
+fi
+
+

+ 39 - 0
data/services/ghost/nginx/ghost.conf

@@ -0,0 +1,39 @@
+upstream ghost {
+ server ghost-service-dockerbunker:2368;
+}
+
+server {
+    listen 80;
+	server_name ${SERVICE_DOMAIN};
+    return 301 https://$host$request_uri;
+}
+
+server {
+    listen 443;
+	server_name ${SERVICE_DOMAIN};
+    ssl on;
+	ssl_certificate /etc/nginx/ssl/${SERVICE_DOMAIN}/cert.pem;
+	ssl_certificate_key /etc/nginx/ssl/${SERVICE_DOMAIN}/key.pem;
+	include /etc/nginx/includes/ssl.conf;
+
+    add_header Strict-Transport-Security "max-age=15768000; includeSubDomains";
+	add_header X-Frame-Options DENY;
+	add_header X-Content-Type-Options nosniff;
+
+	include /etc/nginx/includes/gzip.conf;
+
+    location / {
+        proxy_pass http://ghost/;
+		proxy_set_header  Host              $http_host;   # required for docker client's sake
+		proxy_set_header  X-Real-IP         $remote_addr; # pass on real client's IP
+		proxy_set_header  X-Forwarded-For   $proxy_add_x_forwarded_for;
+		proxy_set_header  X-Forwarded-Proto $scheme;
+		proxy_read_timeout                  900;
+    }
+
+	location ~ /.well-known {
+        allow all;
+		root /var/www/html;
+	}
+}
+

+ 27 - 0
data/services/gitea/containers.sh

@@ -0,0 +1,27 @@
+gitea_db_dockerbunker() {
+	docker run -d \
+		--name=${FUNCNAME[0]//_/-} \
+		--restart=always \
+		--network dockerbunker-${SERVICE_NAME} --net-alias=db \
+		--env-file="${SERVICE_ENV}" \
+		--env MYSQL_ROOT_PASSWORD=${GITEA_DBROOT} \
+		--env MYSQL_DATABASE=${GITEA_DBNAME} \
+		--env MYSQL_USER=${GITEA_DBUSER} \
+		--env MYSQL_PASSWORD=${GITEA_DBPASS} \
+		-v gitea-db-vol-1:/var/lib/mysql \
+		-v "${SERVICES_DIR}"/${SERVICE_NAME}/mysql/:/etc/mysql/conf.d/:ro \
+		--health-cmd="mysqladmin ping --host localhost --silent" --health-interval=10s --health-retries=5 --health-timeout=30s \
+	${IMAGES[db]} >/dev/null
+}
+
+gitea_service_dockerbunker() {
+	docker run -d \
+		--name=${FUNCNAME[0]//_/-} \
+		--restart=always \
+		--network dockerbunker-${SERVICE_NAME} \
+		--env-file "${SERVICE_ENV}" \
+		--env-file "${ENV_DIR}"/${SERVICE_SPECIFIC_MX}mx.env \
+		--env RUN_CROND=true \
+		-v gitea-data-vol-1:/data \
+	${IMAGES[service]} >/dev/null
+}

+ 238 - 0
data/services/gitea/gitea.sh

@@ -0,0 +1,238 @@
+#!/usr/bin/env bash
+
+while true;do ls | grep -q dockerbunker.sh;if [[ $? == 0 ]];then BASE_DIR=$PWD;break;else cd ../;fi;done
+
+PROPER_NAME="Gitea"
+SERVICE_NAME="$(echo -e "${PROPER_NAME,,}" | tr -d '[:space:]')"
+PROMPT_SSL=1
+
+declare -a environment=( "data/env/dockerbunker.env" "data/include/init.sh" )
+
+for env in "${environment[@]}";do
+	[[ -f "${BASE_DIR}"/$env ]] && source "${BASE_DIR}"/$env
+done
+
+declare -A WEB_SERVICES
+declare -a containers=( "${SERVICE_NAME}-db-dockerbunker" "${SERVICE_NAME}-service-dockerbunker" )
+declare -a add_to_network=( "${SERVICE_NAME}-service-dockerbunker" )
+declare -a volumes=( "${SERVICE_NAME}-db-vol-1" "${SERVICE_NAME}-data-vol-1" )
+declare -a networks=( "dockerbunker-gitea" )
+declare -A IMAGES=( [db]="mariadb:10.2" [service]="gitea/gitea:1.4" )
+declare -A BUILD_IMAGES=( [dockerbunker/${SERVICE_NAME}]="${DOCKERFILES}/${SERVICE_NAME}" )
+
+[[ -z $1 ]] && options_menu
+
+configure() {
+	pre_configure_routine
+
+	echo -e "# \e[4mGitea Settings\e[0m"
+
+	set_domain
+
+	if [ "$GITEA_APP_NAME" ]; then
+	  read -p "Gitea Application Name: " -ei "$GITEA_APP_NAME" GITEA_APP_NAME
+	else
+	  read -p "Gitea Application Name: " -ei "Gitea Go Git Service" GITEA_APP_NAME
+	fi
+	
+	echo "# User Settings"
+	echo ""
+
+	unset GITEA_ADMIN
+	if [ "$GITEA_ADMIN" ]; then
+	  read -p "Gitea Admin User: " -ei "$GITEA_ADMIN" GITEA_ADMIN
+	else
+		while [[ -z $GITEA_ADMIN || $GITEA_ADMIN == "admin" ]];do
+			read -p "Gitea Admin User: " -ei "$GITEA_ADMIN" GITEA_ADMIN
+			[[ ${GITEA_ADMIN} == "admin" ]] && echo -e "\n\e[31mAdmin account setting is invalid: name is reserved [name: admin]\e[0m\n"
+		done
+	fi
+	
+	if [ "$GITEA_ADMIN_EMAIL" ]; then
+	  read -p "Gitea Admin E-Mail: " -ei "$GITEA_ADMIN_EMAIL" GITEA_ADMIN_EMAIL
+	else
+	  read -p "Gitea Admin E-Mail: " GITEA_ADMIN_EMAIL
+	fi
+	
+	unset GITEA_ADMIN_PASSWORD
+	while [[ "${#GITEA_ADMIN_PASSWORD}" -le 6 || "$GITEA_ADMIN_PASSWORD" != *[A-Z]* || "$GITEA_ADMIN_PASSWORD" != *[a-z]* || "$GITEA_ADMIN_PASSWORD" != *[0-9]* ]];do
+		if [ $VALIDATE ];then
+			echo -e "\n\e[31m  Password does not meet requirements\e[0m"
+		fi
+			stty_orig=$(stty -g)
+			stty -echo
+	  		read -p " $(printf "\n   \e[4mPassword requirements\e[0m\n   Minimum Length 6,Uppercase, Lowercase, Integer\n\n   Enter Password:") " -ei "" GITEA_ADMIN_PASSWORD
+			stty "$stty_orig"
+			echo ""
+		VALIDATE=1
+	done
+	unset VALIDATE
+	echo ""
+
+	prompt_confirm "Enable Registration Confirmation?" && GITEA_REGISTER_CONFIRM="on" || GITEA_REGISTER_CONFIRM="off"
+	
+	prompt_confirm "Enable Mail Notification?" && GITEA_MAIL_NOTIFY="on" || GITEA_MAIL_NOTIFY="off"
+
+	echo ""	
+	echo "# Server & Other  Service Settings"
+	echo ""
+	
+	prompt_confirm "Enable Offline Mode?" && GITEA_OFFLINE_MODE="on" || GITEA_OFFLINE_MODE="off"
+	
+	prompt_confirm "Disable Gravatar Service?" && GITEA_DISABLE_GRAVATAR="on" || GITEA_DISABLE_GRAVATAR="off"
+	
+	prompt_confirm "Enable Federated Avatars Lookup?" && GITEA_ENABLE_FEDERATED_AVATAR="on" || GITEA_ENABLE_FEDERATED_AVATAR="on"
+
+	prompt_confirm "Disable Self Registration?" && GITEA_DISABLE_REGISTRATION="off" || GITEA_DISABLE_REGISTRATION="on"
+	
+	prompt_confirm "Enable Captcha?" && GITEA_ENABLE_CAPTCHA="on" || GITEA_ENABLE_CAPTCHA="off"
+	
+	prompt_confirm "Enable Require Sign In To View Pages?" && GITEA_REQUIRE_SIGN_IN_VIEW="on" || GITEA_REQUIRE_SIGN_IN_VIEW="off"
+	
+	configure_mx
+
+	# avoid tr illegal byte sequence in macOS when generating random strings
+	if [[ $OSTYPE =~ "darwin" ]];then
+		if [[ $LC_ALL ]];then
+			oldLC_ALL=$LC_ALL
+			export LC_ALL=C
+		else
+			export LC_ALL=C
+		fi
+	fi
+	cat <<-EOF >> "${SERVICE_ENV}"
+	PROPER_NAME=${PROPER_NAME}
+	SERVICE_NAME=${SERVICE_NAME}
+	SSL_CHOICE=${SSL_CHOICE}
+	LE_EMAIL=${LE_EMAIL}
+
+	# ------------------------------
+	# SQL database configuration
+	# ------------------------------
+
+	GITEA_DBNAME=gitea
+	GITEA_DBUSER=gitea
+	
+	# Please use long, random alphanumeric strings (A-Za-z0-9)
+	GITEA_DBPASS=$(tr -dc 'a-zA-Z0-9' < /dev/urandom | head -c 28)
+	GITEA_DBROOT=$(tr -dc 'a-zA-Z0-9' < /dev/urandom | head -c 28)
+	
+	# ------------------------------
+	# General Settings
+	# ------------------------------
+	
+	SERVICE_DOMAIN=${SERVICE_DOMAIN}
+	GITEA_APP_NAME="${GITEA_APP_NAME}"
+	
+	GITEA_REGISTER_CONFIRM=${GITEA_REGISTER_CONFIRM}
+	GITEA_MAIL_NOTIFY=${GITEA_MAIL_NOTIFY}
+	
+	# ------------------------------
+	# User configuration
+	# ------------------------------
+	
+	GITEA_ADMIN=${GITEA_ADMIN}
+	GITEA_ADMIN_EMAIL=${GITEA_ADMIN_EMAIL}
+	GITEA_ADMIN_PASSWORD="${GITEA_ADMIN_PASSWORD}"
+	
+	# ------------------------------
+	# Server & Other  Service Settings
+	# ------------------------------
+	
+	GITEA_OFFLINE_MODE=${GITEA_OFFLINE_MODE}
+	GITEA_DISABLE_GRAVATAR=${GITEA_DISABLE_GRAVATAR}
+	GITEA_ENABLE_FEDERATED_AVATAR=${GITEA_ENABLE_FEDERATED_AVATAR}
+	GITEA_DISABLE_REGISTRATION=${GITEA_DISABLE_REGISTRATION}
+	GITEA_ENABLE_CAPTCHA=${GITEA_ENABLE_CAPTCHA}
+	GITEA_REQUIRE_SIGN_IN_VIEW=${GITEA_REQUIRE_SIGN_IN_VIEW}
+	
+	## ------------------------------
+	SERVICE_SPECIFIC_MX=${SERVICE_SPECIFIC_MX}
+	EOF
+	if [[ $OSTYPE =~ "darwin" ]];then
+		[[ $oldLC_ALL ]] && export LC_ALL=$oldLC_ALL || unset LC_ALL
+	fi
+
+	post_configure_routine
+}
+setup() {
+	initial_setup_routine
+
+	SUBSTITUTE=( "\${SERVICE_DOMAIN}" )
+	basic_nginx
+
+	docker_run_all
+
+	# wait for gitea db to be available
+	if ! docker exec gitea-db-dockerbunker mysqladmin ping -h"127.0.0.1" --silent;then
+		echo -e "\n\e[3mWaiting for gitea-db-dockerbunker to be ready...\e[0m"
+		while ! docker exec gitea-db-dockerbunker mysqladmin ping -h"127.0.0.1" --silent;do
+			sleep 3
+		done
+	fi
+
+	echo -e "\n\e[3mWaiting for https://${SERVICE_DOMAIN}/install to be accessible ...\e[0m"
+	# Check if installation page is accessible and then install gitea
+	while [[ $response != 200 ]];do
+		response=$(curl -kso /dev/null -w '%{http_code}' https://${SERVICE_DOMAIN}/install)
+		sleep 1
+		count+=1
+		[[ $count > 30 ]] && echo "\e[31mfailed\n\nCannot reach https://${SERVICE_DOMAIN}/install. Exiting\e[0m\n" && exit 1
+	done
+	[[ $response == 200 ]] && true
+
+	echo -en "\n\e[1mInstalling Gitea via cURL ...\e[0m"
+curl -k \
+	-H 'Origin: null' -H 'Accept-Encoding: gzip, deflate' \
+	-H 'Accept-Language: en-US,en;q=0.8,en-US;q=0.6,en;q=0.4' \
+	-H 'Upgrade-Insecure-Requests: 1' \
+	-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36' \
+	-H 'Content-Type: application/x-www-form-urlencoded' \
+	-H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8' \
+	-H 'Cache-Control: max-age=0' \
+	-H 'Cookie: _ga=GA1.1.221947403.1432575239; lang=en_US; i_like_gogits=4e8b96e2a97347e0; _csrf=0NHae8OjPYKJ6xiplpZcsUhkwu86MTQ0MTExMTA2MDEyMzQ4ODk0Ng%3D%3D' \
+	-H 'Connection: keep-alive' \
+	-d "db_type=MySQL\
+&db_host=db%3A3306\
+&db_user=${GITEA_DBUSER}\
+&db_passwd=${GITEA_DBPASS}\
+&db_name=${GITEA_DBNAME}\
+&ssl_mode=disable\
+&db_path=data/gitea.db\
+&app_name=${GITEA_APP_NAME}\
+&repo_root_path=/data/git/gitea-repositories\
+&run_user=git\
+&domain=${SERVICE_DOMAIN}\
+&ssh_port=22\
+&http_port=3000\
+&app_url=https://${SERVICE_DOMAIN}\
+&log_root_path=/app/gitea/log\
+&smtp_host=${MX_DOMAIN}:587\
+&smtp_from=\
+&smtp_user=${MX_EMAIL}\
+&smtp_passwd=${MX_PASSWORD}\
+&admin_name=${GITEA_ADMIN}\
+&admin_passwd=${GITEA_ADMIN_PASSWORD}\
+&admin_confirm_passwd=${GITEA_ADMIN_PASSWORD}\
+&admin_email=${GITEA_ADMIN_EMAIL}\
+&register_confirm=${GITEA_REGISTER_CONFIRM}\
+&mail_notify=${GITEA_MAIL_NOTIFY}\
+&offline_mode=${GITEA_OFFLINE_MODE}\
+&disable_gravatar=${GITEA_DISABLE_GRAVATAR}\
+&enable_federated_avatar=${GITEA_ENABLE_FEDERATED_AVATAR}\
+&disable_registration=${GITEA_DISABLE_REGISTRATION}\
+&enable_captcha=${GITEA_ENABLE_CAPTCHA}\
+&require_sign_in_view=${GITEA_REQUIRE_SIGN_IN_VIEW}" --compressed \
+-X POST "https://${SERVICE_DOMAIN}/install"
+
+	response=$(curl -kso /dev/null -w '%{http_code}' https://${SERVICE_DOMAIN})
+	[[ $response == 200 ]] && echo -e " \e[32m\xE2\x9C\x94\e[0m" || echo  -e " \e[31mfailed\e[0m"
+
+	post_setup_routine
+}
+
+if [[ $1 == "letsencrypt" ]];then
+	$1 $*
+else
+	$1
+fi

+ 14 - 0
data/services/gitea/mysql/my.cnf

@@ -0,0 +1,14 @@
+[mysqld]
+character-set-client-handshake = FALSE
+character-set-server           = utf8mb4
+collation-server               = utf8mb4_unicode_ci
+innodb_file_per_table          = TRUE
+innodb_file_format             = barracuda
+innodb_large_prefix            = TRUE
+#sql_mode=IGNORE_SPACE,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
+
+[client]
+default-character-set = utf8mb4
+
+[mysql]
+default-character-set = utf8mb4

+ 48 - 0
data/services/gitea/nginx/gitea.conf

@@ -0,0 +1,48 @@
+##
+# You should look at the following URL's in order to grasp a solid understanding
+# of Nginx configuration files in order to fully unleash the power of Nginx.
+# http://wiki.nginx.org/Pitfalls
+# http://wiki.nginx.org/QuickStart
+# http://wiki.nginx.org/Configuration
+#
+# Generally, you will want to move this file somewhere, and start with a clean
+# file but keep this around for reference. Or just disable in sites-enabled.
+#
+# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples.
+##
+
+# Default server configuration
+#
+upstream gitea {
+	server gitea-service-dockerbunker:3000;
+}
+
+server {
+	listen 80;
+	server_name ${SERVICE_DOMAIN};
+	return 301 https://$host$request_uri;
+}
+
+server {
+	listen 443;
+	server_name ${SERVICE_DOMAIN};
+	ssl on;
+	ssl_certificate /etc/nginx/ssl/${SERVICE_DOMAIN}/cert.pem;
+	ssl_certificate_key /etc/nginx/ssl/${SERVICE_DOMAIN}/key.pem;
+	include /etc/nginx/includes/ssl.conf;
+
+	add_header Strict-Transport-Security "max-age=15768000; includeSubDomains";
+	add_header X-Frame-Options DENY;
+	add_header X-Content-Type-Options nosniff;
+
+	include /etc/nginx/includes/gzip.conf;
+
+	location / {
+		proxy_pass http://gitea/;
+	}
+
+	location ~ /.well-known {
+		allow all;
+		root /var/www/html;
+	}
+}

+ 19 - 0
data/services/gitlabce/containers.sh

@@ -0,0 +1,19 @@
+gitlabce_service_dockerbunker() {
+	echo -en "Starting up '${PROPER_NAME}' container"
+	docker run -d \
+		--hostname ${SERVICE_DOMAIN} \
+		--sysctl net.core.somaxconn=1024 \
+		--ulimit sigpending=62793 \
+		--ulimit nproc=131072 \
+		--ulimit nofile=60000 \
+		--ulimit core=0 \
+		--name=${FUNCNAME[0]//_/-} \
+		--restart=always \
+		--network ${NETWORK} \
+		--env-file ${SERVICE_ENV} \
+		-v ${SERVICE_NAME}-conf-vol-1:/etc/gitlab \
+		-v ${SERVICE_NAME}-data-vol-1:/etc/opt/gitlab \
+		-v ${SERVICE_NAME}-log-vol-1:/var/log/gitlab \
+	${IMAGES[service]} >/dev/null
+	exit_response
+}

+ 61 - 0
data/services/gitlabce/gitlabce.sh

@@ -0,0 +1,61 @@
+#!/usr/bin/env bash
+
+while true;do ls | grep -q dockerbunker.sh;if [[ $? == 0 ]];then BASE_DIR=$PWD;break;else cd ../;fi;done
+
+PROPER_NAME="Gitlab CE"
+SERVICE_NAME="$(echo -e "${PROPER_NAME,,}" | tr -d '[:space:]')"
+PROMPT_SSL=1
+
+declare -a environment=( "data/env/dockerbunker.env" "data/include/init.sh" )
+
+for env in "${environment[@]}";do
+	[[ -f "${BASE_DIR}"/$env ]] && source "${BASE_DIR}"/$env
+done
+
+declare -A WEB_SERVICES
+declare -a containers=( "${SERVICE_NAME}-service-dockerbunker" )
+declare -a volumes=( "${SERVICE_NAME}-data-vol-1" "${SERVICE_NAME}-conf-vol-1" "${SERVICE_NAME}-log-vol-1" "${SERVICE_NAME}-log-vol-2" )
+declare -a add_to_network=( "${SERVICE_NAME}-service-dockerbunker" )
+declare -a networks=( )
+declare -A IMAGES=( [service]="gitlab/gitlab-ce:latest" )
+
+[[ -z $1 ]] && options_menu
+
+configure() {
+	pre_configure_routine
+
+	echo -e "# \e[4m'${PROPER_NAME}' Settings\e[0m"
+
+	set_domain
+	
+	cat <<-EOF >> "${SERVICE_ENV}"
+	PROPER_NAME="${PROPER_NAME}"
+	SERVICE_NAME=${SERVICE_NAME}
+	SSL_CHOICE=${SSL_CHOICE}
+	LE_EMAIL=${LE_EMAIL}
+
+	SERVICE_DOMAIN=${SERVICE_DOMAIN}
+	EOF
+
+	post_configure_routine
+}
+setup() {
+	initial_setup_routine
+
+	SUBSTITUTE=( "\${SERVICE_DOMAIN}" )
+	basic_nginx
+
+	#/proc/sys/fs/file-max #this is shared with the host:
+	GITLAB_FILEMAX=1000000
+	[[ $(cat /proc/sys/fs/file-max) -lt ${GITLAB_FILEMAX} ]] && echo $GITLAB_FILEMAX > /proc/sys/fs/file-max
+
+	docker_run_all
+
+	post_setup_routine
+}
+
+if [[ $1 == "letsencrypt" ]];then
+	$1 $*
+else
+	$1
+fi

+ 50 - 0
data/services/gitlabce/nginx/gitlabce.conf

@@ -0,0 +1,50 @@
+upstream gitlabce {
+	server gitlabce-service-dockerbunker:80;
+}
+
+server {
+	listen 80;
+	server_name ${SERVICE_DOMAIN};
+	return 301 https://$host$request_uri;
+	add_header X-Content-Type-Options "nosniff" always;
+	add_header X-XSS-Protection "1; mode=block" always;
+	add_header X-Frame-Options "DENY" always;
+	add_header Referrer-Policy "strict-origin" always;
+	add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
+	server_tokens off;
+}
+
+server {
+	listen 443;
+	server_name ${SERVICE_DOMAIN};
+	ssl on;
+	ssl_certificate /etc/nginx/ssl/${SERVICE_DOMAIN}/cert.pem;
+	ssl_certificate_key /etc/nginx/ssl/${SERVICE_DOMAIN}/key.pem;
+	include /etc/nginx/includes/ssl.conf;
+
+	add_header X-Content-Type-Options "nosniff" always;
+	add_header X-XSS-Protection "1; mode=block" always;
+	add_header X-Frame-Options "DENY" always;
+	add_header Referrer-Policy "strict-origin" always;
+	add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
+	server_tokens off;
+
+	include /etc/nginx/includes/gzip.conf;
+
+	location / {
+		proxy_pass http://gitlabce/;
+		proxy_set_header  Host			  $http_host;   # required for docker client's sake
+		proxy_set_header  X-Real-IP		 $remote_addr; # pass on real client's IP
+		proxy_set_header  X-Forwarded-For   $proxy_add_x_forwarded_for;
+		proxy_set_header  X-Forwarded-Proto $scheme;
+		proxy_read_timeout				  900;
+	}
+
+	location ~ /.well-known {
+		allow all;
+		root /var/www/html;
+	}
+}
+
+
+

+ 27 - 0
data/services/gogs/containers.sh

@@ -0,0 +1,27 @@
+gogs_db_dockerbunker() {
+	docker run -d \
+		--name=${FUNCNAME[0]//_/-} \
+		--restart=always \
+		--network dockerbunker-${SERVICE_NAME} --net-alias=db \
+		--env-file="${SERVICE_ENV}" \
+		--env MYSQL_ROOT_PASSWORD=${GOGS_DBROOT} \
+		--env MYSQL_DATABASE=${GOGS_DBNAME} \
+		--env MYSQL_USER=${GOGS_DBUSER} \
+		--env MYSQL_PASSWORD=${GOGS_DBPASS} \
+		-v gogs-db-vol-1:/var/lib/mysql \
+		-v "${SERVICES_DIR}"/${SERVICE_NAME}/mysql/:/etc/mysql/conf.d/:ro \
+		--health-cmd="mysqladmin ping --host localhost --silent" --health-interval=10s --health-retries=5 --health-timeout=30s \
+	${IMAGES[db]} >/dev/null
+}
+
+gogs_service_dockerbunker() {
+	docker run -d \
+		--name=${FUNCNAME[0]//_/-} \
+		--restart=always \
+		--network dockerbunker-${SERVICE_NAME} \
+		--env-file "${SERVICE_ENV}" \
+		--env-file "${ENV_DIR}"/${SERVICE_SPECIFIC_MX}mx.env \
+		--env RUN_CROND=true \
+		-v gogs-data-vol-1:/data \
+	${IMAGES[service]} >/dev/null
+}

+ 237 - 0
data/services/gogs/gogs.sh

@@ -0,0 +1,237 @@
+#!/usr/bin/env bash
+
+while true;do ls | grep -q dockerbunker.sh;if [[ $? == 0 ]];then BASE_DIR=$PWD;break;else cd ../;fi;done
+
+PROPER_NAME="Gogs"
+SERVICE_NAME="$(echo -e "${PROPER_NAME,,}" | tr -d '[:space:]')"
+PROMPT_SSL=1
+
+declare -a environment=( "data/env/dockerbunker.env" "data/include/init.sh" )
+
+for env in "${environment[@]}";do
+	[[ -f "${BASE_DIR}"/$env ]] && source "${BASE_DIR}"/$env
+done
+
+declare -A WEB_SERVICES
+declare -a containers=( "${SERVICE_NAME}-db-dockerbunker" "${SERVICE_NAME}-service-dockerbunker" )
+declare -a add_to_network=( "${SERVICE_NAME}-service-dockerbunker" )
+declare -a volumes=( "${SERVICE_NAME}-db-vol-1" "${SERVICE_NAME}-data-vol-1" )
+declare -a networks=( "dockerbunker-gogs" )
+declare -A IMAGES=( [db]="mariadb:10.2" [service]="gogs/gogs" )
+declare -A BUILD_IMAGES=( [dockerbunker/${SERVICE_NAME}]="${DOCKERFILES}/${SERVICE_NAME}" )
+
+[[ -z $1 ]] && options_menu
+
+configure() {
+	pre_configure_routine
+
+	echo -e "# \e[4mGogs Settings\e[0m"
+
+	set_domain
+
+	if [ "$GOGS_APP_NAME" ]; then
+	  read -p "Gogs Application Name: " -ei "$GOGS_APP_NAME" GOGS_APP_NAME
+	else
+	  read -p "Gogs Application Name: " -ei "Gogs Go Git Service" GOGS_APP_NAME
+	fi
+	
+	echo "# User Settings"
+	echo ""
+
+	unset GOGS_ADMIN
+	if [ "$GOGS_ADMIN" ]; then
+	  read -p "Gogs Admin User: " -ei "$GOGS_ADMIN" GOGS_ADMIN
+	else
+		while [[ -z $GOGS_ADMIN || $GOGS_ADMIN == "admin" ]];do
+			read -p "Gogs Admin User: " -ei "$GOGS_ADMIN" GOGS_ADMIN
+			[[ ${GOGS_ADMIN} == "admin" ]] && echo -e "\n\e[31mAdmin account setting is invalid: name is reserved [name: admin]\e[0m\n"
+		done
+	fi
+	
+	if [ "$GOGS_ADMIN_EMAIL" ]; then
+	  read -p "Gogs Admin E-Mail: " -ei "$GOGS_ADMIN_EMAIL" GOGS_ADMIN_EMAIL
+	else
+	  read -p "Gogs Admin E-Mail: " GOGS_ADMIN_EMAIL
+	fi
+	
+	unset GOGS_ADMIN_PASSWORD
+	while [[ "${#GOGS_ADMIN_PASSWORD}" -le 6 || "$GOGS_ADMIN_PASSWORD" != *[A-Z]* || "$GOGS_ADMIN_PASSWORD" != *[a-z]* || "$GOGS_ADMIN_PASSWORD" != *[0-9]* ]];do
+		if [ $VALIDATE ];then
+			echo -e "\n\e[31m  Password does not meet requirements\e[0m"
+		fi
+			stty_orig=$(stty -g)
+			stty -echo
+	  		read -p " $(printf "\n   \e[4mPassword requirements\e[0m\n   Minimum Length 6,Uppercase, Lowercase, Integer\n\n   Enter Password:") " -ei "" GOGS_ADMIN_PASSWORD
+			stty "$stty_orig"
+			echo ""
+		VALIDATE=1
+	done
+	unset VALIDATE
+	echo ""
+
+	prompt_confirm "Enable Registration Confirmation?" && GOGS_REGISTER_CONFIRM="on" || GOGS_REGISTER_CONFIRM="off"
+	
+	prompt_confirm "Enable Mail Notification?" && GOGS_MAIL_NOTIFY="on" || GOGS_MAIL_NOTIFY="off"
+
+	echo ""	
+	echo "# Server & Other  Service Settings"
+	echo ""
+	
+	prompt_confirm "Enable Offline Mode?" && GOGS_OFFLINE_MODE="on" || GOGS_OFFLINE_MODE="off"
+	
+	prompt_confirm "Disable Gravatar Service?" && GOGS_DISABLE_GRAVATAR="on" || GOGS_DISABLE_GRAVATAR="off"
+	
+	prompt_confirm "Enable Federated Avatars Lookup?" && GOGS_ENABLE_FEDERATED_AVATAR="on" || GOGS_ENABLE_FEDERATED_AVATAR="on"
+
+	prompt_confirm "Disable Self Registration?" && GOGS_DISABLE_REGISTRATION="off" || GOGS_DISABLE_REGISTRATION="on"
+	
+	prompt_confirm "Enable Captcha?" && GOGS_ENABLE_CAPTCHA="on" || GOGS_ENABLE_CAPTCHA="off"
+	
+	prompt_confirm "Enable Require Sign In To View Pages?" && GOGS_REQUIRE_SIGN_IN_VIEW="on" || GOGS_REQUIRE_SIGN_IN_VIEW="off"
+	
+	configure_mx
+
+	# avoid tr illegal byte sequence in macOS when generating random strings
+	if [[ $OSTYPE =~ "darwin" ]];then
+		if [[ $LC_ALL ]];then
+			oldLC_ALL=$LC_ALL
+			export LC_ALL=C
+		else
+			export LC_ALL=C
+		fi
+	fi
+	cat <<-EOF >> "${SERVICE_ENV}"
+	PROPER_NAME=${PROPER_NAME}
+	SERVICE_NAME=${SERVICE_NAME}
+	SSL_CHOICE=${SSL_CHOICE}
+	LE_EMAIL=${LE_EMAIL}
+
+	# ------------------------------
+	# SQL database configuration
+	# ------------------------------
+
+	GOGS_DBNAME=gogs
+	GOGS_DBUSER=gogs
+	
+	# Please use long, random alphanumeric strings (A-Za-z0-9)
+	GOGS_DBPASS=$(tr -dc 'a-zA-Z0-9' < /dev/urandom | head -c 28)
+	GOGS_DBROOT=$(tr -dc 'a-zA-Z0-9' < /dev/urandom | head -c 28)
+	
+	# ------------------------------
+	# General Settings
+	# ------------------------------
+	
+	SERVICE_DOMAIN=${SERVICE_DOMAIN}
+	GOGS_APP_NAME="${GOGS_APP_NAME}"
+	
+	GOGS_REGISTER_CONFIRM=${GOGS_REGISTER_CONFIRM}
+	GOGS_MAIL_NOTIFY=${GOGS_MAIL_NOTIFY}
+	
+	# ------------------------------
+	# User configuration
+	# ------------------------------
+	
+	GOGS_ADMIN=${GOGS_ADMIN}
+	GOGS_ADMIN_EMAIL=${GOGS_ADMIN_EMAIL}
+	GOGS_ADMIN_PASSWORD="${GOGS_ADMIN_PASSWORD}"
+	
+	# ------------------------------
+	# Server & Other  Service Settings
+	# ------------------------------
+	
+	GOGS_OFFLINE_MODE=${GOGS_OFFLINE_MODE}
+	GOGS_DISABLE_GRAVATAR=${GOGS_DISABLE_GRAVATAR}
+	GOGS_ENABLE_FEDERATED_AVATAR=${GOGS_ENABLE_FEDERATED_AVATAR}
+	GOGS_DISABLE_REGISTRATION=${GOGS_DISABLE_REGISTRATION}
+	GOGS_ENABLE_CAPTCHA=${GOGS_ENABLE_CAPTCHA}
+	GOGS_REQUIRE_SIGN_IN_VIEW=${GOGS_REQUIRE_SIGN_IN_VIEW}
+	
+	SERVICE_SPECIFIC_MX=${SERVICE_SPECIFIC_MX}
+	EOF
+	if [[ $OSTYPE =~ "darwin" ]];then
+		[[ $oldLC_ALL ]] && export LC_ALL=$oldLC_ALL || unset LC_ALL
+	fi
+
+	post_configure_routine
+}
+setup() {
+	initial_setup_routine
+
+	SUBSTITUTE=( "\${SERVICE_DOMAIN}" )
+	basic_nginx
+
+	docker_run_all
+
+	# wait for gogs db to be available
+	if ! docker exec gogs-db-dockerbunker mysqladmin ping -h"127.0.0.1" --silent;then
+		echo -e "\n\e[3mWaiting for gogs-db-dockerbunker to be ready...\e[0m"
+		while ! docker exec gogs-db-dockerbunker mysqladmin ping -h"127.0.0.1" --silent;do
+			sleep 1
+		done
+	fi
+
+	echo -e "\n\e[3mWaiting for https://${SERVICE_DOMAIN}/install to be accessible ...\e[0m"
+	# Check if installation page is accessible and then install gogs
+	while [[ $response != 200 ]];do
+		response=$(curl -kso /dev/null -w '%{http_code}' https://${SERVICE_DOMAIN}/install)
+		sleep 1
+		count+=1
+		[[ $count > 30 ]] && echo "\e[31mfailed\n\nCannot reach https://${SERVICE_DOMAIN}/install. Exiting\e[0m\n" && exit 1
+	done
+	[[ $response == 200 ]] && true
+
+	echo -en "\n\e[1mInstalling Gogs via cURL ...\e[0m"
+curl -k \
+	-H 'Origin: null' -H 'Accept-Encoding: gzip, deflate' \
+	-H 'Accept-Language: en-US,en;q=0.8,en-US;q=0.6,en;q=0.4' \
+	-H 'Upgrade-Insecure-Requests: 1' \
+	-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36' \
+	-H 'Content-Type: application/x-www-form-urlencoded' \
+	-H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8' \
+	-H 'Cache-Control: max-age=0' \
+	-H 'Cookie: _ga=GA1.1.221947403.1432575239; lang=en_US; i_like_gogits=4e8b96e2a97347e0; _csrf=0NHae8OjPYKJ6xiplpZcsUhkwu86MTQ0MTExMTA2MDEyMzQ4ODk0Ng%3D%3D' \
+	-H 'Connection: keep-alive' \
+	-d "db_type=MySQL\
+&db_host=db%3A3306\
+&db_user=${GOGS_DBUSER}\
+&db_passwd=${GOGS_DBPASS}\
+&db_name=${GOGS_DBNAME}\
+&ssl_mode=disable\
+&db_path=data/gogs.db\
+&app_name=${GOGS_APP_NAME}\
+&repo_root_path=/data/git/gogs-repositories\
+&run_user=git\
+&domain=${SERVICE_DOMAIN}\
+&ssh_port=22\
+&http_port=3000\
+&app_url=https://${SERVICE_DOMAIN}\
+&log_root_path=/app/gogs/log\
+&smtp_host=${MX_DOMAIN}:587\
+&smtp_from=\
+&smtp_user=${MX_EMAIL}\
+&smtp_passwd=${MX_PASSWORD}\
+&admin_name=${GOGS_ADMIN}\
+&admin_passwd=${GOGS_ADMIN_PASSWORD}\
+&admin_confirm_passwd=${GOGS_ADMIN_PASSWORD}\
+&admin_email=${GOGS_ADMIN_EMAIL}\
+&register_confirm=${GOGS_REGISTER_CONFIRM}\
+&mail_notify=${GOGS_MAIL_NOTIFY}\
+&offline_mode=${GOGS_OFFLINE_MODE}\
+&disable_gravatar=${GOGS_DISABLE_GRAVATAR}\
+&enable_federated_avatar=${GOGS_ENABLE_FEDERATED_AVATAR}\
+&disable_registration=${GOGS_DISABLE_REGISTRATION}\
+&enable_captcha=${GOGS_ENABLE_CAPTCHA}\
+&require_sign_in_view=${GOGS_REQUIRE_SIGN_IN_VIEW}" --compressed \
+-X POST "https://${SERVICE_DOMAIN}/install"
+
+	response=$(curl -kso /dev/null -w '%{http_code}' https://${SERVICE_DOMAIN})
+	[[ $response == 200 ]] && echo -e " \e[32m\xE2\x9C\x94\e[0m" || echo  -e " \e[31mfailed\e[0m"
+
+	post_setup_routine
+}
+if [[ $1 == "letsencrypt" ]];then
+	$1 $*
+else
+	$1
+fi
+

+ 14 - 0
data/services/gogs/mysql/my.cnf

@@ -0,0 +1,14 @@
+[mysqld]
+character-set-client-handshake = FALSE
+character-set-server           = utf8mb4
+collation-server               = utf8mb4_unicode_ci
+innodb_file_per_table          = TRUE
+innodb_file_format             = barracuda
+innodb_large_prefix            = TRUE
+#sql_mode=IGNORE_SPACE,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
+
+[client]
+default-character-set = utf8mb4
+
+[mysql]
+default-character-set = utf8mb4

+ 48 - 0
data/services/gogs/nginx/gogs.conf

@@ -0,0 +1,48 @@
+##
+# You should look at the following URL's in order to grasp a solid understanding
+# of Nginx configuration files in order to fully unleash the power of Nginx.
+# http://wiki.nginx.org/Pitfalls
+# http://wiki.nginx.org/QuickStart
+# http://wiki.nginx.org/Configuration
+#
+# Generally, you will want to move this file somewhere, and start with a clean
+# file but keep this around for reference. Or just disable in sites-enabled.
+#
+# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples.
+##
+
+# Default server configuration
+#
+upstream gogs {
+	server gogs-service-dockerbunker:3000;
+}
+
+server {
+	listen 80;
+	server_name ${SERVICE_DOMAIN};
+	return 301 https://$host$request_uri;
+}
+
+server {
+	listen 443;
+	server_name ${SERVICE_DOMAIN};
+	ssl on;
+	ssl_certificate /etc/nginx/ssl/${SERVICE_DOMAIN}/cert.pem;
+	ssl_certificate_key /etc/nginx/ssl/${SERVICE_DOMAIN}/key.pem;
+	include /etc/nginx/includes/ssl.conf;
+
+	add_header Strict-Transport-Security "max-age=15768000; includeSubDomains";
+	add_header X-Frame-Options DENY;
+	add_header X-Content-Type-Options nosniff;
+
+	include /etc/nginx/includes/gzip.conf;
+
+	location / {
+		proxy_pass http://gogs/;
+	}
+
+	location ~ /.well-known {
+		allow all;
+		root /var/www/html;
+	}
+}

+ 9 - 0
data/services/hastebin/containers.sh

@@ -0,0 +1,9 @@
+hastebin_service_dockerbunker() {
+	docker run -d \
+		--name=${FUNCNAME[0]//_/-} \
+		--restart=always \
+		--network ${NETWORK} \
+		--env-file "${SERVICE_ENV}" \
+		-v ${SERVICE_NAME}-data-vol-1:/hastebin/data \
+	${IMAGES[service]} >/dev/null
+}

+ 48 - 0
data/services/hastebin/hastebin.sh

@@ -0,0 +1,48 @@
+#!/usr/bin/env bash
+
+while true;do ls | grep -q dockerbunker.sh;if [[ $? == 0 ]];then BASE_DIR=$PWD;break;else cd ../;fi;done
+
+PROPER_NAME="Hastebin"
+SERVICE_NAME="$(echo -e "${PROPER_NAME,,}" | tr -d '[:space:]')"
+PROMPT_SSL=1
+
+declare -a environment=( "data/env/dockerbunker.env" "data/include/init.sh" )
+
+for env in "${environment[@]}";do
+	[[ -f "${BASE_DIR}"/$env ]] && source "${BASE_DIR}"/$env
+done
+
+declare -A WEB_SERVICES
+declare -a containers=( "${SERVICE_NAME}-service-dockerbunker" )
+declare -a add_to_network=( "${SERVICE_NAME}-service-dockerbunker" )
+declare -A IMAGES=( [service]="dockerbunker/${SERVICE_NAME}" )
+declare -A BUILD_IMAGES=( [dockerbunker/${SERVICE_NAME}]="${DOCKERFILES}/${SERVICE_NAME}" )
+declare -a volumes=( "${SERVICE_NAME}-data-vol-1" "${SERVICE_NAME}-data-vol-2" )
+declare -a networks=( )
+
+[[ -z $1 ]] && options_menu
+
+configure() {
+	pre_configure_routine
+	
+	echo -e "# \e[4mHastebin Settings\e[0m"
+
+	set_domain
+	
+	cat <<-EOF >> "${SERVICE_ENV}"
+	PROPER_NAME=${PROPER_NAME}
+	SERVICE_NAME=${SERVICE_NAME}
+	SSL_CHOICE=${SSL_CHOICE}
+	LE_EMAIL=${LE_EMAIL}
+
+	SERVICE_DOMAIN=${SERVICE_DOMAIN}
+	EOF
+
+	post_configure_routine
+}
+
+if [[ $1 == "letsencrypt" ]];then
+	$1 $*
+else
+	$1
+fi

+ 55 - 0
data/services/hastebin/nginx/hastebin.conf

@@ -0,0 +1,55 @@
+##
+# You should look at the following URL's in order to grasp a solid understanding
+# of Nginx configuration files in order to fully unleash the power of Nginx.
+# http://wiki.nginx.org/Pitfalls
+# http://wiki.nginx.org/QuickStart
+# http://wiki.nginx.org/Configuration
+#
+# Generally, you will want to move this file somewhere, and start with a clean
+# file but keep this around for reference. Or just disable in sites-enabled.
+#
+# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples.
+##
+
+# Default server configuration
+#
+upstream hastebin {
+ server hastebin-service-dockerbunker:7777;
+}
+
+server {
+    listen 80;
+	server_name ${SERVICE_DOMAIN};
+    return 301 https://$host$request_uri;
+}
+
+server {
+    listen 443;
+	server_name ${SERVICE_DOMAIN};
+    ssl on;
+	ssl_certificate /etc/nginx/ssl/${SERVICE_DOMAIN}/cert.pem;
+	ssl_certificate_key /etc/nginx/ssl/${SERVICE_DOMAIN}/key.pem;
+	include /etc/nginx/includes/ssl.conf;
+
+    add_header Strict-Transport-Security "max-age=15768000; includeSubDomains";
+	add_header X-Frame-Options DENY;
+	add_header X-Content-Type-Options nosniff;
+
+	include /etc/nginx/includes/gzip.conf;
+
+    location / {
+        proxy_pass http://hastebin/;
+		proxy_set_header  Host              $http_host;   # required for docker client's sake
+		proxy_set_header  X-Real-IP         $remote_addr; # pass on real client's IP
+		proxy_set_header  X-Forwarded-For   $proxy_add_x_forwarded_for;
+		proxy_set_header  X-Forwarded-Proto $scheme;
+		proxy_read_timeout                  900;
+    }
+
+	location ~ /.well-known {
+        allow all;
+		root /var/www/html;
+	}
+}
+
+

+ 7 - 0
data/services/ipsecvpnserver/README.md

@@ -0,0 +1,7 @@
+__IPsec VPN Server__
+
+To set up your client, please follow the instructions that fit your needs. The _shared secret/VPN IPsec PSK_ is automatically generated and stored in data/env/ipsecvpnserver.env
+
+    - [Configure IPsec/L2TP VPN Clients](https://github.com/hwdsl2/setup-ipsec-vpn/blob/master/docs/clients.md)
+    - [Configure IPsec/XAuth VPN Clients](https://github.com/hwdsl2/setup-ipsec-vpn/blob/master/docs/clients-xauth.md)
+	- [Configure using IKEv2 (Windows and Android](https://github.com/hwdsl2/setup-ipsec-vpn/blob/master/docs/ikev2-howto.md)

+ 10 - 0
data/services/ipsecvpnserver/containers.sh

@@ -0,0 +1,10 @@
+ipsecvpnserver_service_dockerbunker() {
+	docker run -d --privileged \
+		--name=${FUNCNAME[0]//_/-} \
+		--restart=always \
+		-p 500:500/udp \
+		-p 4500:4500/udp \
+		--env-file ${SERVICE_ENV} \
+		-v ${SERVICE_NAME}-data-vol-1:/lib/modules:ro \
+	${IMAGES[service]} >/dev/null
+}

+ 94 - 0
data/services/ipsecvpnserver/ipsecvpnserver.sh

@@ -0,0 +1,94 @@
+#!/usr/bin/env bash
+
+while true;do ls | grep -q dockerbunker.sh;if [[ $? == 0 ]];then BASE_DIR=$PWD;break;else cd ../;fi;done
+
+PROPER_NAME="IPsec VPN Server"
+SERVICE_NAME="$(echo -e "${PROPER_NAME,,}" | tr -d '[:space:]')"
+
+declare -a environment=( "data/env/dockerbunker.env" "data/include/init.sh" )
+
+for env in "${environment[@]}";do
+	[[ -f "${BASE_DIR}"/$env ]] && source "${BASE_DIR}"/$env
+done
+
+declare -a containers=( "${SERVICE_NAME}-service-dockerbunker" )
+declare -a volumes=( "${SERVICE_NAME}-data-vol-1" )
+declare -A IMAGES=( [service]="dockerbunker/${SERVICE_NAME}" )
+declare -A BUILD_IMAGES=( [dockerbunker/${SERVICE_NAME}]="${DOCKERFILES}/${SERVICE_NAME}" )
+
+[[ -z $1 ]] && options_menu
+
+configure() {
+	pre_configure_routine
+	
+	! [[ -d "${BASE_DIR}/data/Dockerfiles/ipsecvpnserver" ]] \
+	&& git submodule add -f https://github.com/hwdsl2/docker-ipsec-vpn-server.git data/Dockerfiles/ipsecvpnserver >/dev/null
+
+	echo -e "\n# \e[4mIPsec VPN Server Settings\e[0m"
+	if [ -z "$VPN_USER" ]; then
+		read -p "VPN Username: " -ei "vpnuser" VPN_USER
+	else
+		read -p "VPN Username: " -ei "${VPN_USER}" VPN_USER
+	fi
+	
+	if [ -z "$VPN_PASSWORD" ]; then
+		stty_orig=`stty -g`
+		stty -echo
+	  	read -p "VPN Password: " -ei "" VPN_PASSWORD
+		stty $stty_orig
+		echo ""
+	fi
+
+	prompt_confirm "OK to use Google DNS?"
+	if [[ $? == 0 ]];then
+		read -p "Enter DNS 1: " dns1
+		read -p "Enter DNS 2: " dns2
+		sed -i 's/8\.8\.8\.8/${dns1}/' "${BASE_DIR}"/data/Dockerfiles/${SERVICE_NAME}/run.sh
+		sed -i 's/8\.8\.4\.4/${dns2}/' "${BASE_DIR}"/data/Dockerfiles/${SERVICE_NAME}/run.sh
+	fi
+
+	# avoid tr illegal byte sequence in macOS when generating random strings
+	if [[ $OSTYPE =~ "darwin" ]];then
+		if [[ $LC_ALL ]];then
+			oldLC_ALL=$LC_ALL
+			export LC_ALL=C
+		else
+			export LC_ALL=C
+		fi
+	fi
+	cat <<-EOF >> "${SERVICE_ENV}"
+	# Please use long, random alphanumeric strings (A-Za-z0-9)
+	VPN_IPSEC_PSK=$(tr -dc 'a-zA-Z0-9' < /dev/urandom | head -c 64)
+
+	# ------------------------------
+	# User configuration
+	# ------------------------------
+	
+	VPN_USER=${VPN_USER}
+	VPN_PASSWORD="${VPN_PASSWORD}"
+	EOF
+
+	if [[ $OSTYPE =~ "darwin" ]];then
+		unset LC_ALL
+	fi
+
+	post_configure_routine
+}
+
+setup() {
+	initial_setup_routine
+	
+	if ! lsmod | grep -q af_key;then
+		echo -en "\e[1mLoading the IPsec NETKEY kernel module on the Docker host\e[0m"
+		modprobe af_key
+		exit_response
+		[[ ! $(grep ^af_key$ /etc/modules) ]] && echo af_key >> /etc/modules
+	fi
+
+	docker_run_all
+
+	post_setup_routine
+
+}
+
+$1

+ 9 - 0
data/services/kanboard/containers.sh

@@ -0,0 +1,9 @@
+kanboard_service_dockerbunker() {
+	docker run -d \
+		--name=${FUNCNAME[0]//_/-} \
+		--restart=always \
+		--network ${NETWORK} \
+		-v ${SERVICE_NAME}-db-vol-1:/var/www/app/data \
+		-v ${SERVICE_NAME}-data-vol-1:/var/www/app/plugins \
+	${IMAGES[service]} >/dev/null
+}

+ 62 - 0
data/services/kanboard/kanboard.sh

@@ -0,0 +1,62 @@
+#!/usr/bin/env bash
+
+while true;do ls | grep -q dockerbunker.sh;if [[ $? == 0 ]];then BASE_DIR=$PWD;break;else cd ../;fi;done
+
+PROPER_NAME="Kanboard"
+SERVICE_NAME="$(echo -e "${PROPER_NAME,,}" | tr -d '[:space:]')"
+PROMPT_SSL=1
+safe_to_keep_volumes_when_reconfiguring=1
+
+declare -a environment=( "data/env/dockerbunker.env" "data/include/init.sh" )
+
+for env in "${environment[@]}";do
+	[[ -f "${BASE_DIR}"/$env ]] && source "${BASE_DIR}"/$env
+done
+
+declare -A WEB_SERVICES
+declare -a containers=( "${SERVICE_NAME}-service-dockerbunker" )
+declare -a volumes=( "${SERVICE_NAME}-data-vol-1" )
+declare -a add_to_network=( "${SERVICE_NAME}-service-dockerbunker" )
+declare -a networks=( )
+declare -A IMAGES=( [service]="kanboard/kanboard:v1.2.1" )
+
+[[ -z $1 ]] && options_menu
+
+configure() {
+
+	pre_configure_routine
+
+	echo -e "# \e[4mKanboard Settings\e[0m"
+	set_domain
+	
+	cat <<-EOF >> "${SERVICE_ENV}"
+	SSL_CHOICE=${SSL_CHOICE}
+	LE_EMAIL=${LE_EMAIL}
+
+	# ------------------------------
+	# General Settings
+	# ------------------------------
+	
+	SERVICE_DOMAIN=${SERVICE_DOMAIN}
+	EOF
+
+	post_configure_routine
+}
+
+setup() {
+	initial_setup_routine
+
+	SUBSTITUTE=( "\${SERVICE_DOMAIN}" )
+	basic_nginx
+
+	docker_run_all
+
+	post_setup_routine
+}
+
+# i think this can/should go now... if it goes, change tests in letsencrypt function (\$1, \$2 \$* etc)
+if [[ $1 == "letsencrypt" ]];then
+	$1 $*
+else
+	$1
+fi

+ 70 - 0
data/services/kanboard/nginx/kanboard.conf

@@ -0,0 +1,70 @@
+##
+# You should look at the following URL's in order to grasp a solid understanding
+# of Nginx configuration files in order to fully unleash the power of Nginx.
+# http://wiki.nginx.org/Pitfalls
+# http://wiki.nginx.org/QuickStart
+# http://wiki.nginx.org/Configuration
+#
+# Generally, you will want to move this file somewhere, and start with a clean
+# file but keep this around for reference. Or just disable in sites-enabled.
+#
+# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples.
+##
+
+# Default server configuration
+#
+
+map $sent_http_content_type $expires {
+	default                    off;
+	text/html                  epoch;
+	text/css                   max;
+	application/javascript     max;
+	~image/                    max;
+}
+
+upstream kanboard {
+ server kanboard-service-dockerbunker:80;
+}
+
+server {
+	listen 80;
+	server_name ${SERVICE_DOMAIN};
+	return 301 https://$host$request_uri;
+}
+
+server {
+	listen 443;
+	server_name ${SERVICE_DOMAIN};
+	ssl on;
+	ssl_certificate /etc/nginx/ssl/${SERVICE_DOMAIN}/cert.pem;
+	ssl_certificate_key /etc/nginx/ssl/${SERVICE_DOMAIN}/key.pem;
+	include /etc/nginx/includes/ssl.conf;
+
+	add_header Strict-Transport-Security "max-age=15768000; includeSubDomains";
+	add_header X-Frame-Options SAMEORIGIN;
+	add_header X-XSS-Protection "1; mode=block";
+
+	include /etc/nginx/includes/gzip.conf;
+
+	location / {
+		proxy_pass http://kanboard/;
+	}
+
+expires $expires;
+
+# Deny all attempts to access hidden files such as .htaccess, .htpasswd, .DS_Store (Mac).
+	location ~ /\. {
+			deny all;
+			access_log off;
+			log_not_found off;
+	}
+
+	location ^~ /.well-known/ {
+	    access_log           off;
+	    log_not_found        off;
+	    root                 /var/www/html;
+#	    autoindex            off;
+	    index                index.html; # "no-such-file.txt",if expected protos don't need it
+	    try_files            $uri $uri/ =404;
+	}
+}

+ 143 - 0
data/services/mailcowdockerized/mailcowdockerized.sh

@@ -0,0 +1,143 @@
+#!/usr/bin/env bash
+
+while true;do ls | grep -q dockerbunker.sh;if [[ $? == 0 ]];then BASE_DIR=$PWD;break;else cd ../;fi;done
+
+SERVICE_HOME="${BASE_DIR}"/data/docker-compose/mailcowdockerized
+PROPER_NAME="Mailcow Dockerized"
+SERVICE_NAME="$(echo -e "${PROPER_NAME,,}" | tr -d '[:space:]')"
+PROMPT_SSL=1
+
+declare -a environment=( "data/include/init.sh" "data/env/dockerbunker.env" "data/docker-compose/${SERVICE_NAME}/mailcow.conf" )
+
+for env in "${environment[@]}";do
+	[[ -f "${BASE_DIR}"/$env ]] && source "${BASE_DIR}"/$env
+done
+
+declare -A WEB_SERVICES
+declare -a containers=( "mailcowdockerized_acme-mailcow_1" "mailcowdockerized_rspamd-mailcow_1" "mailcowdockerized_nginx-mailcow_1" "mailcowdockerized_netfilter-mailcow_1" "mailcowdockerized_php-fpm-mailcow_1" "mailcowdockerized_redis-mailcow_1" "mailcowdockerized_unbound-mailcow_1" "mailcowdockerized_ipv6nat_1" "mailcowdockerized_postfix-mailcow_1" "mailcowdockerized_memcached-mailcow_1" "mailcowdockerized_sogo-mailcow_1" "mailcowdockerized_watchdog-mailcow_1" "mailcowdockerized_dockerapi-mailcow_1" "mailcowdockerized_clamd-mailcow_1" "mailcowdockerized_dovecot-mailcow_1" "mailcowdockerized_mysql-mailcow_1" )
+declare -a volumes=( "mailcowdockerized_crypt-vol-1" "mailcowdockerized_dkim-vol-1" "mailcowdockerized_mysql-vol-1" "mailcowdockerized_postfix-vol-1" "mailcowdockerized_redis-vol-1" "mailcowdockerized_rspamd-sock" "mailcowdockerized_rspamd-vol-1" "mailcowdockerized_vmail-vol-1" )
+declare -a networks=( )
+declare -a add_to_network=( "mailcowdockerized_nginx-mailcow_1" )
+
+unset images
+for image in ${IMAGES[@]};do
+	images+=( $image )
+done
+
+DOCKER_COMPOSE=1
+
+[[ -z $1 ]] && options_menu
+
+
+upgrade() {
+	echo ""
+	echo "Please manually update the mailcow repository first, before continuing."
+	echo "The repository is located in ${SERVICE_HOME}"
+	echo ""
+	echo "For instructions refer to:"
+	echo "https://mailcow.github.io/mailcow-dockerized-docs/install-update/"
+	echo ""
+	prompt_confirm "Continue?"
+
+	if [[ $? == 0 ]];then
+		pull_and_compare
+		delete_old_images
+		restart_nginx
+	else
+		exit 0
+	fi
+}
+
+configure() {
+	pre_configure_routine
+
+	! [[ -d "${BASE_DIR}"/data/docker-compose ]] && mkdir -p "${BASE_DIR}"/data/docker-compose
+
+	pushd "${BASE_DIR}" >/dev/null
+	! [[ -d "${SERVICE_HOME}" ]] \
+	&& echo -n "Cloning Mailcow Dockerized repository into "${BASE_DIR}"/data/docker-compose/mailcowdockerized" \
+	&& git submodule add https://github.com/mailcow/mailcow-dockerized.git data/docker-compose/mailcowdockerized >/dev/null \
+	&& exit_response
+	popd >/dev/null
+
+	echo -e "# \e[4mMailcow Dockerized Settings\e[0m"
+
+	pushd "${SERVICE_HOME}" >/dev/null
+	echo -e "\n\e[3m\xe2\x86\x92 Running external script generate_config.sh\e[0m"
+	echo -e "\nThis script is required by mailcow. It generates the file "mailcow.conf". Both files can be found in the following directory\e"
+	echo ""
+	echo " ${SERVICE_HOME}"
+	echo ""
+	./generate_config.sh
+	popd >/dev/null
+	echo -e "\n\e[3m\xe2\x86\x92 Finished running generate_config.sh\e[0m"
+	echo ""
+
+	for i in $(grep "image:" "${SERVICE_HOME}"/docker-compose.yml | awk '{print $NF}');do IMAGES+=( \"$i\" );done
+
+	configure_ssl
+
+	[[ -f "${SERVICE_HOME}"/mailcow.conf ]] &&  source "${SERVICE_HOME}"/mailcow.conf || echo "Could not find mailcow.conf. Exiting."
+
+	sed -i "s/HTTP_PORT=.*/HTTP_PORT=8080/" "${SERVICE_HOME}"/mailcow.conf
+	sed -i "s/HTTPS_PORT=.*/HTTPS_PORT=8443/" "${SERVICE_HOME}"/mailcow.conf
+	sed -i "s/HTTP_BIND=.*/HTTP_BIND=127\.0\.0\.1/" "${SERVICE_HOME}"/mailcow.conf
+	sed -i "s/HTTPS_BIND=.*/HTTPS_BIND=127\.0\.0\.1/" "${SERVICE_HOME}"/mailcow.conf
+
+	cat <<-EOF >> "${SERVICE_ENV}"
+	SERVICE_HOME="${SERVICE_HOME}"
+	PROPER_NAME="${PROPER_NAME}"
+	SERVICE_NAME="${SERVICE_NAME}"
+	SSL_CHOICE=${SSL_CHOICE}
+	LE_EMAIL=${LE_EMAIL}
+	DOCKER_COMPOSE=${DOCKER_COMPOSE}
+
+	SERVICE_DOMAIN="${MAILCOW_HOSTNAME}"
+	DOMAIN=${MAILCOW_HOSTNAME#*.}
+
+	IMAGES=( ${IMAGES[@]} )
+
+	EOF
+
+	post_configure_routine
+}
+
+setup() {
+	initial_setup_routine
+
+	which docker-compose >/dev/null
+	if [[ $? == 1 ]];then
+		echo -e "docker-compose not found. You can now automatically be installed via\n\n\
+			curl -L https://github.com/docker/compose/releases/download/$(curl -Ls https://www.servercow.de/docker-compose/latest.php)/docker-compose-$(uname -s)-$(uname -m) > /usr/local/bin/docker-compose\n"
+		prompt_confirm "Continue?"
+		if [[ $? == 0 ]];then
+			curl -L https://github.com/docker/compose/releases/download/$(curl -Ls https://www.servercow.de/docker-compose/latest.php)/docker-compose-$(uname -s)-$(uname -m) > /usr/local/bin/docker-compose
+			chmod +x /usr/local/bin/docker-compose
+		else
+			echo "Please install docker-compose and try again."
+			exit 0;
+		fi
+	fi
+
+	SUBSTITUTE=( "\${SERVICE_DOMAIN}" "\${DOMAIN}" )
+
+	basic_nginx
+
+	pushd "${SERVICE_HOME}" >/dev/null
+	docker-compose up -d
+	popd >/dev/null
+
+	connect_containers_to_network
+
+	restart_nginx
+
+	[[ SSL_CHOICE == "le" ]] && echo -e "\nMake sure to add autodiscover.${DOMAIN} and autoconfig.${DOMAIN} to your Let's Encrypt certificate."
+
+	post_setup_routine
+}
+
+if [[ $1 == "letsencrypt" ]];then
+	$1 $*
+else
+	$1
+fi

+ 64 - 0
data/services/mailcowdockerized/nginx/mailcowdockerized.conf

@@ -0,0 +1,64 @@
+##
+# You should look at the following URL's in order to grasp a solid understanding
+# of Nginx configuration files in order to fully unleash the power of Nginx.
+# http://wiki.nginx.org/Pitfalls
+# http://wiki.nginx.org/QuickStart
+# http://wiki.nginx.org/Configuration
+#
+# Generally, you will want to move this file somewhere, and start with a clean
+# file but keep this around for reference. Or just disable in sites-enabled.
+#
+# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples.
+##
+
+# Default server configuration
+#
+upstream mailcow {
+ server mailcowdockerized_nginx-mailcow_1:8080;
+}
+
+server {
+    listen 80;
+    server_name ${SERVICE_DOMAIN} autodiscover.${DOMAIN} autoconfig.${DOMAIN};
+    return 301 https://$host$request_uri;
+	add_header X-Content-Type-Options "nosniff" always;
+	add_header X-XSS-Protection "1; mode=block" always;
+	add_header X-Frame-Options "DENY" always;
+	add_header Referrer-Policy "strict-origin" always;
+	add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
+	server_tokens off;
+}
+
+server {
+    listen 443;
+    server_name ${SERVICE_DOMAIN} autodiscover.${DOMAIN} autoconfig.${DOMAIN};
+    ssl on;
+	ssl_certificate /etc/nginx/ssl/${SERVICE_DOMAIN}/cert.pem;
+	ssl_certificate_key /etc/nginx/ssl/${SERVICE_DOMAIN}/key.pem;
+	include /etc/nginx/includes/ssl.conf;
+
+	add_header X-Content-Type-Options "nosniff" always;
+	add_header X-XSS-Protection "1; mode=block" always;
+	add_header X-Frame-Options "DENY" always;
+	add_header Referrer-Policy "strict-origin" always;
+	add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
+	server_tokens off;
+
+	include /etc/nginx/includes/gzip.conf;
+
+    location / {
+        proxy_pass http://mailcow/;
+		proxy_set_header  Host              $http_host;   # required for docker client's sake
+		proxy_set_header  X-Real-IP         $remote_addr; # pass on real client's IP
+		proxy_set_header  X-Forwarded-For   $proxy_add_x_forwarded_for;
+		proxy_set_header  X-Forwarded-Proto $scheme;
+		proxy_read_timeout                  900;
+    }
+
+	location ~ /.well-known {
+        allow all;
+		root /var/www/html;
+	}
+}
+
+

+ 3 - 0
data/services/mailpile/README.md

@@ -0,0 +1,3 @@
+__Mailpile__
+
+Not really usable. I haven't figured out how to really persist data volumes yet :-/

+ 9 - 0
data/services/mailpile/containers.sh

@@ -0,0 +1,9 @@
+mailpile_service_dockerbunker() {
+	docker run -d \
+		--name=${FUNCNAME[0]//_/-} \
+		--restart=always \
+		--network ${NETWORK} \
+		-v mailpile-data-vol-1:/mailpile-data \
+		-p ${PORT}:${PORT} \
+	${IMAGES[service]} ${COMMAND} >/dev/null
+}

+ 94 - 0
data/services/mailpile/mailpile.sh

@@ -0,0 +1,94 @@
+#!/usr/bin/env bash
+
+while true;do ls | grep -q dockerbunker.sh;if [[ $? == 0 ]];then BASE_DIR=$PWD;break;else cd ../;fi;done
+
+PROPER_NAME="Mailpile"
+SERVICE_NAME="$(echo -e "${PROPER_NAME,,}" | tr -d '[:space:]')"
+PROMPT_SSL=1
+
+declare -a environment=( "data/env/dockerbunker.env" "data/include/init.sh" )
+
+for env in "${environment[@]}";do
+	[[ -f "${BASE_DIR}/$env" ]] && source "${BASE_DIR}/$env"
+done
+
+declare -A WEB_SERVICES
+declare -a containers=( "${SERVICE_NAME}-service-dockerbunker" )
+declare -a volumes=( "${SERVICE_NAME}-data-vol-1")
+declare -a add_to_network=( "${SERVICE_NAME}-service-dockerbunker" )
+declare -a networks=( )
+declare -A IMAGES=( [service]="dockerbunker/dillinger" )
+declare -A BUILD_IMAGES=( [dockerbunker/${SERVICE_NAME}]="${DOCKERFILES}/${SERVICE_NAME}" )
+
+[[ -z $1 ]] && options_menu
+
+configure() {
+	pre_configure_routine
+	
+	! [[ -d "${BASE_DIR}"/data/Dockerfiles/mailpile ]] \
+	&& echo -n "Cloning Mailpile repository into ${BASE_DIR}/data/Dockerfiles/mailpile" \
+	&& git submodule add https://github.com/mailpile/Mailpile.git "${BASE_DIR}"/data/Dockerfiles/mailpile >/dev/null \
+	&& exit_response
+	
+	
+	echo -e "# \e[4mMailpile Settings\e[0m"
+	
+	set_domain
+	
+	unset COMMAND
+	prompt_confirm "Install in subdirectory?"
+	if [[ $? == 0 ]];then
+		if [ -z "$SUBDIR" ]; then
+			read -p "Enter subdirectory: /" -ei "mailpile" SUBDIR
+		else
+			read -p "Enter subdirectory: /" -ei "${SUBDIR}" SUBDIR
+		fi
+		LOCATION="^~ /${SUBDIR}"
+		PORT="12345"
+		COMMAND="./mp --www=0.0.0.0:${PORT}/${SUBDIR}"
+	else
+		PORT="33411"
+		LOCATION="/"
+	fi
+	
+	cat <<-EOF >> "${SERVICE_ENV}"
+	PROPER_NAME="${PROPER_NAME}"
+	SERVICE_NAME=${SERVICE_NAME}
+	PROMPT_SSL=${PROMPT_SSL}
+	SUBDIR="${SUBDIR}"
+	PORT=${PORT}
+	LOCATION="${LOCATION}"
+	COMMAND="${COMMAND}"
+	SSL_CHOICE=${SSL_CHOICE}
+	LE_EMAIL="${LE_EMAIL}"
+	
+	# ------------------------------
+	# General Settings
+	# ------------------------------
+	
+	SERVICE_DOMAIN=${SERVICE_DOMAIN}
+	EOF
+
+	SUBSTITUTE=( "\${SERVICE_DOMAIN}" "\${PORT}" "\${LOCATION}" )
+
+	post_configure_routine
+}
+setup() {
+	# add volume section to dockerfile
+	[[ ! $(grep "VOLUME" "${BASE_DIR}/data/Dockerfiles/${SERVICE_NAME}/Dockerfile") ]] && sed -i "/EXPOSE/a VOLUME \/mailpile-data\/.local\/share\/Mailpile\nVOLUME \/mailpile-data\/.gnupg/" "${DOCKERFILES}"/${SERVICE_NAME}/Dockerfile
+
+	initial_setup_routine
+
+	SUBSTITUTE=( "\${SERVICE_DOMAIN}" "\${PORT}" "\${LOCATION}" )
+	basic_nginx
+
+	docker_run_all
+
+	post_setup_routine
+}
+
+if [[ $1 == "letsencrypt" ]];then
+	$1 $*
+else
+	$1
+fi

+ 53 - 0
data/services/mailpile/nginx/mailpile.conf

@@ -0,0 +1,53 @@
+##
+# You should look at the following URL's in order to grasp a solid understanding
+# of Nginx configuration files in order to fully unleash the power of Nginx.
+# http://wiki.nginx.org/Pitfalls
+# http://wiki.nginx.org/QuickStart
+# http://wiki.nginx.org/Configuration
+#
+# Generally, you will want to move this file somewhere, and start with a clean
+# file but keep this around for reference. Or just disable in sites-enabled.
+#
+# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples.
+##
+
+# Default server configuration
+#
+upstream mailpile {
+	server mailpile-service-dockerbunker:${PORT};
+}
+
+server {
+	listen 80;
+	server_name ${SERVICE_DOMAIN};
+	return 301 https://$host$request_uri;
+}
+
+server {
+	listen 443;
+	server_name ${SERVICE_DOMAIN};
+	ssl on;
+	ssl_certificate /etc/nginx/ssl/${SERVICE_DOMAIN}/cert.pem;
+	ssl_certificate_key /etc/nginx/ssl/${SERVICE_DOMAIN}/key.pem;
+	include /etc/nginx/includes/ssl.conf;
+
+	add_header Strict-Transport-Security "max-age=15768000; includeSubDomains";
+	add_header X-Frame-Options SAMEORIGIN;
+	add_header X-Content-Type-Options nosniff;
+
+	include /etc/nginx/includes/gzip.conf;
+
+	location ${LOCATION} {
+		rewrite ^(.*)$ $1 break; # prevents additional rewrites of the path via proxy_pass
+		proxy_set_header X-Real-IP $remote_addr;
+		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+		proxy_set_header Host $http_host;
+		proxy_set_header X-NginX-Proxy true;
+		proxy_pass http://mailpile/;
+	}
+
+	location ~ /.well-known {
+		allow all;
+		root /var/www/html;
+	}
+}

+ 210 - 0
data/services/mastodon/.env-production

@@ -0,0 +1,210 @@
+# Service dependencies
+# You may set REDIS_URL instead for more advanced options
+# You may also set REDIS_NAMESPACE to share Redis between multiple Mastodon servers
+REDIS_HOST=redis
+REDIS_PORT=6379
+# You may set DATABASE_URL instead for more advanced options
+DB_HOST=postgres
+DB_USER=postgres
+DB_NAME=postgres
+DB_PASS=
+DB_PORT=5432
+# Optional ElasticSearch configuration
+ES_ENABLED=true
+ES_HOST=es
+ES_PORT=9200
+
+# Federation
+# Note: Changing LOCAL_DOMAIN at a later time will cause unwanted side effects, including breaking all existing federation.
+# LOCAL_DOMAIN should *NOT* contain the protocol part of the domain e.g https://example.com.
+#LOCAL_DOMAIN=mastodon.chaosbunker.com
+
+# Changing LOCAL_HTTPS in production is no longer supported. (Mastodon will always serve https:// links)
+
+# Use this only if you need to run mastodon on a different domain than the one used for federation.
+# You can read more about this option on https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Serving_a_different_domain.md
+# DO *NOT* USE THIS UNLESS YOU KNOW *EXACTLY* WHAT YOU ARE DOING.
+# WEB_DOMAIN=mastodon.example.com
+
+# Use this if you want to have several aliases handler@example1.com
+# handler@example2.com etc. for the same user. LOCAL_DOMAIN should not
+# be added. Comma separated values
+# ALTERNATE_DOMAINS=example1.com,example2.com
+
+# Application secrets
+# Generate each with the `RAILS_ENV=production bundle exec rake secret` task (`docker-compose run --rm web rake secret` if you use docker compose)
+#SECRET_KEY_BASE=
+#OTP_SECRET=
+
+# VAPID keys (used for push notifications
+# You can generate the keys using the following command (first is the private key, second is the public one)
+# You should only generate this once per instance. If you later decide to change it, all push subscription will
+# be invalidated, requiring the users to access the website again to resubscribe.
+#
+# Generate with `RAILS_ENV=production bundle exec rake mastodon:webpush:generate_vapid_key` task (`docker-compose run --rm web rake mastodon:webpush:generate_vapid_key` if you use docker compose)
+#
+# For more information visit https://rossta.net/blog/using-the-web-push-api-with-vapid.html
+#VAPID_PRIVATE_KEY=
+#VAPID_PUBLIC_KEY=
+
+# Registrations
+# Single user mode will disable registrations and redirect frontpage to the first profile
+# SINGLE_USER_MODE=true
+# Prevent registrations with following e-mail domains
+# EMAIL_DOMAIN_BLACKLIST=example1.com|example2.de|etc
+# Only allow registrations with the following e-mail domains
+# EMAIL_DOMAIN_WHITELIST=example1.com|example2.de|etc
+
+# Optionally change default language
+# DEFAULT_LOCALE=de
+
+# E-mail configuration
+# Note: Mailgun and SparkPost (https://sparkpo.st/smtp) each have good free tiers
+# If you want to use an SMTP server without authentication (e.g local Postfix relay)
+# then set SMTP_AUTH_METHOD and SMTP_OPENSSL_VERIFY_MODE to 'none' and
+# *comment* SMTP_LOGIN and SMTP_PASSWORD (leaving them blank is not enough).
+#SMTP_SERVER=mail.chaosbunker.com
+#SMTP_PORT=587
+#SMTP_LOGIN=mailbot@chaosbunker.com
+#SMTP_PASSWORD="pLD8IcJvW7rhxKve6BYfqoLGjcNKoySezvKdzyBG"
+#SMTP_FROM_ADDRESS=mailbot@chaosbunker.com
+#SMTP_DOMAIN= # defaults to LOCAL_DOMAIN
+#SMTP_DELIVERY_METHOD=smtp # delivery method can also be sendmail
+#SMTP_AUTH_METHOD=plain
+#SMTP_CA_FILE=/etc/ssl/certs/ca-certificates.crt
+#SMTP_OPENSSL_VERIFY_MODE=peer
+#SMTP_ENABLE_STARTTLS_AUTO=true
+#SMTP_TLS=true
+
+# Optional user upload path and URL (images, avatars). Default is :rails_root/public/system. If you set this variable, you are responsible for making your HTTP server (eg. nginx) serve these files.
+# PAPERCLIP_ROOT_PATH=/var/lib/mastodon/public-system
+# PAPERCLIP_ROOT_URL=/system
+
+# Optional asset host for multi-server setups
+# CDN_HOST=https://assets.example.com
+
+# S3 (optional)
+# S3_ENABLED=true
+# S3_BUCKET=
+# AWS_ACCESS_KEY_ID=
+# AWS_SECRET_ACCESS_KEY=
+# S3_REGION=
+# S3_PROTOCOL=http
+# S3_HOSTNAME=192.168.1.123:9000
+
+# S3 (Minio Config (optional) Please check Minio instance for details)
+# S3_ENABLED=true
+# S3_BUCKET=
+# AWS_ACCESS_KEY_ID=
+# AWS_SECRET_ACCESS_KEY=
+# S3_REGION=
+# S3_PROTOCOL=https
+# S3_HOSTNAME=
+# S3_ENDPOINT=
+# S3_SIGNATURE_VERSION=
+
+# Swift (optional)
+# SWIFT_ENABLED=true
+# SWIFT_USERNAME=
+# For Keystone V3, the value for SWIFT_TENANT should be the project name
+# SWIFT_TENANT=
+# SWIFT_PASSWORD=
+# Keystone V2 and V3 URLs are supported. Use a V3 URL if possible to avoid
+# issues with token rate-limiting during high load.
+# SWIFT_AUTH_URL=
+# SWIFT_CONTAINER=
+# SWIFT_OBJECT_URL=
+# SWIFT_REGION=
+# Defaults to 'default'
+# SWIFT_DOMAIN_NAME=
+# Defaults to 60 seconds. Set to 0 to disable
+# SWIFT_CACHE_TTL=
+
+# Optional alias for S3 if you want to use Cloudfront or Cloudflare in front
+# S3_CLOUDFRONT_HOST=
+
+# Streaming API integration
+# STREAMING_API_BASE_URL=
+
+# Advanced settings
+# If you need to use pgBouncer, you need to disable prepared statements:
+# PREPARED_STATEMENTS=false
+
+# Cluster number setting for streaming API server.
+# If you comment out following line, cluster number will be `numOfCpuCores - 1`.
+STREAMING_CLUSTER_NUM=1
+
+# Docker mastodon user
+# If you use Docker, you may want to assign UID/GID manually.
+# UID=1000
+# GID=1000
+
+# LDAP authentication (optional)
+# LDAP_ENABLED=true
+# LDAP_HOST=localhost
+# LDAP_PORT=389
+# LDAP_METHOD=simple_tls
+# LDAP_BASE=
+# LDAP_BIND_DN=
+# LDAP_PASSWORD=
+# LDAP_UID=cn
+
+# PAM authentication (optional)
+# PAM authentication uses for the email generation the "email" pam variable
+# and optional as fallback PAM_DEFAULT_SUFFIX
+# The pam environment variable "email" is provided by:
+# https://github.com/devkral/pam_email_extractor
+# PAM_ENABLED=true
+# Fallback Suffix for email address generation (nil by default)
+# PAM_DEFAULT_SUFFIX=pam
+# Name of the pam service (pam "auth" section is evaluated)
+# PAM_DEFAULT_SERVICE=rpam
+# Name of the pam service used for checking if an user can register (pam "account" section is evaluated) (nil (disabled) by default)
+# PAM_CONTROLLED_SERVICE=rpam
+
+# Global OAuth settings (optional) :
+# If you have only one strategy, you may want to enable this
+# OAUTH_REDIRECT_AT_SIGN_IN=true
+
+# Optional CAS authentication (cf. omniauth-cas) :
+# CAS_ENABLED=true
+# CAS_URL=https://sso.myserver.com/
+# CAS_HOST=sso.myserver.com/
+# CAS_PORT=443
+# CAS_SSL=true
+# CAS_VALIDATE_URL=
+# CAS_CALLBACK_URL=
+# CAS_LOGOUT_URL=
+# CAS_LOGIN_URL=
+# CAS_UID_FIELD='user'
+# CAS_CA_PATH=
+# CAS_DISABLE_SSL_VERIFICATION=false
+# CAS_UID_KEY='user'
+# CAS_NAME_KEY='name'
+# CAS_EMAIL_KEY='email'
+# CAS_NICKNAME_KEY='nickname'
+# CAS_FIRST_NAME_KEY='firstname'
+# CAS_LAST_NAME_KEY='lastname'
+# CAS_LOCATION_KEY='location'
+# CAS_IMAGE_KEY='image'
+# CAS_PHONE_KEY='phone'
+
+# Optional SAML authentication (cf. omniauth-saml)
+# SAML_ENABLED=true
+# SAML_ACS_URL=
+# SAML_ISSUER=http://localhost:3000/auth/auth/saml/callback
+# SAML_IDP_SSO_TARGET_URL=https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO
+# SAML_IDP_CERT=
+# SAML_IDP_CERT_FINGERPRINT=
+# SAML_NAME_IDENTIFIER_FORMAT=
+# SAML_CERT=
+# SAML_PRIVATE_KEY=
+# SAML_SECURITY_WANT_ASSERTION_SIGNED=true
+# SAML_SECURITY_WANT_ASSERTION_ENCRYPTED=true
+# SAML_SECURITY_ASSUME_EMAIL_IS_VERIFIED=true
+# SAML_ATTRIBUTES_STATEMENTS_UID="urn:oid:0.9.2342.19200300.100.1.1"
+# SAML_ATTRIBUTES_STATEMENTS_EMAIL="urn:oid:1.3.6.1.4.1.5923.1.1.1.6"
+# SAML_ATTRIBUTES_STATEMENTS_FULL_NAME="urn:oid:2.5.4.42"
+# SAML_UID_ATTRIBUTE="urn:oid:0.9.2342.19200300.100.1.1"
+# SAML_ATTRIBUTES_STATEMENTS_VERIFIED=
+# SAML_ATTRIBUTES_STATEMENTS_VERIFIED_EMAIL=

+ 99 - 0
data/services/mastodon/containers.sh

@@ -0,0 +1,99 @@
+mastodon_service_dockerbunker() {
+	docker run -d --user mastodon \
+		--name=${FUNCNAME[0]//_/-} \
+		--restart=always \
+		--network ${NETWORK} \
+		--network dockerbunker-${SERVICE_NAME} \
+		--env RUN_DB_MIGRATIONS=true --env UID=991 --env GID=991 --env WEB_CONCURRENCY=16 --env MAX_THREADS=20 --env SIDEKIQ_WORKERS=25 \
+		--env-file "${SERVICE_ENV}" \
+		-v mastodon-data-vol-1:/mastodon/public/system \
+		-v mastodon-data-vol-2:/mastodon/public/assets \
+		-v mastodon-data-vol-3:/mastodon/public/packs \
+	${IMAGES[service]}${GLITCH} bundle exec rails s -p 3000 -b '0.0.0.0' >/dev/null
+}
+
+mastodon_streaming_dockerbunker() {
+	docker run -d --user mastodon \
+		--name=${FUNCNAME[0]//_/-} \
+		--restart=always \
+		--network ${NETWORK} \
+		--network dockerbunker-${SERVICE_NAME} \
+		--env RUN_DB_MIGRATIONS=true --env UID=991 --env GID=991 --env WEB_CONCURRENCY=16 --env MAX_THREADS=20 --env SIDEKIQ_WORKERS=25 \
+		--env-file "${SERVICE_ENV}" \
+	${IMAGES[service]}${GLITCH} npm run start >/dev/null
+}
+
+mastodon_sidekiq_dockerbunker() {
+	docker run -d --user mastodon \
+		--name=${FUNCNAME[0]//_/-} \
+		--restart=always \
+		--network dockerbunker-${SERVICE_NAME} --net-alias=sidekiq \
+		--env RUN_DB_MIGRATIONS=true --env UID=991 --env GID=991 --env WEB_CONCURRENCY=16 --env MAX_THREADS=20 --env SIDEKIQ_WORKERS=25 \
+		--env-file "${SERVICE_ENV}" \
+		-v mastodon-data-vol-1:/mastodon/public/system \
+		-v mastodon-data-vol-2:/mastodon/public/assets \
+		-v mastodon-data-vol-3:/mastodon/public/packs \
+	${IMAGES[service]}${GLITCH} bundle exec sidekiq -q default -q mailers -q pull -q push >/dev/null
+}
+
+mastodon_redis_dockerbunker() {
+	docker run -d --user redis \
+		--name ${FUNCNAME[0]//_/-} \
+		--network dockerbunker-${SERVICE_NAME} --net-alias redis \
+		-v mastodon-redis-vol-1:/data \
+	${IMAGES[redis]} >/dev/null
+}
+
+mastodon_elasticsearch_dockerbunker() {
+	docker run -d --user elasticsearch \
+		--name=${FUNCNAME[0]//_/-} \
+		--restart=unless-stopped \
+		--network dockerbunker-${SERVICE_NAME} --net-alias=es \
+		--env ES_JAVA_OPTS="-Xms512m -Xmx512m" \
+		-v mastodon-elasticsearch-vol-1:/usr/share/elasticsearch/data \
+	${IMAGES[elasticsearch]} >/dev/null
+}
+
+mastodon_postgres_dockerbunker() {
+	docker run -d --user postgres \
+		--name=${FUNCNAME[0]//_/-} \
+		--restart=unless-stopped \
+		--network dockerbunker-${SERVICE_NAME} --net-alias=postgres \
+		-v mastodon-postgres-vol-1:/var/lib/postgresql/data \
+	${IMAGES[postgres]} >/dev/null
+}
+
+mastodon_generatevapidkeys_dockerbunker() {
+	echo -en "\n\e[1mGenerating VAPID keys\e[0m"
+	docker run -it --rm \
+		--name=${SERVICE_NAME}-vapidgen-dockerbunker \
+		--env-file "${SERVICE_ENV}" \
+	${IMAGES[service]}${GLITCH} rake mastodon:webpush:generate_vapid_key | grep VAPID > "${ENV_DIR}"/${SERVICE_NAME}_tmp.env
+	exit_response
+}
+
+mastodon_dbmigrateandprecompileassets_dockerbunker() {
+	echo -en "\n\e[1mCreating DB and precompiling assets\e[0m"
+	docker run -it --rm \
+		--name=${SERVICE_NAME}-dbsetup-dockerbunker \
+		--network dockerbunker-${SERVICE_NAME} \
+		--env-file "${SERVICE_ENV}" \
+		-v mastodon-data-vol-1:/mastodon/public/system \
+		-v mastodon-data-vol-2:/mastodon/public/assets \
+		-v mastodon-data-vol-3:/mastodon/public/packs \
+	${IMAGES[service]}${GLITCH} bash -c "rake db:migrate && rake assets:precompile" >/dev/null
+	exit_response
+}
+
+mastodon_makeadmin_dockerbunker() {
+	echo -en "\n\e[1mMaking ${1} admin...\e[0m"
+	docker run -it --rm \
+		--name=${FUNCNAME[0]//_/-} \
+		--network dockerbunker-${SERVICE_NAME} \
+		--env-file "${SERVICE_ENV}" \
+		-v mastodonglitch-data-vol-1:/mastodon/public/system \
+		-v mastodonglitch-data-vol-2:/mastodon/public/assets \
+		-v mastodonglitch-data-vol-3:/mastodon/public/packs \
+	${IMAGES[service]} bash -c "RAILS_ENV=production bundle exec rails mastodon:make_admin USERNAME=${1}" >/dev/null
+	exit_response
+}

+ 25 - 0
data/services/mastodon/make_admin.sh

@@ -0,0 +1,25 @@
+#!/usr/bin/env bash
+
+[[ -z $1 || $2 ]] && echo "Usage: ./make_admin.sh username" && exit 1
+
+while true;do ls | grep -q dockerbunker.sh;if [[ $? == 0 ]];then BASE_DIR=$PWD;break;else cd ../;fi;done
+
+declare -a environment=( "data/include/init.sh" "data/env/dockerbunker.env" "data/env/mastodon.env" )
+
+for env in "${environment[@]}";do
+	[[ -f "${BASE_DIR}/$env" ]] && source "${BASE_DIR}/$env"
+done
+
+image=( "dockerbunker/mastodon${glitch}" )
+
+echo -en "Making ${2} admin..."
+docker run -it --rm \
+	--name=${SERVICE_NAME}-setup-dockerbunker \
+	--network dockerbunker-${SERVICE_NAME} \
+	--env-file "${SERVICE_ENV}" \
+	-v mastodonglitch-data-vol-1:/mastodon/public/system \
+	-v mastodonglitch-data-vol-2:/mastodon/public/assets \
+	-v mastodonglitch-data-vol-3:/mastodon/public/packs \
+${IMAGES[service]} bash -c "RAILS_ENV=production bundle exec rails mastodon:make_admin USERNAME=${2}" >/dev/null
+exit_response
+

+ 199 - 0
data/services/mastodon/mastodon.sh

@@ -0,0 +1,199 @@
+#!/usr/bin/env bash
+while true;do ls | grep -q dockerbunker.sh;if [[ $? == 0 ]];then BASE_DIR=$PWD;break;else cd ../;fi;done
+
+PROPER_NAME="Mastodon"
+SERVICE_NAME="$(echo -e "${PROPER_NAME,,}" | tr -d '[:space:]')"
+PROMPT_SSL=1
+
+declare -A WEB_SERVICES
+declare -a environment=( "data/env/dockerbunker.env" "data/include/init.sh" )
+
+for env in "${environment[@]}";do
+	[[ -f "${BASE_DIR}"/$env ]] && source "${BASE_DIR}"/$env
+done
+
+declare -a containers=( "${SERVICE_NAME}-postgres-dockerbunker" "${SERVICE_NAME}-redis-dockerbunker" "${SERVICE_NAME}-service-dockerbunker" "${SERVICE_NAME}-streaming-dockerbunker" "${SERVICE_NAME}-sidekiq-dockerbunker" "${SERVICE_NAME}-elasticsearch-dockerbunker" )
+declare -a add_to_network=( "${SERVICE_NAME}-service-dockerbunker" "${SERVICE_NAME}-streaming-dockerbunker" )
+declare -a volumes=( "${SERVICE_NAME}-data-vol-1" "${SERVICE_NAME}-data-vol-2" "${SERVICE_NAME}-data-vol-3" "${SERVICE_NAME}-redis-data-vol-1" "${SERVICE_NAME}-elasticsearch-vol-1" "${SERVICE_NAME}-postgres-vol-1" "${SERVICE_NAME}-sidekiq-vol-1" "${SERVICE_NAME}-sidekiq-vol-2" )
+declare -a networks=( "dockerbunker-${SERVICE_NAME}" )
+declare -A IMAGES=( [service]="dockerbunker/${SERVICE_NAME}" [redis]="redis:4.0-alpine" [postgres]="postgres" [elasticsearch]="docker.elastic.co/elasticsearch/elasticsearch-oss:6.1.3" )
+declare -A BUILD_IMAGES=( [dockerbunker/${SERVICE_NAME}${GLITCH}]="${DOCKERFILES}/${SERVICE_NAME}${GLITCH}" )
+
+if [[ $1 == "make_admin" ]];then
+	if [[ -z $2 || $3 ]];then
+		echo "Usage: ./mastodon.sh make_admin username"
+		exit 1
+	else
+		mastodon_makeadmin_dockerbunker $2
+		exit 0
+	fi
+fi
+
+[[ -z $1 ]] && options_menu
+
+upgrade() {
+
+	get_current_images_sha256
+
+	[[ -z $GLITCH ]] && current="Mastodon" alt="Mastodon Glitch Edition" \
+		|| current="Mastodon Glitch Edition" alt="Mastodon" \
+
+	prompt_confirm "You are currently using ${current}. Switch to ${alt}?"
+	
+	if [[ $? == 0 && -z $GLITCH ]];then
+		GLITCH=glitch
+		sed -i "s/GLITCH=.*/GLITCH=${GLITCH}/" "${SERVICE_ENV}"
+		# temporarily change service name to build image from mastodon glitch repo
+		SERVICE_NAME=mastodonglitch
+	else
+		GLITCH=""
+		sed -i "s/GLITCH=.*/GLITCH=/" "${SERVICE_ENV}"
+	fi
+
+	docker_build
+	docker_pull
+
+	stop_containers
+	remove_containers
+
+	[[ $GLITCH ]] && SERVICE_NAME="$(echo -e "${PROPER_NAME,,}" | tr -d '[:space:]')"
+
+	mastodon_postgres_dockerbunker
+	mastodon_redis_dockerbunker
+	mastodon_dbmigrateandprecompileassets_dockerbunker
+
+	docker_run_all
+
+	delete_old_images
+
+	restart_nginx
+}
+
+configure() {
+	pre_configure_routine
+	
+	echo -e "# \e[4mMastodon Settings\e[0m"
+
+	unset GLITCH
+	prompt_confirm "Deploy Glitch Edition (https://-soc.github.io/docs/)?"
+	if [[ $? == 0 ]];then
+		GLITCH=glitch
+		SERVICE_NAME=${SERVICE_NAME}glitch
+		! [[ -d "${BASE_DIR}"/data/Dockerfiles/${SERVICE_NAME} ]] \
+			&& git submodule add -f https://github.com/glitch-soc/mastodon.git "data/Dockerfiles/mastodonglitch" >/dev/null
+	else
+		! [[ -d "${BASE_DIR}"/data/Dockerfiles/mastodon ]] \
+			&& git submodule add -f https://github.com/tootsuite/mastodon.git "data/Dockerfiles/${SERVICE_NAME}" >/dev/null
+	fi
+
+	set_domain
+
+	### Increase number of characters / toot
+	sed -i -e 's/500/800/g' \
+	    "${BASE_DIR}"/data/Dockerfiles/${SERVICE_NAME}/app/javascript/mastodon/features/compose/components/compose_form.js \
+	    "${BASE_DIR}"/data/Dockerfiles/${SERVICE_NAME}/app/validators/status_length_validator.rb \
+		${BASE_DIR}/data/Dockerfiles/${SERVICE_NAME}/config/locales/*.yml
+	
+	### Increase bio length
+	sed -i -e 's/160/400/g' \
+	    "${BASE_DIR}"/data/Dockerfiles/${SERVICE_NAME}/app/javascript/packs/public.js \
+	    "${BASE_DIR}"/data/Dockerfiles/${SERVICE_NAME}/app/models/account.rb \
+	    "${BASE_DIR}"/data/Dockerfiles/${SERVICE_NAME}/app/views/settings/profiles/show.html.haml
+	
+	### Cat emoji
+	sed -i -e 's/1f602/1f431/g' \
+		"${BASE_DIR}"/data/Dockerfiles/${SERVICE_NAME}/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
+
+	configure_mx
+
+	SERVICE_NAME="$(echo -e "${PROPER_NAME,,}" | tr -d '[:space:]')"
+
+	# avoid tr illegal byte sequence in macOS when generating random strings
+	if [[ $OSTYPE =~ "darwin" ]];then
+		if [[ $LC_ALL ]];then
+			oldLC_ALL=$LC_ALL
+			export LC_ALL=C
+		else
+			export LC_ALL=C
+		fi
+	fi
+	cat <<-EOF >> "${SERVICE_ENV}"
+	PROPER_NAME="${PROPER_NAME}"
+	SERVICE_NAME="${SERVICE_NAME}"
+	SSL_CHOICE=${SSL_CHOICE}
+	LE_EMAIL=${LE_EMAIL}
+	GLITCH=${GLITCH}
+
+	SERVICE_DOMAIN="${SERVICE_DOMAIN}"
+	LOCAL_DOMAIN=${SERVICE_DOMAIN}
+	SECRET_KEY_BASE=$(tr -dc 'a-zA-Z0-9' < /dev/urandom | head -c 128)
+	OTP_SECRET=$(tr -dc 'a-zA-Z0-9' < /dev/urandom | head -c 128)
+	VAPID_PRIVATE_KEY=
+	VAPID_PUBLIC_KEY=
+
+	REDIS_HOST=redis
+	REDIS_PORT=6379
+	DB_HOST=postgres
+	DB_USER=postgres
+	DB_NAME=postgres
+	DB_PASS=
+	DB_PORT=5432
+	ES_ENABLED=true
+	ES_HOST=es
+	ES_PORT=9200
+
+	SMTP_SERVER=${MX_HOSTNAME}
+	SMTP_PORT=587
+	SMTP_LOGIN=${MX_EMAIL}
+	SMTP_PASSWORD=${MX_PASSWORD}
+	SMTP_FROM_ADDRESS=${MX_EMAIL}
+	STREAMING_CLUSTER_NUM=1
+
+	SERVICE_SPECIFIC_MX=${SERVICE_SPECIFIC_MX}
+	EOF
+
+	source "${SERVICE_ENV}"
+	if [[ $OSTYPE =~ "darwin" ]];then
+		[[ $oldLC_ALL ]] && export LC_ALL=$oldLC_ALL || unset LC_ALL
+	fi
+
+	docker_build dockerbunker/mastodon${GLITCH}
+
+	mastodon_generatevapidkeys_dockerbunker
+	source "${ENV_DIR}"/${SERVICE_NAME}_tmp.env
+	rm "${ENV_DIR}"/${SERVICE_NAME}_tmp.env
+
+	sed -i "s/VAPID_PRIVATE_KEY=.*/VAPID_PRIVATE_KEY=${VAPID_PRIVATE_KEY}/" "${ENV_DIR}"/${SERVICE_NAME}.env
+	sed -i "s/VAPID_PUBLIC_KEY=.*/VAPID_PUBLIC_KEY=${VAPID_PUBLIC_KEY}/" "${ENV_DIR}"/${SERVICE_NAME}.env
+
+	post_configure_routine
+}
+setup() {
+
+	[[ $GLITCH ]] && SERVICE_NAME=${SERVICE_NAME}${GLITCH}
+
+	initial_setup_routine
+
+	[[ $GLITCH ]] && SERVICE_NAME="$(echo -e "${PROPER_NAME,,}" | tr -d '[:space:]')"
+
+	SUBSTITUTE=( "\${SERVICE_DOMAIN}" )
+	basic_nginx
+
+	mastodon_postgres_dockerbunker
+	mastodon_redis_dockerbunker
+	mastodon_dbmigrateandprecompileassets_dockerbunker
+
+	docker_run_all
+
+	post_setup_routine
+
+	echo -e "\nAfter signing up on ${SERVICE_DOMAIN} make your user an admin by running\n\n\
+${SERVICES_DIR}/${SERVICE_NAME}/./make_admin.sh username\n"
+
+}
+
+if [[ $1 == "letsencrypt" ]];then
+	$1 $*
+else
+	$1
+fi

+ 85 - 0
data/services/mastodon/nginx/mastodon.conf

@@ -0,0 +1,85 @@
+##
+# You should look at the following URL's in order to grasp a solid understanding
+# of Nginx configuration files in order to fully unleash the power of Nginx.
+# http://wiki.nginx.org/Pitfalls
+# http://wiki.nginx.org/QuickStart
+# http://wiki.nginx.org/Configuration
+#
+# Generally, you will want to move this file somewhere, and start with a clean
+# file but keep this around for reference. Or just disable in sites-enabled.
+#
+# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples.
+##
+
+# Default server configuration
+#
+upstream mastodon {
+ server mastodon-service-dockerbunker:3000;
+}
+
+upstream mastodon-streaming {
+ server mastodon-streaming-dockerbunker:4000;
+}
+
+server {
+    listen 80;
+	server_name ${SERVICE_DOMAIN};
+    return 301 https://$host$request_uri;
+	add_header X-Content-Type-Options "nosniff" always;
+	add_header X-XSS-Protection "1; mode=block" always;
+	add_header X-Frame-Options "DENY" always;
+	add_header Referrer-Policy "strict-origin" always;
+	add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
+	server_tokens off;
+}
+
+server {
+    listen 443;
+	server_name ${SERVICE_DOMAIN};
+    ssl on;
+	ssl_certificate /etc/nginx/ssl/${SERVICE_DOMAIN}/cert.pem;
+	ssl_certificate_key /etc/nginx/ssl/${SERVICE_DOMAIN}/key.pem;
+	include /etc/nginx/includes/ssl.conf;
+
+	add_header X-Content-Type-Options "nosniff" always;
+	add_header X-XSS-Protection "1; mode=block" always;
+	add_header X-Frame-Options "DENY" always;
+	add_header Referrer-Policy "strict-origin" always;
+	add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
+	server_tokens off;
+
+	include /etc/nginx/includes/gzip.conf;
+
+    location / {
+        proxy_pass http://mastodon/;
+		proxy_set_header  Host              $http_host;   # required for docker client's sake
+		proxy_set_header  X-Real-IP         $remote_addr; # pass on real client's IP
+		proxy_set_header  X-Forwarded-For   $proxy_add_x_forwarded_for;
+		proxy_set_header  X-Forwarded-Proto $scheme;
+		proxy_read_timeout                  900;
+    }
+
+location /api/v1/streaming {
+    proxy_set_header Host $host;
+    proxy_set_header X-Real-IP $remote_addr;
+    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+    proxy_set_header X-Forwarded-Proto https;
+    proxy_set_header Proxy "";
+
+    proxy_pass http://mastodon-streaming/;
+    proxy_buffering off;
+    proxy_redirect off;
+    proxy_http_version 1.1;
+#    proxy_set_header Upgrade $http_upgrade;
+#    proxy_set_header Connection $connection_upgrade;
+
+    tcp_nodelay on;
+  }
+
+	location ~ /.well-known {
+        allow all;
+		root /var/www/html;
+	}
+}
+
+

+ 29 - 0
data/services/nextcloud/containers.sh

@@ -0,0 +1,29 @@
+nextcloud_db_dockerbunker() {
+	docker run -d \
+		--name=${FUNCNAME[0]//_/-} \
+		--restart=always \
+		--network dockerbunker-${SERVICE_NAME} --net-alias=db \
+		--env-file="${SERVICE_ENV}" \
+		-v nextcloud-db-vol-1:/var/lib/mysql \
+		-v "${SERVICES_DIR}"/${SERVICE_NAME}/mysql/:/etc/mysql/conf.d/:ro \
+		--health-cmd="mysqladmin ping --host localhost --silent" --health-interval=10s --health-retries=5 --health-timeout=30s \
+	${IMAGES[db]} >/dev/null
+
+	wait_for_db
+}
+
+nextcloud_service_dockerbunker() {
+	docker run -d \
+		--name=${FUNCNAME[0]//_/-} \
+		--restart=always \
+		--network dockerbunker-${SERVICE_NAME} \
+		--env-file="${SERVICE_ENV}" \
+		-v nextcloud-data-vol-1:/var/www/html \
+		-v nextcloud-data-vol-2:/var/www/html/custom_apps \
+		-v nextcloud-data-vol-3:/var/www/html/config \
+		-v nextcloud-data-vol-4:/var/www/html/data \
+	${IMAGES[service]} >/dev/null
+}
+
+
+

+ 98 - 0
data/services/nextcloud/nextcloud.sh

@@ -0,0 +1,98 @@
+#!/usr/bin/env bash
+
+while true;do ls | grep -q dockerbunker.sh;if [[ $? == 0 ]];then BASE_DIR=$PWD;break;else cd ../;fi;done
+
+PROPER_NAME="Nextcloud"
+SERVICE_NAME="$(echo -e "${PROPER_NAME,,}" | tr -d '[:space:]')"
+PROMPT_SSL=1
+
+declare -a environment=( "data/env/dockerbunker.env" "data/include/init.sh" )
+
+for env in "${environment[@]}";do
+	[[ -f "${BASE_DIR}"/$env ]] && source "${BASE_DIR}"/$env
+done
+
+declare -A WEB_SERVICES
+declare -a containers=( "${SERVICE_NAME}-service-dockerbunker" "${SERVICE_NAME}-db-dockerbunker" )
+declare -a add_to_network=( "${SERVICE_NAME}-service-dockerbunker" )
+declare -A IMAGES=( [service]="nextcloud:stable" [db]="mariadb:10.2" )
+declare -a volumes=( "${SERVICE_NAME}-data-vol-1" "${SERVICE_NAME}-data-vol-2" "${SERVICE_NAME}-data-vol-3" "${SERVICE_NAME}-data-vol-4" "${SERVICE_NAME}-db-vol-1" )
+declare -a networks=( "dockerbunker-${SERVICE_NAME}" )
+
+[[ -z $1 ]] && options_menu
+
+configure() {
+	pre_configure_routine
+	
+	echo -e "# \e[4mNextcloud Settings\e[0m"
+
+	set_domain
+	
+	unset NEXTCLOUD_ADMIN_USER
+	if [ "$NEXTCLOUD_ADMIN_USER" ]; then
+	  read -p "Nextcloud Admin User: " -ei "$NEXTCLOUD_ADMIN_USER" NEXTCLOUD_ADMIN_USER
+	else
+		while [[ -z $NEXTCLOUD_ADMIN_USER || $NEXTCLOUD_ADMIN_USER == "admin" ]];do
+			read -p "Nextcloud Admin User: " -ei "$NEXTCLOUD_ADMIN_USER" NEXTCLOUD_ADMIN_USER
+			[[ ${NEXTCLOUD_ADMIN_USER} == "admin" ]] && echo -e "\n\e[31mAdmin account setting is invalid: name is reserved [name: admin]\e[0m\n"
+		done
+	fi
+	
+	unset NEXTCLOUD_ADMIN_PASSWORD
+	while [[ "${#NEXTCLOUD_ADMIN_PASSWORD}" -le 6 || "$NEXTCLOUD_ADMIN_PASSWORD" != *[A-Z]* || "$NEXTCLOUD_ADMIN_PASSWORD" != *[a-z]* || "$NEXTCLOUD_ADMIN_PASSWORD" != *[0-9]* ]];do
+		if [ $VALIDATE ];then
+			echo -e "\n\e[31m  Password does not meet requirements\e[0m"
+		fi
+			stty_orig=$(stty -g)
+			stty -echo
+	  		read -p " $(printf "\n   \e[4mPassword requirements\e[0m\n   Minimum Length 6,Uppercase, Lowercase, Integer\n\n   Enter Password:") " -ei "" NEXTCLOUD_ADMIN_PASSWORD
+			stty "$stty_orig"
+			echo ""
+		VALIDATE=1
+	done
+	unset VALIDATE
+	echo ""
+
+	# avoid tr illegal byte sequence in macOS when generating random strings
+	if [[ $OSTYPE =~ "darwin" ]];then
+		if [[ $LC_ALL ]];then
+			oldLC_ALL=$LC_ALL
+			export LC_ALL=C
+		else
+			export LC_ALL=C
+		fi
+	fi
+	cat <<-EOF >> "${SERVICE_ENV}"
+	PROPER_NAME=${PROPER_NAME}
+	SERVICE_NAME=${SERVICE_NAME}
+	SSL_CHOICE=${SSL_CHOICE}
+	LE_EMAIL=${LE_EMAIL}
+
+	SERVICE_DOMAIN=${SERVICE_DOMAIN}
+	NEXTCLOUD_ADMIN_USER=${NEXTCLOUD_ADMIN_USER}
+	NEXTCLOUD_ADMIN_PASSWORD=${NEXTCLOUD_ADMIN_PASSWORD}
+
+	# ------------------------------
+	# SQL database configuration
+	# ------------------------------
+
+	MYSQL_DATABASE=nextcloud
+	MYSQL_USER=nextcloud
+	MYSQL_HOST=db
+	
+	# Please use long, random alphanumeric strings (A-Za-z0-9)
+	MYSQL_PASSWORD=$(tr -dc 'a-zA-Z0-9' < /dev/urandom | head -c 28)
+	MYSQL_ROOT_PASSWORD=$(tr -dc 'a-zA-Z0-9' < /dev/urandom | head -c 28)
+	EOF
+	if [[ $OSTYPE =~ "darwin" ]];then
+		[[ $oldLC_ALL ]] && export LC_ALL=$oldLC_ALL || unset LC_ALL
+	fi
+
+	post_configure_routine
+}
+
+if [[ $1 == "letsencrypt" ]];then
+	$1 $*
+else
+	$1
+fi

+ 64 - 0
data/services/nextcloud/nginx/nextcloud.conf

@@ -0,0 +1,64 @@
+##
+# You should look at the following URL's in order to grasp a solid understanding
+# of Nginx configuration files in order to fully unleash the power of Nginx.
+# http://wiki.nginx.org/Pitfalls
+# http://wiki.nginx.org/QuickStart
+# http://wiki.nginx.org/Configuration
+#
+# Generally, you will want to move this file somewhere, and start with a clean
+# file but keep this around for reference. Or just disable in sites-enabled.
+#
+# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples.
+##
+
+# Default server configuration
+#
+upstream nextcloud {
+ server nextcloud-service-dockerbunker:80;
+}
+
+server {
+    listen 80;
+	server_name ${SERVICE_DOMAIN};
+    return 301 https://$host$request_uri;
+	add_header X-Content-Type-Options "nosniff" always;
+	add_header X-XSS-Protection "1; mode=block" always;
+	add_header X-Frame-Options "DENY" always;
+	add_header Referrer-Policy "strict-origin" always;
+	add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
+	server_tokens off;
+}
+
+server {
+    listen 443;
+	server_name ${SERVICE_DOMAIN};
+    ssl on;
+	ssl_certificate /etc/nginx/ssl/${SERVICE_DOMAIN}/cert.pem;
+	ssl_certificate_key /etc/nginx/ssl/${SERVICE_DOMAIN}/key.pem;
+	include /etc/nginx/includes/ssl.conf;
+
+	add_header X-Content-Type-Options "nosniff" always;
+	add_header X-XSS-Protection "1; mode=block" always;
+	add_header X-Frame-Options "DENY" always;
+	add_header Referrer-Policy "strict-origin" always;
+	add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
+	server_tokens off;
+
+	include /etc/nginx/includes/gzip.conf;
+
+    location / {
+        proxy_pass http://nextcloud/;
+		proxy_set_header  Host              $http_host;   # required for docker client's sake
+		proxy_set_header  X-Real-IP         $remote_addr; # pass on real client's IP
+		proxy_set_header  X-Forwarded-For   $proxy_add_x_forwarded_for;
+		proxy_set_header  X-Forwarded-Proto $scheme;
+		proxy_read_timeout                  900;
+    }
+
+	location ~ /.well-known {
+        allow all;
+		root /var/www/html;
+	}
+}
+
+

+ 3 - 0
data/services/nginx/basic_auth.conf

@@ -0,0 +1,3 @@
+auth_basic            "Restricted Area";
+auth_basic_user_file  /etc/nginx/conf.d/${SERVICE_DOMAIN}/.htpasswd;
+

+ 17 - 0
data/services/nginx/containers.sh

@@ -0,0 +1,17 @@
+nginx_dockerbunker() {
+	echo -en "\n\e[1mStarting up nginx container\e[0m"
+	docker run -d \
+		--name=${NGINX_CONTAINER} \
+		--restart=always \
+		--net=${NETWORK} --net-alias=nginx \
+		-p 80:80 -p 443:443 \
+		-v "${BASE_DIR}/data/web":/var/www/html:ro \
+		-v "${SERVICES_DIR}/nginx/nginx.conf":/etc/nginx/nginx.conf:ro \
+		-v "${CONF_DIR}/nginx/ssl":/etc/nginx/ssl \
+		-v "${CONF_DIR}/nginx/conf.d":/etc/nginx/conf.d \
+		-v "${SERVICES_DIR}/nginx/ssl/dhparam.pem":/etc/nginx/ssl/dhparam.pem:ro \
+		-v "${SERVICES_DIR}/nginx/includes":/etc/nginx/includes \
+	${IMAGES[service]} >/dev/null
+	exit_response
+}
+

+ 39 - 0
data/services/nginx/ghost.chaosbunker.me.conf

@@ -0,0 +1,39 @@
+upstream ghost {
+ server ghost-service-dockerbunker:2368;
+}
+
+server {
+    listen 80;
+	server_name ${SERVICE_DOMAIN};
+    return 301 https://$host$request_uri;
+}
+
+server {
+    listen 443;
+	server_name ${SERVICE_DOMAIN};
+    ssl on;
+	ssl_certificate /etc/nginx/ssl/${SERVICE_DOMAIN}/cert.pem;
+	ssl_certificate_key /etc/nginx/ssl/${SERVICE_DOMAIN}/key.pem;
+	include /etc/nginx/includes/ssl.conf;
+
+    add_header Strict-Transport-Security "max-age=15768000; includeSubDomains";
+	add_header X-Frame-Options DENY;
+	add_header X-Content-Type-Options nosniff;
+
+	include /etc/nginx/includes/gzip.conf;
+
+    location / {
+        proxy_pass http://ghost/;
+		proxy_set_header  Host              $http_host;   # required for docker client's sake
+		proxy_set_header  X-Real-IP         $remote_addr; # pass on real client's IP
+		proxy_set_header  X-Forwarded-For   $proxy_add_x_forwarded_for;
+		proxy_set_header  X-Forwarded-Proto $scheme;
+		proxy_read_timeout                  900;
+    }
+
+	location ~ /.well-known {
+        allow all;
+		root /var/www/html;
+	}
+}
+

+ 25 - 0
data/services/nginx/includes/gzip.conf

@@ -0,0 +1,25 @@
+gzip                    on;
+gzip_http_version       1.0;
+gzip_min_length         1100;
+gzip_buffers            4 8k;
+gzip_types
+    # text/html is always compressed by HttpGzipModule
+    text/css
+    text/javascript
+    text/xml
+    text/plain
+    text/x-component
+    application/javascript
+    application/x-javascript
+    application/json
+    application/xml
+    application/rss+xml
+    application/atom+xml
+    font/truetype
+    font/opentype
+    application/vnd.ms-fontobject
+    image/svg+xml;
+gzip_proxied            expired no-cache no-store private auth;
+gzip_disable            "msie6";
+gzip_vary               on;
+gzip_comp_level         1;

+ 9 - 0
data/services/nginx/includes/ssl.conf

@@ -0,0 +1,9 @@
+ssl_session_cache shared:SSL:10m;
+ssl_session_timeout 10m;
+ssl_session_tickets off;
+ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
+ssl_prefer_server_ciphers on;
+ssl_ciphers 'EDH+CAMELLIA:EDH+aRSA:EECDH+aRSA+AESGCM:EECDH+aRSA+SHA256:EECDH:+CAMELLIA128:+AES128:+SSLv3:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!DSS:!RC4:!SEED:!IDEA:!ECDSA:kEDH:CAMELLIA128-SHA:AES128-SHA';
+ssl_dhparam /etc/nginx/ssl/dhparam.pem;
+ssl_ecdh_curve secp384r1;
+

+ 34 - 0
data/services/nginx/nginx.conf

@@ -0,0 +1,34 @@
+user  nginx;
+worker_processes 1;
+
+error_log /var/log/nginx/error.log warn;
+pid /var/run/nginx.pid;
+
+
+events {
+	worker_connections  1024;
+}
+
+
+http {
+	include /etc/nginx/mime.types;
+	default_type application/octet-stream;
+
+	log_format main '$remote_addr - $remote_user [$time_local] "$request" '
+					'$status $body_bytes_sent "$http_referer" '
+					'"$http_user_agent" "$http_x_forwarded_for"';
+
+	access_log /var/log/nginx/access.log  main;
+
+	sendfile on;
+	#tcp_nopush on;
+
+	client_max_body_size 50m;
+
+	keepalive_timeout 65;
+
+	#gzip on;
+
+	include /etc/nginx/conf.d/*.conf;
+
+}

+ 45 - 0
data/services/nginx/nginx.sh

@@ -0,0 +1,45 @@
+while true;do ls | grep -q dockerbunker.sh;if [[ $? == 0 ]];then BASE_DIR=$PWD;break;else cd ../;fi;done
+
+PROPER_NAME="Nginx"
+SERVICE_NAME="$(echo -e "${PROPER_NAME,,}" | tr -d '[:space:]')"
+
+declare -a environment=( "data/env/dockerbunker.env" "data/include/init.sh" )
+
+for env in "${environment[@]}";do
+	[[ -f "${BASE_DIR}"/$env ]] && source "${BASE_DIR}"/$env
+done
+
+declare -a containers=( "${SERVICE_NAME}-dockerbunker" )
+declare -a networks=( "dockerbunker-network" )
+declare -A IMAGES=( [service]="nginx:mainline-alpine" )
+
+setup() {
+	source "${ENV_DIR}"/dockerbunker.env
+
+	echo -e "\n\e[1mNo nginx container found\e[0m"
+	echo -e "\n\e[3m\xe2\x86\x92 Setup nginx\e[0m"
+	docker_pull
+
+	[[ ! $(docker network ls -q --filter name=^${NETWORK}$) ]] \
+		&& docker network create $NETWORK >/dev/null \
+
+	[[ ! -d "${BASE_DIR}"/data/web ]] && mkdir "${BASE_DIR}"/data/web
+
+	docker_run nginx_dockerbunker
+}
+
+destroy_service() {
+	stop_containers
+	remove_containers
+	remove_networks
+
+	[[ -f "${ENV_DIR}"/mx.env ]] \
+		&& rm "${ENV_DIR}"/mx.env
+	
+	[[ -f "${ENV_DIR}"/dockerbunker.env ]] \
+		&& rm "${ENV_DIR}"/dockerbunker.env
+}
+
+$1
+
+

+ 13 - 0
data/services/nginx/ssl/dhparam.pem

@@ -0,0 +1,13 @@
+-----BEGIN DH PARAMETERS-----
+MIICCAKCAgEAys6il1TPpUlc4jZ+PG5VWxbJiFPwyus7w58ywBxDtDxTZszHegcy
+NCalmYkvrHxayVPLUhUOZwh8IVvPyKpAfo7x79LtUDrNDnTH6sLPY37ikFYNUKmt
+SB8u/NGckTdqKtyXuliMndp/SXlCiMSSVaxFEyYHyk5+/CXHqHpxoHtBIMWA21tz
+Fp5sEP1ZOwlE2YeBDvt4+lYEMu9b6M/jPqNuCSwymJ8Df6yics1ESaMu/6bOcTuI
+RtK7NTLY7nmqwl+MS24ewRsGXAfUa2wRVFYHVYb07yMP0YTGHEwBGos/SltMV0Wu
+P4pVVLOjWLn51qsZF8zlEGvk8i2sxzqQmklbg0j470cMWZiEMDh+TuCCnoDJuo+5
+gh8pDZsvw9PhtVLUTJlrnoEg1Nd2leHebFU74LGVsqVhqL1PgZC1nDefOD2LUhSs
+mJXvtA0NuvapaPSEJmqBzEFAA2b86TGIbzkst6mjQ16sNX34qNtCfSfh9OswjKKD
+jf3uJa2tZVcEoQpDychFEKP92UETpfMBBHfByXGbymi3YusvWkLoTkvW55aZNzOL
+RnQkgEZPNcE+6oF02Pd5nC7Ly9Spc1fwTj6itpuurwz0teTWTRLXy4kzhFYLEZCH
+EGtPZ+PTAhIzIO+y4oQh0axvHfMMEul2FrlnV51PBSS69R7nqK8ofzMCAQI=
+-----END DH PARAMETERS-----

+ 11 - 0
data/services/openproject/containers.sh

@@ -0,0 +1,11 @@
+openproject_service_dockerbunker() {
+	docker run -d \
+		--name=${FUNCNAME[0]//_/-} \
+		--restart=always \
+		--env-file "${ENV_DIR}"/${SERVICE_SPECIFIC_MX}mx.env \
+		--env-file "${SERVICE_ENV}" \
+		-v openproject-pgdata-vol-1:/var/lib/postgresql/9.4/main \
+		-v openproject-logs-vol-1:/var/log/supervisor \
+		-v openproject-data-vol-1:/var/db/openproject \
+	${IMAGES[service]} >/dev/null
+}

+ 55 - 0
data/services/openproject/nginx/openproject.conf

@@ -0,0 +1,55 @@
+##
+# You should look at the following URL's in order to grasp a solid understanding
+# of Nginx configuration files in order to fully unleash the power of Nginx.
+# http://wiki.nginx.org/Pitfalls
+# http://wiki.nginx.org/QuickStart
+# http://wiki.nginx.org/Configuration
+#
+# Generally, you will want to move this file somewhere, and start with a clean
+# file but keep this around for reference. Or just disable in sites-enabled.
+#
+# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples.
+##
+
+# Default server configuration
+#
+upstream openproject {
+ server openproject-service-dockerbunker:80;
+}
+
+server {
+    listen 80;
+	server_name ${SERVICE_DOMAIN};
+    return 301 https://$host$request_uri;
+}
+
+server {
+    listen 443;
+	server_name ${SERVICE_DOMAIN};
+    ssl on;
+	ssl_certificate /etc/nginx/ssl/${SERVICE_DOMAIN}/cert.pem;
+	ssl_certificate_key /etc/nginx/ssl/${SERVICE_DOMAIN}/key.pem;
+	include /etc/nginx/includes/ssl.conf;
+
+    add_header Strict-Transport-Security "max-age=15768000; includeSubDomains";
+	add_header X-Frame-Options DENY;
+	add_header X-Content-Type-Options nosniff;
+
+	include /etc/nginx/includes/gzip.conf;
+
+    location / {
+        proxy_pass http://openproject/;
+		proxy_set_header  Host              $http_host;   # required for docker client's sake
+		proxy_set_header  X-Real-IP         $remote_addr; # pass on real client's IP
+		proxy_set_header  X-Forwarded-For   $proxy_add_x_forwarded_for;
+		proxy_set_header  X-Forwarded-Proto $scheme;
+		proxy_read_timeout                  900;
+    }
+
+	location ~ /.well-known {
+        allow all;
+		root /var/www/html;
+	}
+}
+
+

+ 77 - 0
data/services/openproject/openproject.sh

@@ -0,0 +1,77 @@
+#!/usr/bin/env bash
+
+while true;do ls | grep -q dockerbunker.sh;if [[ $? == 0 ]];then BASE_DIR=$PWD;break;else cd ../;fi;done
+
+PROPER_NAME="Open Project"
+SERVICE_NAME="$(echo -e "${PROPER_NAME,,}" | tr -d '[:space:]')"
+PROMPT_SSL=1
+safe_to_keep_volumes_when_reconfiguring=1
+
+declare -a environment=( "data/env/dockerbunker.env" "data/include/init.sh" )
+
+for env in "${environment[@]}";do
+	[[ -f "${BASE_DIR}/$env" ]] && source "${BASE_DIR}/$env"
+done
+
+declare -A WEB_SERVICES
+declare -a containers=( "openproject-service-dockerbunker" )
+declare -a volumes=( "openproject-data-vol-1" "openproject-pgdata-vol-1" "openproject-logs-vol-1" )
+declare -a add_to_network=( "openproject-service-dockerbunker" )
+declare -a networks=( )
+declare -A IMAGES=( [service]="openproject/community" )
+
+[[ -z $1 ]] && options_menu
+
+configure() {
+	pre_configure_routine
+
+	echo -e "# \e[4mOpen Project Settings\e[0m"
+
+	set_domain
+
+	configure_mx
+
+	cat <<-EOF >> "${SERVICE_ENV}"
+	PROPER_NAME="${PROPER_NAME}"
+	SERVICE_NAME="${SERVICE_NAME}"
+	SSL_CHOICE=${SSL_CHOICE}
+	LE_EMAIL=${LE_EMAIL}
+	SERVICE_SPECIFIC_MX=${SERVICE_SPECIFIC_MX}
+	
+	# ------------------------------
+	# General Settings
+	# ------------------------------
+	
+	SERVICE_DOMAIN=${SERVICE_DOMAIN}
+	
+	# ------------------------------
+	# Open Project Settings
+	# ------------------------------
+	
+	SECRET_KEY_BASE=$(</dev/urandom tr -dc A-Za-z0-9 | head -c 28)
+	
+	# ------------------------------
+	# E-Mail Server Settings
+	# ------------------------------
+	
+	EMAIL_DELIVERY_METHOD=smtp
+	SMTP_AUTHENTICATION=login
+	SMTP_PORT=587
+	SMTP_ENABLE_STARTTLS_AUTO=true
+	SMTP_ADDRESS=${MX_DOMAIN}
+	SMTP_USER_NAME=${MX_EMAIL}
+	SMTP_PASSWORD=${MX_PASSWORD}
+	SMTP_DOMAIN=${MX_DOMAIN}
+	EOF
+
+	post_configure_routine
+}
+
+if [[ $1 == "letsencrypt" ]];then
+	$1 $*
+else
+	$1
+fi
+
+
+

+ 23 - 0
data/services/piwik/containers.sh

@@ -0,0 +1,23 @@
+piwik_db_dockerbunker() {
+	docker run -d \
+		--name=${FUNCNAME[0]//_/-} \
+		--restart=always \
+		--network dockerbunker-${SERVICE_NAME} --net-alias=db \
+		--env-file="${SERVICE_ENV}"\
+		-v ${SERVICE_NAME}-db-vol-1:/var/lib/mysql \
+		-v "${SERVICES_DIR}"/${SERVICE_NAME}/mysql/:/etc/mysql/conf.d/:ro \
+		--health-cmd="mysqladmin ping --host localhost --silent" --health-interval=10s --health-retries=5 --health-timeout=30s \
+	${IMAGES[db]} >/dev/null
+
+	wait_for_db ${FUNCNAME[0]//_/-}
+}
+
+piwik_service_dockerbunker() {
+	docker run -d \
+		--name=${SERVICE_NAME}-service-dockerbunker \
+		--restart=always \
+		--network dockerbunker-piwik \
+		-v ${SERVICE_NAME}-data-vol-1:/var/www/app/data \
+		-v ${SERVICE_NAME}-data-vol-2:/var/www/app/plugins \
+	${IMAGES[service]} >/dev/null
+}

+ 57 - 0
data/services/piwik/nginx/piwik.conf

@@ -0,0 +1,57 @@
+map $sent_http_content_type $expires {
+	default                    off;
+	text/html                  epoch;
+	text/css                   max;
+	application/javascript     max;
+	~image/                    max;
+}
+
+upstream piwik {
+	server piwik-service-dockerbunker:80;
+}
+
+server {
+	listen 80;
+	server_name ${SERVICE_DOMAIN};
+	return 301 https://$host$request_uri;
+}
+
+server {
+
+    listen 443;
+	server_name ${SERVICE_DOMAIN};
+    ssl on;
+	ssl_certificate /etc/nginx/ssl/${SERVICE_DOMAIN}/cert.pem;
+	ssl_certificate_key /etc/nginx/ssl/${SERVICE_DOMAIN}/key.pem;
+	include /etc/nginx/includes/ssl.conf;
+
+	add_header Strict-Transport-Security "max-age=15768000; includeSubDomains";
+	add_header X-Frame-Options SAMEORIGIN;
+	add_header X-XSS-Protection "1; mode=block";
+
+	proxy_set_header X-Real-IP $remote_addr;
+	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+	proxy_set_header Host $http_host; 
+
+	include /etc/nginx/includes/gzip.conf;
+
+	location / {
+		proxy_pass http://piwik/;
+	}
+
+	expires $expires;
+
+	location = /favicon.ico {
+		log_not_found off;
+		access_log off;
+	}
+
+	location ^~ /.well-known/ {
+	    access_log           off;
+	    log_not_found        off;
+	    root                 /var/www/html;
+#	    autoindex            off;
+	    index                index.html; # "no-such-file.txt",if expected protos don't need it
+	    try_files            $uri $uri/ =404;
+	}
+}

+ 74 - 0
data/services/piwik/piwik.sh

@@ -0,0 +1,74 @@
+#!/usr/bin/env bash
+
+while true;do ls | grep -q dockerbunker.sh;if [[ $? == 0 ]];then BASE_DIR=$PWD;break;else cd ../;fi;done
+
+PROPER_NAME="Piwik"
+SERVICE_NAME="$(echo -e "${PROPER_NAME,,}" | tr -d '[:space:]')"
+PROMPT_SSL=1
+
+declare -a environment=( "data/env/dockerbunker.env" "data/include/init.sh" )
+
+for env in "${environment[@]}";do
+	[[ -f "${BASE_DIR}"/$env ]] && source "${BASE_DIR}"/$env
+done
+
+declare -A WEB_SERVICES
+declare -a containers=( "${SERVICE_NAME}-service-dockerbunker" "${SERVICE_NAME}-db-dockerbunker" )
+declare -a volumes=( "${SERVICE_NAME}-data-vol-1" "${SERVICE_NAME}-data-vol-2" "${SERVICE_NAME}-db-vol-1" )
+declare -a add_to_network=( "${SERVICE_NAME}-service-dockerbunker" )
+declare -a networks=( "dockerbunker-${SERVICE_NAME}" )
+declare -A IMAGES=( [service]="piwik" [db]="mariadb:10.2" )
+
+[[ -z $1 ]] && options_menu
+
+configure() {
+	pre_configure_routine
+
+	echo -e "# \e[4mPiwik Settings\e[0m"
+	set_domain
+	
+	# avoid tr illegal byte sequence in macOS when generating random strings
+	if [[ $OSTYPE =~ "darwin" ]];then
+		if [[ $LC_ALL ]];then
+			oldLC_ALL=$LC_ALL
+			export LC_ALL=C
+		else
+			export LC_ALL=C
+		fi
+	fi
+	cat <<-EOF >> ${SERVICE_ENV}
+	SSL_CHOICE=${SSL_CHOICE}
+	LE_EMAIL=${LE_EMAIL}
+
+	# ------------------------------
+	# General Settings
+	# ------------------------------
+	
+	SERVICE_DOMAIN=${SERVICE_DOMAIN}
+	
+	# ------------------------------
+	# Piwik SQL database configuration
+	# ------------------------------
+	
+	MYSQL_DATABASE=piwik
+	MYSQL_USER=piwik
+	
+	# Please use long, random alphanumeric strings (A-Za-z0-9)
+	MYSQL_PASSWORD=$(tr -dc 'a-zA-Z0-9' < /dev/urandom | head -c 28)
+	MYSQL_ROOT_PASSWORD=$(tr -dc 'a-zA-Z0-9' < /dev/urandom | head -c 28)
+	EOF
+	
+	if [[ $OSTYPE =~ "darwin" ]];then
+		[[ $oldLC_ALL ]] && export LC_ALL=$oldLC_ALL || unset LC_ALL
+	fi
+
+	post_configure_routine
+}
+
+# i think this can/should go now... if it goes, change tests in letsencrypt function (\$1, \$2 \$* etc)
+if [[ $1 == "letsencrypt" ]];then
+	$1 $*
+else
+	$1
+fi
+

+ 6 - 0
data/services/seafilepro/README.md

@@ -0,0 +1,6 @@
+Seafile Pro install is interactive and happens during setup. Make sure to
+	- enter `db` as mysql server host
+	- use port 3306 for the mysql host
+	- enter the mysql root password, seafile db user 'seafile' and use the corresponding password as set in `data/env/seafilepro.env` - the passwords can optionally be displayed in the shell during setup
+	- If you have a valid `seafile-license.txt`, make sure to place it in `data/services/seafilepro/seafile-license.txt` before running setup for Seafile Pro. Without a license you are limited to 3 users
+

+ 46 - 0
data/services/seafilepro/containers.sh

@@ -0,0 +1,46 @@
+seafilepro_db_dockerbunker() {
+	docker run  -d \
+		--name=${FUNCNAME[0]//_/-} \
+		--restart=always \
+		--network dockerbunker-${SERVICE_NAME} \
+		--net-alias=db \
+		-v seafilepro-db-vol-1:/var/lib/mysql \
+		--env MYSQL_ROOT_PASSWORD=${DBROOT} \
+		--env MYSQL_USER=${DBUSER} \
+		--env MYSQL_PASSWORD=${DBPASS} \
+	${IMAGES[db]} >/dev/null
+
+	if [[ -z $keep_volumes ]];then
+		if ! docker exec seafilepro-db-dockerbunker mysqladmin ping -h"127.0.0.1" --silent;then
+			echo -en "Waiting for Seafile DB to be ready..."
+			while ! docker exec seafilepro-db-dockerbunker mysqladmin ping -h"127.0.0.1" --silent;do
+				sleep 3
+			done
+			if [ $? != 1 ];then
+				echo -e " \e[32m\xE2\x9C\x94\e[0m"
+			else
+				echo  -e " \e[31mfailed\e[0m"
+			fi
+		fi
+	fi
+}
+
+seafilepro_setup_dockerbunker() {
+	docker run -it --rm \
+		--name=${FUNCNAME[0]//_/-} \
+		--network=dockerbunker-${SERVICE_NAME} \
+		-v seafilepro-data-vol-1:/seafile \
+		-v "${BASE_DIR}"/data/services/seafilepro/seafile-license.txt \
+	${IMAGES[service]} $1
+}
+
+seafilepro_service_dockerbunker() {
+	docker run -d \
+		--name=${FUNCNAME[0]//_/-} \
+		--restart=always \
+		--network ${NETWORK} \
+		--network dockerbunker-seafilepro \
+		-v seafilepro-data-vol-1:/seafile \
+		-v "${BASE_DIR}"/data/services/seafilepro/seafile-license.txt \
+	${IMAGES[service]} >/dev/null
+}

+ 92 - 0
data/services/seafilepro/nginx/seafilepro.conf

@@ -0,0 +1,92 @@
+##
+# You should look at the following URL's in order to grasp a solid understanding
+# of Nginx configuration files in order to fully unleash the power of Nginx.
+# http://wiki.nginx.org/Pitfalls
+# http://wiki.nginx.org/QuickStart
+# http://wiki.nginx.org/Configuration
+#
+# Generally, you will want to move this file somewhere, and start with a clean
+# file but keep this around for reference. Or just disable in sites-enabled.
+#
+# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples.
+##
+
+# Default server configuration
+#
+
+map $sent_http_content_type $expires {
+	default                    off;
+	text/html                  epoch;
+	text/css                   max;
+	application/javascript     max;
+	~image/                    max;
+}
+
+upstream seafilepro {
+ server seafilepro-service-dockerbunker:80;
+}
+
+server {
+	listen 80;
+	server_name ${SERVICE_DOMAIN};
+	return 301 https://$host$request_uri;
+	add_header X-Content-Type-Options "nosniff" always;
+	add_header X-XSS-Protection "1; mode=block" always;
+	add_header X-Frame-Options "DENY" always;
+	add_header Referrer-Policy "strict-origin" always;
+	add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
+	server_tokens off;
+}
+
+server {
+	listen 443;
+	server_name ${SERVICE_DOMAIN};
+	ssl on;
+	ssl_certificate /etc/nginx/ssl/${SERVICE_DOMAIN}/cert.pem;
+	ssl_certificate_key /etc/nginx/ssl/${SERVICE_DOMAIN}/key.pem;
+	include /etc/nginx/includes/ssl.conf;
+
+	add_header X-Content-Type-Options "nosniff" always;
+	add_header X-XSS-Protection "1; mode=block" always;
+	add_header X-Frame-Options "DENY" always;
+	add_header Referrer-Policy "strict-origin" always;
+	add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
+	
+
+	include /etc/nginx/includes/gzip.conf;
+
+	location / {
+		proxy_pass http://seafilepro/;
+		proxy_set_header   Host $host;
+		proxy_set_header   X-Real-IP $remote_addr;
+		proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
+		proxy_set_header   X-Forwarded-Host $server_name;
+		proxy_set_header   X-Forwarded-Proto https;
+		proxy_request_buffering off;
+		
+		access_log      /var/log/nginx/dav.access.log;
+		error_log       /var/log/nginx/dav.error.log;
+		
+		proxy_read_timeout  1200s;
+		
+		client_max_body_size 0;
+	}
+
+expires $expires;
+
+# Deny all attempts to access hidden files such as .htaccess, .htpasswd, .DS_Store (Mac).
+	location ~ /\. {
+			deny all;
+			access_log off;
+			log_not_found off;
+	}
+
+	location ^~ /.well-known/ {
+	    access_log           off;
+	    log_not_found        off;
+	    root                 /var/www/html;
+#	    autoindex            off;
+	    index                index.html; # "no-such-file.txt",if expected protos don't need it
+	    try_files            $uri $uri/ =404;
+	}
+}

+ 0 - 0
data/services/seafilepro/seafile-license.txt


+ 121 - 0
data/services/seafilepro/seafilepro.sh

@@ -0,0 +1,121 @@
+#!/usr/bin/env bash
+
+while true;do ls | grep -q dockerbunker.sh;if [[ $? == 0 ]];then BASE_DIR=$PWD;break;else cd ../;fi;done
+
+PROPER_NAME="Seafile Pro"
+SERVICE_NAME="$(echo -e "${PROPER_NAME,,}" | tr -d '[:space:]')"
+PROMPT_SSL=true
+safe_to_keep_volumes_when_reconfiguring=1
+
+declare -a environment=( "data/env/dockerbunker.env" "data/include/init.sh" )
+
+for env in "${environment[@]}";do
+	[[ -f "${BASE_DIR}"/$env ]] && source "${BASE_DIR}"/$env
+done
+
+declare -A WEB_SERVICES
+declare -a containers=( "${SERVICE_NAME}-db-dockerbunker" "${SERVICE_NAME}-service-dockerbunker" )
+declare -a add_to_network=( "${SERVICE_NAME}-service-dockerbunker" )
+declare -a volumes=( "${SERVICE_NAME}-db-vol-1" "${SERVICE_NAME}-data-vol-1" )
+declare -a networks=( "dockerbunker-${SERVICE_NAME}" )
+declare -A IMAGES=( [db]="mariadb:10.2" [service]="dockerbunker/${SERVICE_NAME}" )
+declare -A BUILD_IMAGES=( [dockerbunker/${SERVICE_NAME}]="${DOCKERFILES}/${SERVICE_NAME}" )
+
+[[ -z $1 ]] && options_menu
+
+upgrade() {
+	read -p "Please enter the Seafile Version number to upgrade to: " SF_VERSION
+
+	pull_and_compare
+
+	stop_containers
+	remove_containers
+
+	echo -en "\n\e[1mStarting up ${PROPER_NAME} upgrade container\e[0m" \
+		&& seafilepro_setup_dockerbunker "upgrade $SF_VERSION" \
+		&& exit_response
+
+	docker_run_all
+}
+
+configure() {
+	pre_configure_routine
+
+	echo -e "# \e[4mSeafile Pro Settings\e[0m"
+	set_domain
+
+	# avoid tr illegal byte sequence in macOS when generating random strings
+	if [[ $OSTYPE =~ "darwin" ]];then
+		if [[ $LC_ALL ]];then
+			oldLC_ALL=$LC_ALL
+			export LC_ALL=C
+		else
+			export LC_ALL=C
+		fi
+	fi
+	cat <<-EOF >> "${SERVICE_ENV}"
+	# ------------------------------
+	# General Settings
+	# ------------------------------
+	
+	PROPER_NAME="${PROPER_NAME}"
+	SERVICE_NAME=${SERVICE_NAME}
+	SSL_CHOICE=${SSL_CHOICE}
+	LE_EMAIL=${LE_EMAIL}
+	
+	SERVICE_DOMAIN=${SERVICE_DOMAIN}
+
+	# ------------------------------
+	# SQL database configuration
+	# ------------------------------
+	
+	DBUSER=seafile
+	
+	# Please use long, random alphanumeric strings (A-Za-z0-9)
+	DBROOT=$(</dev/urandom tr -dc A-Za-z0-9 | head -c 28)
+	DBPASS=$(</dev/urandom tr -dc A-Za-z0-9 | head -c 28)
+	EOF
+	
+	if [[ $OSTYPE =~ "darwin" ]];then
+		unset LC_ALL
+	fi
+
+	post_configure_routine
+}
+
+setup() {
+	initial_setup_routine
+
+	SUBSTITUTE=( "\${SERVICE_DOMAIN}" )
+	basic_nginx
+
+	[[ $keep_volumes ]] \
+		&& echo -en "\n\e[1mStarting up ${PROPER_NAME} database container\e[0m" \
+		&& seafilepro_db_dockerbunker \
+		&& exit_response
+	
+	if [[ -z $keep_volumes ]];then
+		echo "Starting interactive Seafile Pro setup"
+		echo ""
+		echo "MySQL Server:          db"
+		echo "Port:                  3306"
+		echo "MySQL User:            seafile"
+		prompt_confirm "Display MySQL root and user passwords"
+		[[ $? == 0 ]] && echo -e "MySQL root password:   ${DBROOT}\nMySQL user password:   ${DBPASS}" || echo -e "\e[33mPlease obtain the MySQL root password from ${SERVICE_ENV}\e[0m\n"
+		echo ""
+	
+		echo -e "\n\e[1mStarting up ${PROPER_NAME} setup container\e[0m" \
+			&& seafilepro_setup_dockerbunker setup \
+			&& exit_response
+	fi
+
+	docker_run_all
+
+	post_setup_routine
+}
+
+if [[ $1 == "letsencrypt" ]];then
+	$1 $*
+else
+	$1
+fi

+ 8 - 0
data/services/searx/containers.sh

@@ -0,0 +1,8 @@
+searx_service_dockerbunker() {
+	docker run -d \
+		--name=${FUNCNAME[0]//_/-} \
+		--restart=always \
+		--network ${NETWORK} \
+		--env-file "${SERVICE_ENV}" \
+	${IMAGES[service]} >/dev/null
+}

+ 49 - 0
data/services/searx/nginx/searx.conf

@@ -0,0 +1,49 @@
+upstream searx {
+	server searx-service-dockerbunker:8888;
+}
+
+server {
+	listen 80;
+	server_name ${SERVICE_DOMAIN};
+	return 301 https://$host$request_uri;
+	add_header X-Content-Type-Options "nosniff" always;
+	add_header X-XSS-Protection "1; mode=block" always;
+	add_header X-Frame-Options "DENY" always;
+	add_header Referrer-Policy "strict-origin" always;
+	add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
+	server_tokens off;
+}
+
+server {
+	listen 443;
+	server_name ${SERVICE_DOMAIN};
+	ssl on;
+	ssl_certificate /etc/nginx/ssl/${SERVICE_DOMAIN}/cert.pem;
+	ssl_certificate_key /etc/nginx/ssl/${SERVICE_DOMAIN}/key.pem;
+	include /etc/nginx/includes/ssl.conf;
+
+	add_header X-Content-Type-Options "nosniff" always;
+	add_header X-XSS-Protection "1; mode=block" always;
+	add_header X-Frame-Options "DENY" always;
+	add_header Referrer-Policy "strict-origin" always;
+	add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
+	server_tokens off;
+
+	include /etc/nginx/includes/gzip.conf;
+
+	location / {
+		proxy_pass http://searx/;
+		proxy_set_header  Host			  $http_host;   # required for docker client's sake
+		proxy_set_header  X-Real-IP		 $remote_addr; # pass on real client's IP
+		proxy_set_header  X-Forwarded-For   $proxy_add_x_forwarded_for;
+		proxy_set_header  X-Forwarded-Proto $scheme;
+		proxy_read_timeout				  900;
+	}
+
+	location ~ /.well-known {
+		allow all;
+		root /var/www/html;
+	}
+}
+
+

+ 107 - 0
data/services/searx/searx.sh

@@ -0,0 +1,107 @@
+#!/usr/bin/env bash
+
+while true;do ls | grep -q dockerbunker.sh;if [[ $? == 0 ]];then BASE_DIR=$PWD;break;else cd ../;fi;done
+
+PROPER_NAME="Searx"
+SERVICE_NAME="$(echo -e "${PROPER_NAME,,}" | tr -d '[:space:]')"
+PROMPT_SSL=1
+
+declare -a environment=( "data/env/dockerbunker.env" "data/include/init.sh" )
+
+for env in "${environment[@]}";do
+	[[ -f "${BASE_DIR}/$env" ]] && source "${BASE_DIR}/$env"
+done
+
+declare -A WEB_SERVICES
+declare -a containers=( "${SERVICE_NAME}-service-dockerbunker" )
+declare -a volumes=( "${SERVICE_NAME}-data-vol-1" )
+declare -a add_to_network=( "${SERVICE_NAME}-service-dockerbunker" )
+declare -a networks=( )
+declare -A IMAGES=( [service]="dockerbunker/${SERVICE_NAME}" )
+declare -A BUILD_IMAGES=( [dockerbunker/${SERVICE_NAME}]="${DOCKERFILES}/${SERVICE_NAME}" )
+
+[[ -z $1 ]] && options_menu
+
+upgrade() {
+	get_current_images_sha256
+
+	sed -i "s/default_theme\ :.*/default_theme\ :\ ${THEME}/" data/Dockerfiles/${SERVICE_NAME}/${SERVICE_NAME}/settings.yml
+	sed -i "s/instance_name\ \:.*/instance_name\ \:\ \"${INSTANCE_NAME}\"/" data/Dockerfiles/${SERVICE_NAME}/${SERVICE_NAME}/settings.yml
+	
+	docker_build
+	docker_pull
+
+	stop_containers
+	remove_containers
+
+	docker_run_all
+
+	delete_old_images
+
+	restart_nginx
+}
+
+configure() {
+	pre_configure_routine
+
+	! [[ -d "${BASE_DIR}/data/Dockerfiles/${SERVICE_NAME}" ]] \
+	&& echo -n "Cloning Searx repository into ${BASE_DIR}/data/Dockerfiles/${SERVICE_NAME}" \
+	&& git submodule add -f https://github.com/asciimoo/searx.git "${BASE_DIR}"/data/Dockerfiles/${SERVICE_NAME} >/dev/null \
+	&& exit_response
+
+	echo -e "# \e[4mSearx Settings\e[0m"
+
+	set_domain
+	
+	if [ "$INSTANCE_NAME" ]; then
+	  read -p "Instance Name: " -ei "$INSTANCE_NAME" INSTANCE_NAME
+	else
+	  read -p "Instance Name: " -ei "${SERVICE_NAME}" INSTANCE_NAME
+	fi
+
+	if [ "$THEME" ]; then
+	  read -p "Theme [oscar, courgette, pix-art, simple]: " -ei "$THEME" THEME
+	else
+	  read -p "Theme [oscar, courgette, pix-art, simple]: " -ei "oscar" THEME
+	fi
+
+	cat <<-EOF >> "${SERVICE_ENV}"
+	#SEARX
+	## ------------------------------
+
+	PROPER_NAME="${PROPER_NAME}"
+	SERVICE_NAME="${SERVICE_NAME}"
+	SSL_CHOICE=${SSL_CHOICE}
+	LE_EMAIL=${LE_EMAIL}
+
+	SERVICE_DOMAIN="${SERVICE_DOMAIN}"
+	INSTANCE_NAME="${INSTANCE_NAME}"
+	THEME="${THEME}"
+
+	## ------------------------------
+	#/SEARX
+
+	EOF
+
+	post_configure_routine
+}
+setup() {
+
+	sed -i "s/default_theme\ :.*/default_theme\ :\ ${THEME}/" data/Dockerfiles/${SERVICE_NAME}/${SERVICE_NAME}/settings.yml
+	sed -i "s/instance_name\ \:.*/instance_name\ \:\ \"${INSTANCE_NAME}\"/" data/Dockerfiles/${SERVICE_NAME}/${SERVICE_NAME}/settings.yml
+
+	initial_setup_routine
+
+	SUBSTITUTE=( "\${SERVICE_DOMAIN}" )
+	basic_nginx
+
+	docker_run_all
+
+	post_setup_routine
+}
+
+if [[ $1 == "letsencrypt" ]];then
+	$1 $*
+else
+	$1
+fi

+ 9 - 0
data/services/send/containers.sh

@@ -0,0 +1,9 @@
+send_service_dockerbunker() {
+	docker run -d \
+		--name=${FUNCNAME[0]//_/-} \
+		--restart=always \
+		--network ${NETWORK} \
+		--env-file "${SERVICE_ENV}" \
+		-v ${SERVICE_NAME}-data-vol-1:/send/data \
+	${IMAGES[service]} >/dev/null
+}

+ 57 - 0
data/services/send/nginx/send.conf

@@ -0,0 +1,57 @@
+##
+# You should look at the following URL's in order to grasp a solid understanding
+# of Nginx configuration files in order to fully unleash the power of Nginx.
+# http://wiki.nginx.org/Pitfalls
+# http://wiki.nginx.org/QuickStart
+# http://wiki.nginx.org/Configuration
+#
+# Generally, you will want to move this file somewhere, and start with a clean
+# file but keep this around for reference. Or just disable in sites-enabled.
+#
+# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples.
+##
+
+# Default server configuration
+#
+upstream send {
+ server send-service-dockerbunker:1443;
+}
+
+server {
+    listen 80;
+	server_name ${SERVICE_DOMAIN};
+    return 301 https://$host$request_uri;
+}
+
+server {
+    listen 443;
+	server_name ${SERVICE_DOMAIN};
+    ssl on;
+	ssl_certificate /etc/nginx/ssl/${SERVICE_DOMAIN}/cert.pem;
+	ssl_certificate_key /etc/nginx/ssl/${SERVICE_DOMAIN}/key.pem;
+	include /etc/nginx/includes/ssl.conf;
+
+    add_header Strict-Transport-Security "max-age=15768000; includeSubDomains";
+	add_header X-Frame-Options DENY;
+	add_header X-Content-Type-Options nosniff;
+
+	include /etc/nginx/includes/gzip.conf;
+
+	client_max_body_size 2G;
+
+    location / {
+        proxy_pass http://send/;
+		proxy_set_header  Host              $http_host;   # required for docker client's sake
+		proxy_set_header  X-Real-IP         $remote_addr; # pass on real client's IP
+		proxy_set_header  X-Forwarded-For   $proxy_add_x_forwarded_for;
+		proxy_set_header  X-Forwarded-Proto $scheme;
+		proxy_read_timeout                  900;
+    }
+
+	location ~ /.well-known {
+        allow all;
+		root /var/www/html;
+	}
+}
+
+

+ 48 - 0
data/services/send/send.sh

@@ -0,0 +1,48 @@
+#!/usr/bin/env bash
+
+while true;do ls | grep -q dockerbunker.sh;if [[ $? == 0 ]];then BASE_DIR=$PWD;break;else cd ../;fi;done
+
+PROPER_NAME="Send"
+SERVICE_NAME="$(echo -e "${PROPER_NAME,,}" | tr -d '[:space:]')"
+PROMPT_SSL=1
+
+declare -a environment=( "data/env/dockerbunker.env" "data/include/init.sh" )
+
+for env in "${environment[@]}";do
+	[[ -f "${BASE_DIR}"/$env ]] && source "${BASE_DIR}"/$env
+done
+
+declare -A WEB_SERVICES
+declare -a containers=( "${SERVICE_NAME}-service-dockerbunker" )
+declare -a volumes=( "${SERVICE_NAME}-data-vol-1" )
+declare -a add_to_network=( "${SERVICE_NAME}-service-dockerbunker" )
+declare -a networks=( )
+declare -A IMAGES=( [service]="dockerbunker/${SERVICE_NAME}" )
+declare -A BUILD_IMAGES=( [dockerbunker/${SERVICE_NAME}]="${DOCKERFILES}/${SERVICE_NAME}" )
+
+[[ -z $1 ]] && options_menu
+
+configure() {
+	pre_configure_routine
+
+	echo -e "# \e[4mSend Settings\e[0m"
+
+	set_domain
+	
+	cat <<-EOF >> "${SERVICE_ENV}"
+	PROPER_NAME=${PROPER_NAME}
+	SERVICE_NAME=${SERVICE_NAME}
+	SSL_CHOICE=${SSL_CHOICE}
+	LE_EMAIL=${LE_EMAIL}
+
+	SERVICE_DOMAIN=${SERVICE_DOMAIN}
+	EOF
+
+	post_configure_routine
+}
+
+if [[ $1 == "letsencrypt" ]];then
+	$1 $*
+else
+	$1
+fi

+ 15 - 0
data/services/sftpserver/containers.sh

@@ -0,0 +1,15 @@
+sftpserver_service_dockerbunker() {
+	docker run -d \
+		--name=${FUNCNAME[0]//_/-} \
+		--restart=always \
+		--network ${NETWORK} \
+		--env-file "${SERVICE_ENV}" \
+		-p 2222:22 \
+		-v "${CONF_DIR}"/sftpserver/users.conf:/etc/sftp/users.conf:ro \
+		-v "${CONF_DIR}"/sftpserver/ssh/ssh_host_ed25519_key:/etc/ssh/ssh_host_ed25519_key \
+		-v "${CONF_DIR}"/sftpserver/ssh/ssh_host_rsa_key:/etc/ssh/ssh_host_rsa_key \
+		-v "${SERVICES_DIR}"/sftpserver/run.sh:/etc/sftp.d/fix-permissions:ro \
+		-v "${BASE_DIR}"/data/web/sftpserver:/home \
+	${IMAGES[service]} >/dev/null
+}
+

+ 49 - 0
data/services/sftpserver/nginx/sftpserver.conf

@@ -0,0 +1,49 @@
+##
+# You should look at the following URL's in order to grasp a solid understanding
+# of Nginx configuration files in order to fully unleash the power of Nginx.
+# http://wiki.nginx.org/Pitfalls
+# http://wiki.nginx.org/QuickStart
+# http://wiki.nginx.org/Configuration
+#
+# Generally, you will want to move this file somewhere, and start with a clean
+# file but keep this around for reference. Or just disable in sites-enabled.
+#
+# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples.
+##
+
+# Default server configuration
+#
+server {
+    listen 80;
+    server_name ${SERVICE_DOMAIN};
+    return 301 https://$host$request_uri;
+}
+
+server {
+    listen 443;
+	server_name ${SERVICE_DOMAIN};
+    ssl on;
+	ssl_certificate /etc/nginx/ssl/${SERVICE_DOMAIN}/cert.pem;
+	ssl_certificate_key /etc/nginx/ssl/${SERVICE_DOMAIN}/key.pem;
+	include /etc/nginx/includes/ssl.conf;
+
+    add_header Strict-Transport-Security "max-age=15768000; includeSubDomains";
+	add_header X-Frame-Options DENY;
+	add_header X-Content-Type-Options nosniff;
+	include /etc/nginx/includes/gzip.conf;
+
+    location / {
+	root /var/www/html/sftpserver;
+	index index.php index.html;
+	proxy_set_header  Host              $http_host;   # required for docker client's sake
+	proxy_set_header  X-Real-IP         $remote_addr; # pass on real client's IP
+	proxy_set_header  X-Forwarded-For   $proxy_add_x_forwarded_for;
+	proxy_set_header  X-Forwarded-Proto $scheme;
+	proxy_read_timeout                  900;
+    }
+
+    location ~ /.well-known {
+        allow all;
+	root /var/www/html;
+    }
+}

+ 10 - 0
data/services/sftpserver/run.sh

@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+
+chmod 400 /etc/ssh/ssh_host_ed25519_key
+chmod 400 /etc/ssh/ssh_host_rsa_key
+
+cd /home
+for user in *; do
+	[[ ! -d /home/${user}/upload ]] && mkdir /home/${user}/upload
+	chown -R ${user}:users ${user}/*
+done

+ 91 - 0
data/services/sftpserver/sftpserver.sh

@@ -0,0 +1,91 @@
+#!/usr/bin/env bash
+
+while true;do ls | grep -q dockerbunker.sh;if [[ $? == 0 ]];then BASE_DIR=$PWD;break;else cd ../;fi;done
+
+PROPER_NAME="sFTP Server"
+SERVICE_NAME="$(echo -e "${PROPER_NAME,,}" | tr -d '[:space:]')"
+PROMPT_SSL=1
+safe_to_keep_volumes_when_reconfiguring=1
+
+declare -a environment=( "data/env/dockerbunker.env" "data/include/init.sh" )
+
+for env in "${environment[@]}";do
+	[[ -f "${BASE_DIR}/$env" ]] && source "${BASE_DIR}/$env"
+done
+
+declare -A WEB_SERVICES
+declare -a containers=( "sftpserver-service-dockerbunker" )
+declare -a add_to_network=( "sftpserver-service-dockerbunker" )
+declare -a volumes=( "sftpserver-data-vol-1" )
+declare -a networks=( )
+declare -A IMAGES=( [service]="atmoz/sftp:alpine-3.7" )
+declare -a add_to_network=( "sftpserver-service-dockerbunker" )
+
+[[ -z $1 ]] && options_menu
+
+configure() {
+	pre_configure_routine
+
+	echo -e "# \e[4msFTP Settings\e[0m"
+
+	set_domain
+
+	userNumber=0
+	done=""
+	while [[ -z $done ]];do
+		[[ -f "${CONF_DIR}"/sftpserver/users.conf ]] && rm "${CONF_DIR}"/sftpserver/users.conf
+		unset user password
+		((++userNumber))
+		read -p "sFTP User $userNumber: " -ei "" user
+		echo "USER $user"
+		while [[ "${#password}" -le 6 || "$password" != *[A-Z]* || "$password" != *[a-z]* || "$password" != *[0-9]* ]];do
+			if [ $VALIDATE ];then
+				echo -e "\n\e[31m  Password does not meet requirements\e[0m"
+			fi
+				stty_orig=$(stty -g)
+				stty -echo
+		  		read -p " $(printf "\n   \e[4mPassword requirements\e[0m\n   Minimum Length 6,Uppercase, Lowercase, Integer\n\n   Enter Password:") " -ei "" password
+				stty "$stty_orig"
+				echo ""
+			VALIDATE=1
+		done
+		unset VALIDATE
+		declare -A FTP_USERS
+		FTP_USERS+=( [$user]="$password" )
+		FTP_USERS_ARRAY+=( "[${user}]=\"${password}\"" )
+		prompt_confirm "Add another user?"
+		[[ $? == 1 ]] && done=1
+	done
+
+	[[ ! -d "${CONF_DIR}"/sftpserver/conf/ssh ]] && mkdir -p "${CONF_DIR}"/sftpserver/conf/ssh
+	USERID=1001
+	for user in ${!FTP_USERS[@]};do
+		echo "$user:${FTP_USERS[$user]}:${USERID}:100" >> "${CONF_DIR}"/sftpserver/users.conf
+		((++USERID))
+	done
+	
+	cat <<-EOF >> "${SERVICE_ENV}"
+	PROPER_NAME="${PROPER_NAME}"
+	SERVICE_NAME=${SERVICE_NAME}
+	LE_EMAIL=${LE_EMAIL}
+	SSL_CHOICE=${SSL_CHOICE}
+
+	SERVICE_DOMAIN=${SERVICE_DOMAIN}
+	FTP_USERS=( ${FTP_USERS_ARRAY[@]} )
+	EOF
+
+	[[ ! -d "${BASE_DIR}"/data/web/sftpserver ]] && mkdir -p "${BASE_DIR}"/data/web/sftpserver
+
+	[[ ! -d "${CONF_DIR}"/sftpserver/conf/ssh ]] && mkdir "${CONF_DIR}"/sftpserver/conf/ssh
+
+	[[ ! -f "${CONF_DIR}"/sftpserver/conf/ssh/ssh_host_ed25519_key ]] && ssh-keygen -t ed25519 -f "${CONF_DIR}"/sftpserver/conf/ssh/ssh_host_ed25519_key
+	[[ ! -f "${CONF_DIR}"/sftpserver/conf/ssh/ssh_host_rsa_key ]] && ssh-keygen -t rsa -b 4096 -f "${CONF_DIR}"/sftpserver/conf/ssh/ssh_host_rsa_key
+	post_configure_routine
+}
+
+if [[ $1 == "letsencrypt" ]];then
+	$1 $*
+else
+	$1
+fi
+

+ 81 - 0
data/services/statichtmlsite/nginx/statichtmlsite.conf

@@ -0,0 +1,81 @@
+# Expires map
+map $sent_http_content_type $expires {
+    default                    off;
+    text/html                  epoch;
+    text/css                   max;
+    application/javascript     max;
+    ~image/                    max;
+}
+
+server {
+	listen		80;
+	listen		[::]:80;
+	server_name	${SERVICE_DOMAIN};
+	return 301	https://$http_host$request_uri;
+	add_header X-Content-Type-Options "nosniff" always;
+	add_header X-XSS-Protection "1; mode=block" always;
+	add_header X-Frame-Options "DENY" always;
+	add_header Referrer-Policy "strict-origin" always;
+	add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
+	server_tokens off;
+	}
+
+server {
+	server_name	${SERVICE_DOMAIN};
+	root /var/www/html/${SERVICE_DOMAIN};
+        index index.html index.htm;
+	listen 443 ssl;
+#	listen [::]:443 ipv6only=on  ssl;
+	ssl on;
+	ssl_certificate /etc/nginx/ssl/${SERVICE_DOMAIN}/cert.pem;
+	ssl_certificate_key /etc/nginx/ssl/${SERVICE_DOMAIN}/key.pem;
+	include /etc/nginx/includes/ssl.conf;
+
+	add_header X-Content-Type-Options "nosniff" always;
+	add_header X-XSS-Protection "1; mode=block" always;
+	add_header X-Frame-Options "DENY" always;
+	add_header Referrer-Policy "strict-origin" always;
+	add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
+	server_tokens off;
+
+	include /etc/nginx/includes/gzip.conf;
+
+	location / {
+                #try_files $uri $uri/ =404;
+                try_files $uri $uri/ /index.php?q=$uri&$args;
+        }
+
+	expires $expires;
+
+	location ~* \.(js|css|xml|gz)$ {
+    		add_header Vary "Accept-Encoding";
+	}
+	
+	# custom error pages
+
+	fastcgi_intercept_errors on; # make custom errors work
+	
+	error_page   500 502 503 504  /errors/50x.html;
+	location = /50x.html {
+		root   /var/www/errors;
+	}
+
+	error_page   403   /errors/403.html;
+	location = /403.html {
+		root   /var/www/errors;
+	}
+
+	error_page   404   /errors/404.html;
+		location = /404.html {
+	}
+
+	location ^~ /.well-known/ {
+	    access_log           off;
+	    log_not_found        off;
+	    root                 /var/www/html;
+#	    autoindex            off;
+	    index                index.html; # "no-such-file.txt",if expected protos don't need it
+	    try_files            $uri $uri/ =404;
+	}
+
+}

+ 73 - 0
data/services/statichtmlsite/statichtmlsite.sh

@@ -0,0 +1,73 @@
+#!/usr/bin/env bash
+
+while true;do ls | grep -q dockerbunker.sh;if [[ $? == 0 ]];then BASE_DIR=$PWD;break;else cd ../;fi;done
+
+PROPER_NAME="Static HTML Site"
+SERVICE_NAME="$(echo -e "${PROPER_NAME,,}" | tr -d '[:space:]')"
+PROMPT_SSL=1
+STATIC=1
+
+declare -a environment=( "data/env/dockerbunker.env" "data/include/init.sh" )
+
+for env in "${environment[@]}";do
+	[[ -f "${BASE_DIR}"/$env ]] && source "${BASE_DIR}"/$env
+done
+
+[[ $1 == "letsencrypt" && $2 == "issue" && $3 ]] \
+	&& [[ -f "${ENV_DIR}"/static/${3}.env ]] && source "${ENV_DIR}"/static/${3}.env \
+	&& letsencrypt issue "static"
+
+[[ -z $1 ]] && options_menu
+
+configure() {
+	echo -e "# \e[4mSite Settings\e[0m"
+
+	set_domain
+
+	[[ -f "${ENV_DIR}"/static/${SERVICE_DOMAIN[0]}.env ]] && echo "Site already exists. Exiting." && exit 0
+	
+	STATIC_HOME="${BASE_DIR}/data/web/${SERVICE_DOMAIN[0]}"
+
+	! [[ -d "${ENV_DIR}"/static ]] && mkdir "${ENV_DIR}"/static
+
+	cat <<-EOF >> "${ENV_DIR}"/static/${SERVICE_DOMAIN[0]}.env
+	#STATIC
+	## ------------------------------
+
+	STATIC=${STATIC}
+	SSL_CHOICE=${SSL_CHOICE}
+	LE_EMAIL=${LE_EMAIL}
+
+	STATIC_HOME="${STATIC_HOME}"
+	SERVICE_DOMAIN[0]=${SERVICE_DOMAIN[0]}
+
+	## ------------------------------
+	#/STATIC
+
+	EOF
+
+	source "${ENV_DIR}"/static/${SERVICE_DOMAIN[0]}.env
+
+	if ! [[ -d "${STATIC_HOME}" ]];then
+		mkdir -p "${STATIC_HOME}"
+		echo "Welcome to my cool website." > "${STATIC_HOME}"/index.html
+	else
+		echo -en "Using existing HTML directory[data/web/${SERVICE_DOMAIN[0]}]"
+		exit_response
+	fi
+
+	post_configure_routine
+
+	SUBSTITUTE=( "\${SERVICE_DOMAIN}" )
+	basic_nginx
+
+	[[ ! $(docker ps -q --filter name=^/${NGINX_CONTAINER}$) ]] \
+		&& setup_nginx \
+		|| restart_nginx
+
+	if [[ $SSL_CHOICE == "le" ]];then
+		letsencrypt issue "static"
+	fi
+}
+
+[[ -z $3 ]] && $1

+ 29 - 0
data/services/wordpress/containers.sh

@@ -0,0 +1,29 @@
+wordpress_db_dockerbunker() {
+	docker run -d \
+		--name=${FUNCNAME[0]//_/-} \
+		--restart=always \
+		--network dockerbunker-${SERVICE_NAME} --net-alias=db \
+		--env-file="${SERVICE_ENV}" \
+		-v wordpress-db-vol-1:/var/lib/mysql \
+		-v "${BASE_DIR}/data/services/${SERVICE_NAME}/mysql/":/etc/mysql/conf.d/:ro \
+		--health-cmd="mysqladmin ping --host localhost --silent" --health-interval=10s --health-retries=5 --health-timeout=30s \
+	${IMAGES[db]} >/dev/null
+}
+
+wordpress_service_dockerbunker() {
+	docker run -d \
+		--name=${FUNCNAME[0]//_/-} \
+		--restart=always \
+		--network dockerbunker-${SERVICE_NAME} \
+		--env-file="${SERVICE_ENV}" \
+		--env WORDPRESS_DB_NAME=wpdb \
+		--env WORDPRESS_TABLE_PREFIX=wp_ \
+		--env WORDPRESS_DB_HOST=db:3306 \
+		--env WORDPRESS_DB_USER=${MYSQL_USER} \
+		--env WORDPRESS_DB_PASSWORD=${MYSQL_PASSWORD} \
+		-v "${BASE_DIR}/data/services/${SERVICE_NAME}/php/uploads.ini":/usr/local/etc/php/conf.d/uploads.ini \
+		-v wordpress-data-vol-1:/var/www/html/wp-content \
+	${IMAGES[service]} >/dev/null
+}
+
+

+ 125 - 0
data/services/wordpress/nginx/wordpress.conf

@@ -0,0 +1,125 @@
+##
+# You should look at the following URL's in order to grasp a solid understanding
+# of Nginx configuration files in order to fully unleash the power of Nginx.
+# http://wiki.nginx.org/Pitfalls
+# http://wiki.nginx.org/QuickStart
+# http://wiki.nginx.org/Configuration
+#
+# Generally, you will want to move this file somewhere, and start with a clean
+# file but keep this around for reference. Or just disable in sites-enabled.
+#
+# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples.
+##
+
+# Default server configuration
+#
+
+map $sent_http_content_type $expires {
+	default                    off;
+	text/html                  epoch;
+	text/css                   max;
+	application/javascript     max;
+	~image/                    max;
+}
+
+upstream wordpress {
+	server wordpress-service-dockerbunker:80;
+}
+
+server {
+	listen 80;
+	server_name ${SERVICE_DOMAIN} *.${SERVICE_DOMAIN};
+	return 301 https://$host$request_uri;
+	add_header X-Content-Type-Options "nosniff" always;
+	add_header X-XSS-Protection "1; mode=block" always;
+	add_header X-Frame-Options "DENY" always;
+	add_header Referrer-Policy "strict-origin" always;
+	add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
+	server_tokens off;
+}
+
+server {
+	listen 443;
+	server_name ${SERVICE_DOMAIN};
+	ssl on;
+	ssl_certificate /etc/nginx/ssl/${SERVICE_DOMAIN}/cert.pem;
+	ssl_certificate_key /etc/nginx/ssl/${SERVICE_DOMAIN}/key.pem;
+	include /etc/nginx/includes/ssl.conf;
+
+	add_header X-Content-Type-Options "nosniff" always;
+	add_header X-XSS-Protection "1; mode=block" always;
+	add_header X-Frame-Options "SAMEORIGIN" always;
+	add_header Referrer-Policy "strict-origin" always;
+	add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
+	server_tokens off;
+
+	proxy_redirect off;
+	proxy_read_timeout		90;
+	proxy_connect_timeout	90;
+	proxy_redirect			off;
+	proxy_set_header		X-NginX-Proxy true;
+	proxy_set_header		X-Real-IP $remote_addr;
+	proxy_set_header		X-Forwarded-For $proxy_add_x_forwarded_for;
+	proxy_set_header		X-Forwarded-Proto https;
+	proxy_set_header		X-Forwarded-Port 443;
+	proxy_set_header		Host $host;
+	proxy_set_header X-Forwarded-Host $host;
+	proxy_set_header X-Forwarded-Server $host;
+
+	include /etc/nginx/includes/gzip.conf;
+
+	location / {
+		proxy_pass http://wordpress/;
+		proxy_set_header  Host              $http_host;   # required for docker client's sake
+		proxy_set_header  X-Real-IP         $remote_addr; # pass on real client's IP
+		proxy_set_header  X-Forwarded-For   $proxy_add_x_forwarded_for;
+		proxy_set_header  X-Forwarded-Proto $scheme;
+		proxy_read_timeout                  900;
+	}
+
+	location ^~ /wp-login.php {
+${AUTH_SWITCH}	include /etc/nginx/conf.d/${SERVICE_DOMAIN}/basic_auth.conf;
+		proxy_pass http://wordpress/$request_uri;
+	}
+	
+#	Deny all attempts to access hidden files such as .htaccess, .htpasswd, .DS_Store (Mac).
+	location ~ /\. {
+		deny all;
+		access_log off;
+		log_not_found off;
+	}
+
+	location = /xmlrpc.php {
+        	deny all;
+		log_not_found off;
+		access_log off;
+	}
+	
+
+	location ~* /wp-content/.*.php$ {
+		deny all;
+		access_log off;
+		log_not_found off;
+	}
+
+	location ~* /wp-includes/.*.php$ {
+		deny all;
+		access_log off;
+		log_not_found off;
+	}
+
+	location ~* /(?:uploads|files)/.*.php$ {
+		deny all;
+		access_log off;
+		log_not_found off;
+	}
+
+	location ^~ /.well-known/ {
+		access_log           off;
+		log_not_found        off;
+		root                 /var/www/html;
+		autoindex            off;
+		index                index.html; # "no-such-file.txt",if expected protos don't need it
+		try_files            $uri $uri/ =404;
+	}
+}

+ 5 - 0
data/services/wordpress/php/uploads.ini

@@ -0,0 +1,5 @@
+file_uploads = On
+memory_limit = 64M
+upload_max_filesize = 64M
+post_max_size = 64M
+max_execution_time = 600

+ 28 - 0
data/services/wordpress/wordpress.sh

@@ -0,0 +1,28 @@
+wordpress_db_dockerbunker() {
+	docker run -d \
+		--name=${FUNCNAME[0]//_/-} \
+		--restart=always \
+		--network dockerbunker-${SERVICE_NAME} --net-alias=db \
+		--env-file="${SERVICE_ENV}" \
+		-v wordpress-db-vol-1:/var/lib/mysql \
+		-v "${SERVICES_DIR}"/${SERVICE_NAME}/mysql/:/etc/mysql/conf.d/:ro \
+		--health-cmd="mysqladmin ping --host localhost --silent" --health-interval=10s --health-retries=5 --health-timeout=30s \
+	${IMAGES[db]} >/dev/null
+}
+
+wordpress_service_dockerbunker() {
+	docker run -d \
+		--name=${FUNCNAME[0]//_/-} \
+		--restart=always \
+		--network dockerbunker-${SERVICE_NAME} \
+		--env-file="${SERVICE_ENV}" \
+		--env WORDPRESS_DB_NAME=wpdb \
+		--env WORDPRESS_TABLE_PREFIX=wp_ \
+		--env WORDPRESS_DB_HOST=db:3306 \
+		--env WORDPRESS_DB_USER=${MYSQL_USER} \
+		--env WORDPRESS_DB_PASSWORD=${MYSQL_PASSWORD} \
+		-v "${SERVICES_DIR}"/${SERVICE_NAME}/php/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini \
+		-v wordpress-data-vol-1:/var/www/html/wp-content \
+	${IMAGES[service]} >/dev/null
+}
+

+ 171 - 0
dockerbunker.sh

@@ -0,0 +1,171 @@
+#!/usr/bin/env bash
+
+# Find base dir
+while true;do ls | grep -q dockerbunker.sh;if [[ $? == 0 ]];then BASE_DIR=$PWD;break;else cd ../;fi;done
+
+source "${BASE_DIR}"/data/include/init.sh
+
+[[ -f "data/env/dockerbunker.env" ]] && source "data/env/dockerbunker.env" || init_dockerbunker
+
+unset AVAILABLE_SERVICES count
+# All available services
+declare -a ALL_SERVICES=( \
+	"Static HTML Site" \
+	"Bitbucket" \
+	"Cryptpad" \
+	"CS50 IDE" \
+	"Dillinger" \
+	"Ghost" \
+	"Gitea" \
+	"Gitlab CE" \
+	"Gogs" \
+	"Hastebin" \
+	"IPsec VPN Server" \
+	"Kanboard" \
+	"Mailcow Dockerized" \
+	"Mailpile" \
+	"Mastodon" \
+	"Nextcloud" \
+	"Open Project" \
+	"Piwik" \
+	"Seafile Pro" \
+	"Searx" \
+	"Send" \
+	"sFTP Server" \
+	"Wordpress" \
+	)
+
+# style menu according to what status service has
+declare -A SERVICES_ARR
+for service in "${ALL_SERVICES[@]}";do
+	service_name="$(echo -e "${service,,}" | tr -d '[:space:]')"
+	if [[ "${INSTALLED_SERVICES[@]}" =~ $service ]];then
+		[[ "${STOPPED_SERVICES[@]}" =~ $service ]] && service_status="$(printf "\e[32m${service}\e[0m \e[31m(Stopped)\e[0m")" || service_status="$(printf "\e[32m${service}\e[0m")"
+		SERVICES_ARR+=( [$service_status]="${service_name}" )
+		AVAILABLE_SERVICES+=( "$service_status" )
+	elif [[ "${CONFIGURED_SERVICES[@]}" =~ $service ]];then
+		service_status="$(printf "\e[33m${service}\e[0m")"
+		SERVICES_ARR+=( [$service_status]="${service_name}" )
+		AVAILABLE_SERVICES+=( "$service_status" )
+	else
+		service_status="$(printf "${service}")"
+		SERVICES_ARR+=( [$service_status]="${service_name}" )
+		AVAILABLE_SERVICES+=( "$service_status" )
+	fi
+done
+
+startall=$(printf "\e[1;4;33mStart all stopped containers\e[0m")
+stopall=$(printf "\e[1;4;33mStop all running containers\e[0m")
+startnginx=$(printf "\e[1;4;33mStart nginx container\e[0m")
+stopnginx=$(printf "\e[1;4;33mStop nginx container\e[0m")
+restartnginx=$(printf "\e[1;4;33mRestart nginx container\e[0m")
+restartall=$(printf "\e[1;4;33mRestart all containers\e[0m")
+destroyall=$(printf "\e[1;4;33mDestroy everything\e[0m")
+exitmenu=$(printf "\e[1;4;33mExit\e[0m")
+
+count=$((${#AVAILABLE_SERVICES[@]}+1))
+
+[[ ${STOPPED_SERVICES[0]} ]] \
+	&& AVAILABLE_SERVICES+=( "$startall" )
+
+[[ $(docker ps -q --filter "status=running" --filter name=^/nginx-dockerbunker$) ]] \
+	&& AVAILABLE_SERVICES+=( "$stopnginx" ) \
+	&& AVAILABLE_SERVICES+=( "$restartnginx") \
+	&& count=$(($count+2))
+
+[[ ${#INSTALLED_SERVICES[@]} > 0 \
+	|| ${#STATIC_SITES[@]} > 0 \
+	|| ${#CONFIGURED_SERVICES[@]} > 0 ]] \
+&& AVAILABLE_SERVICES+=( "$destroyall" ) &&  count=$(($count+1))
+
+[[ $(docker ps -q --filter "status=exited" --filter name=^/nginx-dockerbunker$) ]] \
+	&& AVAILABLE_SERVICES+=( "$startnginx" )
+
+
+[[ $(docker ps -q --filter "status=running" --filter name=dockerbunker) && ${#INSTALLED_SERVICES[@]} == 0 && -z ${STATIC_SITES[@]} ]] && AVAILABLE_SERVICES+=( "$destroyall" ) &&  count=$(($count+1))
+[[ $(docker ps -q --filter "status=running" --filter name=dockerbunker) && ${#INSTALLED_SERVICES[@]} > 1 ]] && AVAILABLE_SERVICES+=( "$stopall" ) &&  count=$(($count+1))
+[[ $(docker ps -q --filter "status=exited" --filter name=dockerbunker) && ${#STOPPED_SERVICES[@]} > 1 ]] && AVAILABLE_SERVICES+=( "$startall" ) && count=$(($count+1))
+[[ ${#INSTALLED_SERVICES[@]} > 1 ]] && AVAILABLE_SERVICES+=( "$restartall" ) && count=$(($count+1))
+echo ""
+echo "Please select the service you want to manage"
+echo ""
+[[ ${INSTALLED_SERVICES[@]} ]] && echo -e " \e[32mGreen\e[0m: Installed"
+[[ ${CONFIGURED_SERVICES[@]} ]] && echo -e " \e[33mOrange\e[0m: Configured"
+echo ""
+
+COLUMNS=12
+select choice in "${AVAILABLE_SERVICES[@]}"  "$exitmenu"
+do
+	case $choice in
+	"$exitmenu")
+		exit 0
+		;;
+	"$startnginx")
+		echo ""
+		start_nginx
+		say_done
+		sleep 1
+		break
+		;;
+	"$stopnginx")
+		echo ""
+		stop_nginx
+		say_done
+		sleep 1
+		break
+		;;
+	"$restartnginx")
+		prevent_nginx_restart=1
+		echo ""
+		restart_nginx
+		say_done
+		sleep 1
+		break
+		;;
+	"$startall")
+		prevent_nginx_restart=1
+		start_all
+		say_done
+		sleep 1
+		break
+		;;
+	"$stopall")
+		prevent_nginx_restart=1
+		stop_all
+		say_done
+		sleep 1
+		break
+		;;
+	"$destroyall")
+		echo -e "\n\e[3m\xe2\x86\x92 Destroy everything\e[0m"
+		echo ""
+		echo -e "\e[1mReset dockerbunker to its initial state\e[0m"
+		echo ""
+		echo "The following will be removed:"
+		echo ""
+		echo "- All dockerbunker container(s)"
+		echo "- All dockerbunker volume(s)"
+		echo "- All environment file(s)"
+		echo "- All nginx configuration files"
+		echo "- All self-signed certificates (Let's Encrypt certificates will be retained)"
+		echo ""
+		prompt_confirm "Continue?" \
+			&& destroy_all=1 destroy_all
+		say_done
+		exit 0
+		;;
+	$choice)
+		if [[ -z $choice ]];then
+			echo "Please choose a number from 1 to $count"
+		else
+			service="$(echo -e "${choice,,}" | tr -d '[:space:]')"
+			echo ""
+			echo -e "\n\e[3m\xe2\x86\x92 Checking service status"
+			echo ""
+			source "${BASE_DIR}"/data/services/${SERVICES_ARR[$choice]}/${SERVICES_ARR[$choice]}.sh
+			break
+		fi
+		;;
+	esac
+done
+

BIN
preview.png