diff --git a/helpers/cli.py b/helpers/cli.py
index 61a3daa..1bd794f 100644
--- a/helpers/cli.py
+++ b/helpers/cli.py
@@ -24,7 +24,8 @@ class CLI(object):
     EMPTY_CHARACTER = "-"
 
     @classmethod
-    def get_response(cls, validators=None, default=""):
+    def get_response(cls, validators=None, default="", to_lower=True,
+                     error_msg="Sorry, I didn't understand that!"):
     
         while True:
             try:
@@ -38,13 +39,13 @@ class CLI(object):
                      )):
                     break
                 else:
-                    cls.colored_print("Sorry, I didn't understand that!",
+                    cls.colored_print(error_msg,
                                       cls.COLOR_ERROR)
             except ValueError:
                 cls.colored_print("Sorry, I didn't understand that.",
                                   cls.COLOR_ERROR)
     
-        return response.lower()
+        return response.lower() if to_lower else response
 
     @classmethod
     def colored_print(cls, message, color=NO_COLOR):
diff --git a/helpers/command.py b/helpers/command.py
index 509718d..ca41f64 100644
--- a/helpers/command.py
+++ b/helpers/command.py
@@ -63,27 +63,15 @@ class Command:
                 frontend_command = ["docker-compose",
                                     "-f", "docker-compose.frontend.yml",
                                     "-f", "docker-compose.frontend.override.yml",
+                                    "-p", config_object.get_prefix("frontend"),
                                     "build", "--force-rm", "--no-cache",
                                     image_]
 
-                if config.get("docker_prefix", "") != "":
-                    frontend_command.insert(-4, "-p")
-                    frontend_command.insert(-4, config.get("docker_prefix"))
-
                 CLI.run_command(frontend_command, config.get("kobodocker_path"))
 
-            # ToDo When Python3 container is released.
-            # ToDo Move this to `kobocat` build as `kpi` doesn't use it anymore.
-            pull_base_command = ["docker",
-                                 "pull",
-                                 "kobotoolbox/koboform_base"]
-
-            CLI.run_command(pull_base_command, config.get("kobodocker_path"))
-
             if image is None or image == "kf":
                 config["kpi_dev_build_id"] = "{prefix}{timestamp}".format(
-                    prefix="{}.".format(config.get("docker_prefix"))
-                    if config.get("docker_prefix") else "",
+                    prefix=config_object.get_prefix("frontend"),
                     timestamp=str(int(time.time()))
                 )
                 config_object.write_config()
@@ -91,9 +79,14 @@ class Command:
                 build_image("kpi")
 
             if image is None or image == "kc":
+                pull_base_command = ["docker",
+                                     "pull",
+                                     "kobotoolbox/koboform_base"]
+
+                CLI.run_command(pull_base_command, config.get("kobodocker_path"))
+
                 config["kc_dev_build_id"] = "{prefix}{timestamp}".format(
-                    prefix="{}.".format(config.get("docker_prefix"))
-                    if config.get("docker_prefix") else "",
+                    prefix=config_object.get_prefix("frontend"),
                     timestamp=str(int(time.time()))
                 )
                 config_object.write_config()
@@ -106,9 +99,8 @@ class Command:
         config = config_object.get_config()
         command = ["docker-compose",
                    "-f", "docker-compose.frontend.yml",
-                   "-f", "docker-compose.frontend.override.yml"]
-        if config.get("docker_prefix", "") != "":
-            command.extend(['-p', config.get("docker_prefix")])
+                   "-f", "docker-compose.frontend.override.yml",
+                   "-p", config_object.get_prefix("frontend")]
         command.extend(args)
         subprocess.call(command, cwd=config.get("kobodocker_path"))
 
@@ -121,9 +113,8 @@ class Command:
             "docker-compose",
             "-f", "docker-compose.backend.{}.yml".format(backend_role),
             "-f", "docker-compose.backend.{}.override.yml".format(backend_role),
+            "-p", config_object.get_prefix("backend")
         ]
-        if config.get("docker_prefix", "") != "":
-            command.extend(['-p', config.get("docker_prefix")])
         command.extend(args)
         subprocess.call(command, cwd=config.get("kobodocker_path"))
 
@@ -227,22 +218,16 @@ class Command:
             backend_command = ["docker-compose",
                                "-f", "docker-compose.backend.{}.yml".format(backend_role),
                                "-f", "docker-compose.backend.{}.override.yml".format(backend_role),
+                               "-p", config_object.get_prefix("backend"),
                                "logs", "-f"]
-            if config.get("docker_prefix", "") != "":
-                backend_command.insert(-2, "-p")
-                backend_command.insert(-2, config.get("docker_prefix"))
-
             CLI.run_command(backend_command, config.get("kobodocker_path"), True)
 
         if config_object.frontend:
             frontend_command = ["docker-compose",
                                 "-f", "docker-compose.frontend.yml",
                                 "-f", "docker-compose.frontend.override.yml",
+                                "-p", config_object.get_prefix("frontend"),
                                 "logs", "-f"]
-            if config.get("docker_prefix", "") != "":
-                frontend_command.insert(-2, "-p")
-                frontend_command.insert(-2, config.get("docker_prefix"))
-
             CLI.run_command(frontend_command, config.get("kobodocker_path"), True)
 
     @classmethod
@@ -267,10 +252,8 @@ class Command:
         nginx_stop_command = ["docker-compose",
                               "-f", "docker-compose.frontend.yml",
                               "-f", "docker-compose.frontend.override.yml",
+                              "-p", config_object.get_prefix("frontend"),
                               "stop", "nginx"]
-        if config.get("docker_prefix", "") != "":
-            nginx_stop_command.insert(-2, "-p")
-            nginx_stop_command.insert(-2, config.get("docker_prefix"))
 
         CLI.run_command(nginx_stop_command, config.get("kobodocker_path"))
 
@@ -281,10 +264,8 @@ class Command:
 
         frontend_command = ["docker-compose",
                             "-f", "docker-compose.maintenance.yml",
+                            "-p", config_object.get_prefix("maintenance"),
                             "up", "-d", "maintenance"]
-        if config.get("docker_prefix", "") != "":
-            frontend_command.insert(-3, "-p")
-            frontend_command.insert(-3, config.get("docker_prefix"))
 
         CLI.run_command(frontend_command, config.get("kobodocker_path"))
         CLI.colored_print("Maintenance mode has been started",
@@ -316,8 +297,9 @@ class Command:
                 not config_object.multi_servers:
             ports.append(nginx_port)
 
-        if not frontend_only or config_object.master_backend or \
-                config_object.slave_backend:
+        if (not frontend_only or config_object.master_backend or
+                config_object.slave_backend) and \
+                config_object.expose_backend_ports:
             ports.append(config.get("postgresql_port", 5432))
             ports.append(config.get("mongo_port", 27017))
             ports.append(config.get("redis_main_port", 6379))
@@ -343,11 +325,8 @@ class Command:
                                    "-f",
                                    "docker-compose.backend.{}.override.yml".format(
                                        backend_role),
+                                   "-p", config_object.get_prefix("backend"),
                                    "up", "-d"]
-                if config.get("docker_prefix", "") != "":
-                    backend_command.insert(-2, "-p")
-                    backend_command.insert(-2, config.get("docker_prefix"))
-
                 CLI.run_command(backend_command, config.get("kobodocker_path"))
 
         # If this was previously a shared-database setup, migrate to separate
@@ -359,12 +338,9 @@ class Command:
             frontend_command = ["docker-compose",
                                 "-f", "docker-compose.frontend.yml",
                                 "-f", "docker-compose.frontend.override.yml",
+                                "-p", config_object.get_prefix("frontend"),
                                 "up", "-d"]
 
-            if config.get("docker_prefix", "") != "":
-                frontend_command.insert(-2, "-p")
-                frontend_command.insert(-2, config.get("docker_prefix"))
-
             if config.get('maintenance_enabled', False):
                 cls.start_maintenance()
                 # Start all front-end services except the non-maintenance NGINX
@@ -408,12 +384,9 @@ class Command:
             maintenance_down_command = [
                 "docker-compose",
                 "-f", "docker-compose.maintenance.yml",
+                "-p", config_object.get_prefix("maintenance"),
                 "down"]
 
-            if config.get("docker_prefix", "") != "":
-                maintenance_down_command.insert(-1, "-p")
-                maintenance_down_command.insert(-1, config.get("docker_prefix"))
-
             CLI.run_command(maintenance_down_command,
                             config.get("kobodocker_path"))
 
@@ -421,10 +394,8 @@ class Command:
             frontend_command = ["docker-compose",
                                 "-f", "docker-compose.frontend.yml",
                                 "-f", "docker-compose.frontend.override.yml",
+                                "-p", config_object.get_prefix("frontend"),
                                 "down"]
-            if config.get("docker_prefix", "") != "":
-                frontend_command.insert(-1, "-p")
-                frontend_command.insert(-1, config.get("docker_prefix"))
             CLI.run_command(frontend_command, config.get("kobodocker_path"))
 
             # Stop reverse proxy if user uses it.
@@ -444,14 +415,11 @@ class Command:
                     "docker-compose.backend.{}.yml".format(backend_role),
                     "-f",
                     "docker-compose.backend.{}.override.yml".format(backend_role),
+                    "-p", config_object.get_prefix("backend"),
                     "down"
                 ]
-                if config.get("docker_prefix", "") != "":
-                    backend_command.insert(-1, "-p")
-                    backend_command.insert(-1, config.get("docker_prefix"))
                 CLI.run_command(backend_command, config.get("kobodocker_path"))
 
-
         if output:
             CLI.colored_print("KoBoToolbox has been stopped", CLI.COLOR_SUCCESS)
 
@@ -468,12 +436,9 @@ class Command:
             maintenance_down_command = [
                 "docker-compose",
                 "-f", "docker-compose.maintenance.yml",
+                "-p", config_object.get_prefix("maintenance"),
                 "down"]
 
-            if config.get("docker_prefix", "") != "":
-                maintenance_down_command.insert(-1, "-p")
-                maintenance_down_command.insert(-1, config.get("docker_prefix"))
-
             CLI.run_command(maintenance_down_command,
                             config.get("kobodocker_path"))
 
@@ -481,10 +446,8 @@ class Command:
             frontend_command = ["docker-compose",
                                 "-f", "docker-compose.frontend.yml",
                                 "-f", "docker-compose.frontend.override.yml",
+                                "-p", config_object.get_prefix("frontend"),
                                 "up", "-d", "nginx"]
-            if config.get("docker_prefix", "") != "":
-                frontend_command.insert(-3, "-p")
-                frontend_command.insert(-3, config.get("docker_prefix"))
             CLI.run_command(frontend_command, config.get("kobodocker_path"))
 
             CLI.colored_print("Maintenance mode has been stopped",
diff --git a/helpers/config.py b/helpers/config.py
index f87d1e2..f80a21f 100644
--- a/helpers/config.py
+++ b/helpers/config.py
@@ -18,9 +18,9 @@ from helpers.singleton import Singleton
 
 
 class Config:
-
     CONFIG_FILE = ".run.conf"
     UNIQUE_ID_FILE = ".uniqid"
+    UPSERT_DB_USERS_TRIGGER_FILE = ".upsert_db_users"
     TRUE = "1"
     FALSE = "2"
     LETSENCRYPT_DOCKER_DIR = "nginx-certbot"
@@ -28,10 +28,9 @@ class Config:
     DEFAULT_PROXY_PORT = "8080"
     DEFAULT_NGINX_PORT = "80"
     DEFAULT_NGINX_HTTPS_PORT = "443"
-
-    KOBO_DOCKER_BRANCH = '2.020.18'
+    KOBO_DOCKER_BRANCH = '2.020.19'
     KOBO_INSTALL_BRANCH = 'master'
-    KOBO_INSTALL_VERSION = '2.1.0'
+    KOBO_INSTALL_VERSION = '2.2.0'
 
     # Maybe overkill. Use this class as a singleton to get the same configuration
     # for each instantiation.
@@ -76,6 +75,10 @@ class Config:
         """
         return self.__config.get("use_aws") == Config.TRUE
 
+    @property
+    def expose_backend_ports(self):
+        return self.__config.get("expose_backend_ports") == Config.TRUE
+
     def get_env_files_path(self):
         return os.path.realpath(os.path.normpath(os.path.join(
             self.__config.get("kobodocker_path"),
@@ -90,23 +93,24 @@ class Config:
             Config.LETSENCRYPT_DOCKER_DIR
         )))
 
-    @property
-    def master_backend(self):
-        """
-        Checks whether setup is running on a master backend server
-        :return: bool
-        """
-        return self.multi_servers and \
-               self.__config.get("backend_server_role") == "master"
+    def get_prefix(self, role):
+        roles = {
+            'frontend': 'kobofe',
+            'backend': 'kobobe',
+            'maintenance': 'kobomaintenance'
+        }
 
-    @property
-    def slave_backend(self):
-        """
-        Checks whether setup is running on a slave backend server
-        :return: bool
-        """
-        return self.multi_servers and \
-               self.__config.get("backend_server_role") == "slave"
+        try:
+            prefix_ = roles[role]
+        except KeyError:
+            CLI.colored_print("Invalid composer file", CLI.COLOR_ERROR)
+            sys.exit(-1)
+
+        if not self.__config.get("docker_prefix"):
+            return prefix_
+
+        return "{}-{}".format(self.__config.get("docker_prefix"),
+                              prefix_)
 
     @property
     def backend_questions(self):
@@ -116,82 +120,6 @@ class Config:
         """
         return not self.multi_servers or not self.frontend
 
-    @property
-    def first_time(self):
-        """
-        Checks whether setup is running for the first time
-        :return: bool
-        """
-        if self.__first_time is None:
-            self.__first_time = self.__config.get("date_created") is None
-        return self.__first_time
-
-    @property
-    def frontend(self):
-        """
-        Checks whether setup is running on a frontend server
-        :return: bool
-        """
-        return not self.multi_servers or \
-            self.__config.get("server_role") == "frontend"
-
-    @property
-    def frontend_questions(self):
-        """
-        Checks whether questions are frontend only
-        :return: bool
-        """
-        return not self.multi_servers or self.frontend
-
-    @property
-    def dev_mode(self):
-        return self.__config.get("dev_mode") == Config.TRUE
-
-    @property
-    def is_secure(self):
-        return self.__config.get("https") == Config.TRUE
-
-    def init_letsencrypt(self):
-        if self.use_letsencrypt:
-            reverse_proxy_path = self.get_letsencrypt_repo_path()
-            reverse_proxy_command = [
-                "/bin/bash",
-                "init-letsencrypt.sh"
-            ]
-            CLI.run_command(reverse_proxy_command, reverse_proxy_path)
-
-    @property
-    def use_letsencrypt(self):
-        return self.local_install is False and self.__config["use_letsencrypt"] == Config.TRUE
-
-    @property
-    def local_install(self):
-        """
-        Checks whether installation is for `Workstation`s
-        :return: bool
-        """
-        return self.__config.get("local_installation") == Config.TRUE
-
-    @property
-    def multi_servers(self):
-        """
-        Checks whether installation is for separate frontend and backend servers
-        :return: bool
-        """
-        return self.__config.get("multi") == Config.TRUE
-
-    @property
-    def proxy(self):
-        """
-        Checks whether installation is using a proxy or a load balancer
-        :return: bool
-        """
-        return self.__config.get("proxy") == Config.TRUE
-
-    @property
-    def staging_mode(self):
-        return self.__config.get("staging_mode") == Config.TRUE
-
     def build(self):
         """
         Build configuration based on user's answer
@@ -208,6 +136,10 @@ class Config:
             config = self.get_config_template()
             config.update(self.__config)
 
+            # TODO This method does the same thing as lines below but does not keep
+            # retro-compatibility. Remove this code if it'S not need anymore
+            # self.__config = self.__upgrade_kc_db(config)
+
             # If the configuration came from a previous version that had a
             # single Postgres database, we need to make sure the new
             # `kc_postgres_db` is set to the name of that single database,
@@ -233,21 +165,24 @@ class Config:
                         self.__questions_roles()
                         if self.frontend:
                             self.__questions_private_routes()
+                    else:
+                        self.__reset(private_dns=True)
 
                 if self.frontend_questions:
                     self.__questions_public_routes()
                     self.__questions_https()
-                    self.__question_reverse_proxy()
+                    self.__questions_reverse_proxy()
 
             if self.frontend_questions:
                 self.__questions_smtp()
                 self.__questions_super_user_credentials()
 
             if self.advanced_options:
-
                 self.__questions_docker_prefix()
                 self.__questions_dev_mode()
                 self.__questions_postgres()
+                self.__questions_mongo()
+                self.__questions_redis()
                 self.__questions_ports()
 
                 if self.frontend_questions:
@@ -255,6 +190,8 @@ class Config:
                     self.__questions_google()
                     self.__questions_raven()
                     self.__questions_uwsgi()
+            else:
+                self.__secure_mongo()
 
             self.__questions_backup()
 
@@ -262,21 +199,193 @@ class Config:
             self.write_config()
             return config
 
+    @property
+    def dev_mode(self):
+        return self.__config.get("dev_mode") == Config.TRUE
+
+    @property
+    def first_time(self):
+        """
+        Checks whether setup is running for the first time
+        :return: bool
+        """
+        if self.__first_time is None:
+            self.__first_time = self.__config.get("date_created") is None
+        return self.__first_time
+
+    @property
+    def frontend(self):
+        """
+        Checks whether setup is running on a frontend server
+        :return: bool
+        """
+        return not self.multi_servers or \
+               self.__config.get("server_role") == "frontend"
+
+    @property
+    def frontend_questions(self):
+        """
+        Checks whether questions are frontend only
+        :return: bool
+        """
+        return not self.multi_servers or self.frontend
+
+    @classmethod
+    def generate_password(cls):
+        """
+        Generate 12 characters long random password
+        :return: str
+        """
+        characters = string.ascii_letters \
+                     + "!$%+-_^~@#{}[]()/\'\"`~,;:.<>" \
+                     + string.digits
+        required_chars_count = 12
+
+        return ''.join(choice(characters)
+                       for _ in range(required_chars_count))
+
     def get_config(self):
         return self.__config
 
-    @staticmethod
-    def generate_password():
+    @classmethod
+    def get_config_template(cls):
+
+        primary_ip = Network.get_primary_ip()
+
+        return {
+            "advanced": "2",
+            "workers_max": "2",
+            "workers_start": "1",
+            "debug": Config.FALSE,
+            "kobodocker_path": os.path.realpath(os.path.normpath(os.path.join(
+                os.path.dirname(os.path.realpath(__file__)),
+                "..",
+                "..",
+                "kobo-docker"))
+            ),
+            "internal_domain_name": "docker.internal",
+            "private_domain_name": "kobo.private",
+            "public_domain_name": "kobo.local",
+            "kpi_subdomain": "kf",
+            "kc_subdomain": "kc",
+            "ee_subdomain": "ee",
+            "kc_postgres_db": "kobocat",
+            "kpi_postgres_db": "koboform",
+            "postgres_user": "kobo",
+            "postgres_password": Config.generate_password(),
+            "kc_path": "",
+            "kpi_path": "",
+            "super_user_username": "super_admin",
+            "super_user_password": Config.generate_password(),
+            "postgres_replication_password": Config.generate_password(),
+            "use_aws": Config.FALSE,
+            "use_private_dns": Config.FALSE,
+            "master_backend_ip": primary_ip,
+            "local_interface_ip": primary_ip,
+            "multi": Config.FALSE,
+            "postgres_settings": Config.FALSE,
+            "postgres_ram": "2",
+            "postgres_profile": "Mixed",
+            "postgres_max_connections": "100",
+            "postgres_hard_drive_type": "hdd",
+            "postgres_settings_content": "",
+            "enketo_api_token": binascii.hexlify(os.urandom(60)).decode("utf-8"),
+            "enketo_encryption_key": binascii.hexlify(os.urandom(60)).decode("utf-8"),
+            "django_secret_key": binascii.hexlify(os.urandom(24)).decode("utf-8"),
+            "use_backup": Config.FALSE,
+            "kobocat_media_schedule": "0 0 * * 0",
+            "mongo_backup_schedule": "0 1 * * 0",
+            "postgres_backup_schedule": "0 2 * * 0",
+            "redis_backup_schedule": "0 3 * * 0",
+            "aws_backup_bucket_name": "",
+            "aws_backup_yearly_retention": "2",
+            "aws_backup_monthly_retention": "12",
+            "aws_backup_weekly_retention": "4",
+            "aws_backup_daily_retention": "30",
+            "aws_mongo_backup_minimum_size": "50",
+            "aws_postgres_backup_minimum_size": "50",
+            "aws_redis_backup_minimum_size": "5",
+            "aws_backup_upload_chunk_size": "15",
+            "aws_backup_bucket_deletion_rule_enabled": Config.FALSE,
+            "backend_server_role": "master",
+            "use_letsencrypt": Config.TRUE,
+            "proxy": Config.TRUE,
+            "https": Config.TRUE,
+            "nginx_proxy_port": Config.DEFAULT_PROXY_PORT,
+            "exposed_nginx_docker_port": Config.DEFAULT_NGINX_PORT,
+            "expose_backend_ports": Config.FALSE,
+            "postgresql_port": "5432",
+            "mongo_port": "27017",
+            "redis_main_port": "6379",
+            "redis_cache_port": "6380",
+            "local_installation": Config.FALSE,
+            "block_common_http_ports": Config.TRUE,
+            "npm_container": Config.TRUE,
+            "mongo_root_username": "root",
+            "mongo_root_password": Config.generate_password(),
+            "mongo_user_username": "kobo",
+            "mongo_user_password": Config.generate_password(),
+            "redis_password": Config.generate_password(),
+        }
+
+    def get_service_names(self):
+        service_list_command = ["docker-compose",
+                                "-f", "docker-compose.frontend.yml",
+                                "-f", "docker-compose.frontend.override.yml",
+                                "config", "--services"]
+
+        services = CLI.run_command(service_list_command, self.__config["kobodocker_path"])
+        return services.strip().split('\n')
+
+    @property
+    def is_secure(self):
+        return self.__config.get("https") == Config.TRUE
+
+    def init_letsencrypt(self):
+        if self.use_letsencrypt:
+            reverse_proxy_path = self.get_letsencrypt_repo_path()
+            reverse_proxy_command = [
+                "/bin/bash",
+                "init-letsencrypt.sh"
+            ]
+            CLI.run_command(reverse_proxy_command, reverse_proxy_path)
+
+    @property
+    def local_install(self):
         """
-        Generate random password between 8 to 16 characters
-        :return: str
+        Checks whether installation is for `Workstation`s
+        :return: bool
         """
-        characters = string.ascii_letters + "!$%+-^~" + string.digits
-        return "".join(choice(characters) for x in range(randint(10, 16)))
+        return self.__config.get("local_installation") == Config.TRUE
 
     def maintenance(self):
         self.__questions_maintenance()
 
+    @property
+    def master_backend(self):
+        """
+        Checks whether setup is running on a master backend server
+        :return: bool
+        """
+        return self.multi_servers and \
+               self.__config.get("backend_server_role") == "master"
+
+    @property
+    def multi_servers(self):
+        """
+        Checks whether installation is for separate frontend and backend servers
+        :return: bool
+        """
+        return self.__config.get("multi") == Config.TRUE
+
+    @property
+    def proxy(self):
+        """
+        Checks whether installation is using a proxy or a load balancer
+        :return: bool
+        """
+        return self.__config.get("proxy") == Config.TRUE
+
     def read_config(self):
         """
         Reads config from file `Config.CONFIG_FILE` if exists
@@ -315,6 +424,28 @@ class Config:
 
         return unique_id
 
+    @property
+    def slave_backend(self):
+        """
+        Checks whether setup is running on a slave backend server
+        :return: bool
+        """
+        return self.multi_servers and \
+               self.__config.get("backend_server_role") == "slave"
+
+    @property
+    def staging_mode(self):
+        return self.__config.get("staging_mode") == Config.TRUE
+
+    @property
+    def use_letsencrypt(self):
+        return self.local_install is False and \
+               self.__config["use_letsencrypt"] == Config.TRUE
+
+    @property
+    def use_private_dns(self):
+        return self.__config["use_private_dns"] == Config.TRUE
+
     def write_config(self):
         """
         Writes config to file `Config.CONFIG_FILE`.
@@ -349,15 +480,6 @@ class Config:
 
         return True
 
-    def get_service_names(self):
-        service_list_command = ["docker-compose",
-                                "-f", "docker-compose.frontend.yml",
-                                "-f", "docker-compose.frontend.override.yml",
-                                "config", "--services"]
-
-        services = CLI.run_command(service_list_command, self.__config["kobodocker_path"])
-        return services.strip().split('\n')
-
     def __create_directory(self):
         """
         Create repository directory if it doesn't exist.
@@ -462,80 +584,6 @@ class Config:
             self.__config["local_interface_ip"] = interfaces[self.__config.get("local_interface")]
             self.__config["master_backend_ip"] = self.__config.get("local_interface_ip")
 
-    @classmethod
-    def get_config_template(cls):
-
-        primary_ip = Network.get_primary_ip()
-
-        return {
-            "workers_max": "2",
-            "workers_start": "1",
-            "debug": Config.FALSE,
-            "kobodocker_path": os.path.realpath(os.path.normpath(os.path.join(
-                os.path.dirname(os.path.realpath(__file__)),
-                "..",
-                "..",
-                "kobo-docker"))
-            ),
-            "internal_domain_name": "docker.internal",
-            "private_domain_name": "kobo.private",
-            "public_domain_name": "kobo.local",
-            "kpi_subdomain": "kf",
-            "kc_subdomain": "kc",
-            "ee_subdomain": "ee",
-            "kc_postgres_db": "kobocat",
-            "kpi_postgres_db": "koboform",
-            "postgres_user": "kobo",
-            "postgres_password": Config.generate_password(),
-            "kc_path": "",
-            "kpi_path": "",
-            "super_user_username": "super_admin",
-            "super_user_password": Config.generate_password(),
-            "postgres_replication_password": Config.generate_password(),
-            "use_aws": Config.FALSE,
-            "use_private_dns": Config.FALSE,
-            "master_backend_ip": primary_ip,
-            "local_interface_ip": primary_ip,
-            "multi": Config.FALSE,
-            "postgres_settings": Config.FALSE,
-            "postgres_ram": "2",
-            "postgres_profile": "Mixed",
-            "postgres_max_connections": "100",
-            "postgres_hard_drive_type": "hdd",
-            "postgres_settings_content": "",
-            "enketo_api_token": binascii.hexlify(os.urandom(60)).decode("utf-8"),
-            "enketo_encryption_key": binascii.hexlify(os.urandom(60)).decode("utf-8"),
-            "django_secret_key": binascii.hexlify(os.urandom(24)).decode("utf-8"),
-            "use_backup": Config.FALSE,
-            "kobocat_media_schedule": "0 0 * * 0",
-            "mongo_backup_schedule": "0 1 * * 0",
-            "postgres_backup_schedule": "0 2 * * 0",
-            "redis_backup_schedule": "0 3 * * 0",
-            "aws_backup_bucket_name": "",
-            "aws_backup_yearly_retention": "2",
-            "aws_backup_monthly_retention": "12",
-            "aws_backup_weekly_retention": "4",
-            "aws_backup_daily_retention": "30",
-            "aws_mongo_backup_minimum_size": "50",
-            "aws_postgres_backup_minimum_size": "50",
-            "aws_redis_backup_minimum_size": "5",
-            "aws_backup_upload_chunk_size": "15",
-            "aws_backup_bucket_deletion_rule_enabled": Config.FALSE,
-            "backend_server_role": "master",
-            "use_letsencrypt": Config.TRUE,
-            "proxy": Config.TRUE,
-            "https": Config.TRUE,
-            "nginx_proxy_port": Config.DEFAULT_PROXY_PORT,
-            "exposed_nginx_docker_port": Config.DEFAULT_NGINX_PORT,
-            "postgresql_port": "5432",
-            "mongo_port": "27017",
-            "redis_main_port": "6379",
-            "redis_cache_port": "6380",
-            "local_installation": Config.FALSE,
-            "block_common_http_ports": Config.TRUE,
-            "npm_container": Config.TRUE,
-        }
-
     def __questions_advanced_options(self):
         """
         Asks if user wants to see advanced options
@@ -747,15 +795,14 @@ class Config:
                 # Create an unique id to build fresh image when starting containers
                 if (self.__config.get("kc_dev_build_id", "") == "" or
                         self.__config.get("kc_path") != self.__config.get("kc_path")):
-
                     self.__config["kc_dev_build_id"] = "{prefix}{timestamp}".format(
-                        prefix="{}.".format(self.__config.get("docker_prefix")) if self.__config.get("docker_prefix") else "",
+                        prefix=self.get_prefix("frontend"),
                         timestamp=str(int(time.time()))
                     )
                 if (self.__config.get("kpi_dev_build_id", "") == "" or
                         self.__config.get("kpi_path") != self.__config.get("kpi_path")):
                     self.__config["kpi_dev_build_id"] = "{prefix}{timestamp}".format(
-                        prefix="{}.".format(self.__config.get("docker_prefix")) if self.__config.get("docker_prefix") else "",
+                        prefix=self.get_prefix("frontend"),
                         timestamp=str(int(time.time()))
                     )
                 if self.dev_mode:
@@ -774,7 +821,7 @@ class Config:
                                                                       self.__config.get("npm_container", Config.TRUE))
             else:
                 # Force reset paths
-                self.__reset_dev_mode(self.staging_mode)
+                self.__reset(dev=True, reset_nginx_port=self.staging_mode)
 
     def __questions_docker_prefix(self):
         """
@@ -807,11 +854,16 @@ class Config:
                                                   self.__config.get("https", Config.TRUE))
 
         if self.is_secure:
-            CLI.colored_print("╔════════════════════════════════════════════════════════════════════╗", CLI.COLOR_WARNING)
-            CLI.colored_print("║ Please note that certificates must be installed on a reverse-proxy ║", CLI.COLOR_WARNING)
-            CLI.colored_print("║ or a load balancer.                                                ║", CLI.COLOR_WARNING)
-            CLI.colored_print("║ KoBoInstall can install one, if needed.                            ║", CLI.COLOR_WARNING)
-            CLI.colored_print("╚════════════════════════════════════════════════════════════════════╝", CLI.COLOR_WARNING)
+            CLI.colored_print("╔════════════════════════════════════════════════════════════════════╗",
+                              CLI.COLOR_WARNING)
+            CLI.colored_print("║ Please note that certificates must be installed on a reverse-proxy ║",
+                              CLI.COLOR_WARNING)
+            CLI.colored_print("║ or a load balancer.                                                ║",
+                              CLI.COLOR_WARNING)
+            CLI.colored_print("║ KoBoInstall can install one, if needed.                            ║",
+                              CLI.COLOR_WARNING)
+            CLI.colored_print("╚════════════════════════════════════════════════════════════════════╝",
+                              CLI.COLOR_WARNING)
 
     def __questions_installation_type(self):
         """
@@ -825,12 +877,7 @@ class Config:
                                                                self.__config.get("local_installation", Config.FALSE))
         if self.local_install:
             # Reset previous choices, in case server role is not the same.
-            self.__config["multi"] = Config.FALSE
-            self.__config["use_private_dns"] = Config.FALSE
-            self.__config["https"] = Config.FALSE
-            self.__config["proxy"] = Config.FALSE
-            self.__config["nginx_proxy_port"] = Config.DEFAULT_NGINX_PORT
-            self.__config["use_letsencrypt"] = Config.FALSE
+            self.__reset(local_install=True, private_dns=True)
 
     def __questions_maintenance(self):
         if self.first_time:
@@ -840,13 +887,16 @@ class Config:
             sys.exit(1)
 
         def _round_nearest_quarter(dt):
+
+            minutes = int(15 * round((float(dt.minute) + float(dt.second) / 60) / 15))
             return datetime(dt.year, dt.month, dt.day, dt.hour,
-                            int(15 * round((float(dt.minute) + float(dt.second) / 60) / 15)))
+                            minutes if minutes < 60 else 0)
 
         CLI.colored_print("How long do you plan to this maintenance will last?",
                           CLI.COLOR_SUCCESS)
-        self.__config["maintenance_eta"] = CLI.get_response(r"~^[\w\ ]+$",
-                                                            self.__config.get("maintenance_eta", "2 hours"))
+        self.__config["maintenance_eta"] = CLI.get_response(
+            r"~^[\w\ ]+$",
+            self.__config.get("maintenance_eta", "2 hours"))
 
         date_start = _round_nearest_quarter(datetime.utcnow())
         iso_format = '%Y%m%dT%H%M'
@@ -857,16 +907,103 @@ class Config:
         self.__config["maintenance_date_iso"] = self.__config["maintenance_date_iso"].upper()
 
         date_iso = self.__config["maintenance_date_iso"]
-        self.__config["maintenance_date_str"] = datetime.strptime(date_iso, iso_format).\
+        self.__config["maintenance_date_str"] = datetime.strptime(date_iso, iso_format). \
             strftime('%A,&nbsp;%B&nbsp;%d&nbsp;at&nbsp;%H:%M&nbsp;GMT')
 
-        self.__config["maintenance_email"] = CLI.colored_input("Contact during maintenance?",
-                                                               CLI.COLOR_SUCCESS,
-                                                               self.__config.get(
-                                                                   "maintenance_email",
-                                                                   self.__config.get("default_from_email")))
+        self.__config["maintenance_email"] = CLI.colored_input(
+            "Contact during maintenance?",
+            CLI.COLOR_SUCCESS,
+            self.__config.get("maintenance_email",
+                              self.__config.get("default_from_email")))
         self.write_config()
 
+    def __questions_mongo(self):
+        """
+        Mongo credentials.
+        """
+        mongo_user_username = self.__config["mongo_user_username"]
+        mongo_user_password = self.__config["mongo_user_password"]
+        mongo_root_username = self.__config["mongo_root_username"]
+        mongo_root_password = self.__config["mongo_root_password"]
+
+        CLI.colored_print("MongoDB root's username?",
+                          CLI.COLOR_SUCCESS)
+        self.__config["mongo_root_username"] = CLI.get_response(
+            r"~^\w+$",
+            self.__config.get("mongo_root_username"),
+            to_lower=False)
+
+        CLI.colored_print("MongoDB root's password?", CLI.COLOR_SUCCESS)
+        self.__config["mongo_root_password"] = CLI.get_response(
+            r"~^.{8,}$",
+            self.__config.get("mongo_root_password"),
+            to_lower=False,
+            error_msg='Too short. 8 characters minimum.')
+
+        CLI.colored_print("MongoDB user's username?",
+                          CLI.COLOR_SUCCESS)
+        self.__config["mongo_user_username"] = CLI.get_response(
+            r"~^\w+$",
+            self.__config.get("mongo_user_username"),
+            to_lower=False)
+
+        CLI.colored_print("MongoDB user's password?", CLI.COLOR_SUCCESS)
+        self.__config["mongo_user_password"] = CLI.get_response(
+            r"~^.{8,}$",
+            self.__config.get("mongo_user_password"),
+            to_lower=False,
+            error_msg='Too short. 8 characters minimum.')
+
+        if (self.__config.get("mongo_secured") != Config.TRUE or
+            mongo_user_username != self.__config.get("mongo_user_username") or
+            mongo_user_password != self.__config.get("mongo_user_password") or
+            mongo_root_username != self.__config.get("mongo_root_username") or
+            mongo_root_password != self.__config.get("mongo_root_password")) and \
+                not self.first_time:
+
+            # Because chances are high we cannot communicate with DB
+            # (e.g ports not exposed, containers down), we delegate the task
+            # to MongoDB container to update (create/delete) users.
+            # (see. `kobo-docker/mongo/upsert_users.sh`)
+            # We have to transmit old users (and their respective DB) to
+            # MongoDB to let it know which users need to be deleted.
+
+            # `content` will be read by MongoDB container at next boot
+            # It should contains users to delete if any.
+            # Its format should be: `<user><TAB><database>`
+            content = ''
+
+            if (mongo_user_username != self.__config.get("mongo_user_username") or
+                    mongo_root_username != self.__config.get("mongo_root_username")):
+
+                CLI.colored_print("╔══════════════════════════════════════════════════════╗",
+                                  CLI.COLOR_WARNING)
+                CLI.colored_print("║ MongoDB root's and/or user's usernames have changed! ║",
+                                  CLI.COLOR_WARNING)
+                CLI.colored_print("╚══════════════════════════════════════════════════════╝",
+                                  CLI.COLOR_WARNING)
+                CLI.colored_print("Do you want to remove old users?", CLI.COLOR_SUCCESS)
+                CLI.colored_print("\t1) Yes")
+                CLI.colored_print("\t2) No")
+                delete_users = CLI.get_response([Config.TRUE, Config.FALSE], Config.TRUE)
+
+                if delete_users == Config.TRUE:
+                    usernames_by_db = {
+                        mongo_user_username: 'formhub',
+                        mongo_root_username: 'admin'
+                    }
+                    for username, db in usernames_by_db.items():
+                        if username != "":
+                            content += "{cr}{username}\t{db}".format(
+                                cr="\n" if content else "",
+                                username=username,
+                                db=db
+                            )
+
+            self.__write_upsert_db_users_trigger_file(content, 'mongo')
+
+        self.__config["mongo_secured"] = Config.TRUE
+
     def __questions_multi_servers(self):
         """
         Asks if installation is for only one server
@@ -885,23 +1022,114 @@ class Config:
 
         Settings can be tweaked thanks to pgconfig.org API
         """
-        self.__config["kc_postgres_db"] = CLI.colored_input("KoBoCat PostgreSQL database", CLI.COLOR_SUCCESS,
-                                                            self.__config.get("postgres_db",
-                                                                              self.__config.get("kc_postgres_db")))
-        self.__config["kpi_postgres_db"] = CLI.colored_input("KPI PostgreSQL database", CLI.COLOR_SUCCESS,
-                                                             self.__config.get("kpi_postgres_db"))
-        while self.__config["kc_postgres_db"] == self.__config["kpi_postgres_db"]:
-            self.__config["kpi_postgres_db"] = CLI.colored_input(
-                "KPI must use its own PostgreSQL database, not share one with KoBoCAT. Please enter another database",
+        CLI.colored_print("KoBoCat PostgreSQL database name?",
+                          CLI.COLOR_SUCCESS)
+        kc_postgres_db = CLI.get_response(
+            r"~^\w+$",
+            self.__config.get("kc_postgres_db"),
+            to_lower=False
+        )
+
+        CLI.colored_print("KPI PostgreSQL database name?",
+                          CLI.COLOR_SUCCESS)
+        kpi_postgres_db = CLI.get_response(
+            r"~^\w+$",
+            self.__config.get("kpi_postgres_db"),
+            to_lower=False)
+
+        while kpi_postgres_db == kc_postgres_db:
+            kpi_postgres_db = CLI.colored_input(
+                "KPI must use its own PostgreSQL database, not share one with "
+                "KoBoCAT. Please enter another database",
                 CLI.COLOR_ERROR,
                 Config.get_config_template()["kpi_postgres_db"],
             )
-        self.__config["postgres_user"] = CLI.colored_input("Postgres user", CLI.COLOR_SUCCESS,
-                                                           self.__config.get("postgres_user"))
-        self.__config["postgres_password"] = CLI.colored_input("Postgres password", CLI.COLOR_SUCCESS,
-                                                               self.__config.get("postgres_password"))
 
-        if self.backend_questions and self.advanced_options:
+        if (kc_postgres_db != self.__config["kc_postgres_db"] or
+                (kpi_postgres_db != self.__config["kpi_postgres_db"] and
+                 self.__config.get("two_databases") == Config.TRUE)):
+            CLI.colored_print("╔══════════════════════════════════════════════════════╗",
+                              CLI.COLOR_WARNING)
+            CLI.colored_print("║ PostgreSQL database names have changed!              ║",
+                              CLI.COLOR_WARNING)
+            CLI.colored_print("║ KoBoInstall does not support database name changes   ║",
+                              CLI.COLOR_WARNING)
+            CLI.colored_print("║ after database initialization.                       ║",
+                              CLI.COLOR_WARNING)
+            CLI.colored_print("║ Data will not appear in KPI and/or KoBoCat.          ║",
+                              CLI.COLOR_WARNING)
+            CLI.colored_print("╚══════════════════════════════════════════════════════╝",
+                              CLI.COLOR_WARNING)
+
+            CLI.colored_print("Do you want to continue?", CLI.COLOR_SUCCESS)
+            CLI.colored_print("\t1) Yes")
+            CLI.colored_print("\t2) No")
+
+            if CLI.get_response([Config.TRUE, Config.FALSE], Config.FALSE) == Config.FALSE:
+                sys.exit()
+
+        self.__config["kc_postgres_db"] = kc_postgres_db
+        self.__config["kpi_postgres_db"] = kpi_postgres_db
+        self.__config["two_databases"] = Config.TRUE
+
+        postgres_user = self.__config["postgres_user"]
+        postgres_password = self.__config["postgres_password"]
+
+        CLI.colored_print("PostgreSQL user's username?",
+                          CLI.COLOR_SUCCESS)
+        self.__config["postgres_user"] = CLI.get_response(
+            r"~^\w+$",
+            self.__config.get("postgres_user"),
+            to_lower=False)
+
+        CLI.colored_print("PostgreSQL user's password?", CLI.COLOR_SUCCESS)
+        self.__config["postgres_password"] = CLI.get_response(
+            r"~^.{8,}$",
+            self.__config.get("postgres_password"),
+            to_lower=False,
+            error_msg='Too short. 8 characters minimum.')
+
+        if (postgres_user != self.__config.get("postgres_user") or
+            postgres_password != self.__config.get("postgres_password")) and \
+                not self.first_time:
+
+            # Because chances are high we cannot communicate with DB
+            # (e.g ports not exposed, containers down), we delegate the task
+            # to PostgreSQL container to update (create/delete) users.
+            # (see. `kobo-docker/postgres/shared/upsert_users.sh`)
+            # We need to transmit old user to PostgreSQL to let it know
+            # what was the previous username to log in before performing any
+            # action.
+
+            # `content` will be read by PostgreSQL container at next boot
+            # It should always contain previous username and a boolean for deletion.
+            # Its format should be: `<user><TAB><boolean>`
+            content = '{username}\tfalse'.format(username=postgres_user)
+
+            if postgres_user != self.__config.get("postgres_user"):
+
+                CLI.colored_print("PostgreSQL user's username has changed!", CLI.COLOR_WARNING)
+                CLI.colored_print("Do you want to remove old user?", CLI.COLOR_SUCCESS)
+                CLI.colored_print("\t1) Yes")
+                CLI.colored_print("\t2) No")
+                delete_user = CLI.get_response([Config.TRUE, Config.FALSE], Config.TRUE)
+
+                if delete_user == Config.TRUE:
+                    content = '{username}\ttrue'.format(username=postgres_user)
+                    CLI.colored_print("╔══════════════════════════════════════════════════════╗",
+                                      CLI.COLOR_WARNING)
+                    CLI.colored_print("║ WARNING! User cannot be deleted if it has been used  ║",
+                                      CLI.COLOR_WARNING)
+                    CLI.colored_print("║ to initialize PostgreSQL server.                     ║",
+                                      CLI.COLOR_WARNING)
+                    CLI.colored_print("║ You will need to do it manually!                     ║",
+                                      CLI.COLOR_WARNING)
+                    CLI.colored_print("╚══════════════════════════════════════════════════════╝",
+                                      CLI.COLOR_WARNING)
+
+            self.__write_upsert_db_users_trigger_file(content, 'postgres')
+
+        if self.backend_questions:
             # Postgres settings
             CLI.colored_print("Do you want to tweak PostgreSQL settings?", CLI.COLOR_SUCCESS)
             CLI.colored_print("\t1) Yes")
@@ -966,31 +1194,67 @@ class Config:
         """
         Customize services ports
         """
-        CLI.colored_print("Do you want to customize service ports?", CLI.COLOR_SUCCESS)
-        CLI.colored_print("\t1) Yes")
-        CLI.colored_print("\t2) No")
-        self.__config["customized_ports"] = CLI.get_response([Config.TRUE, Config.FALSE],
-                                                             self.__config.get("customized_ports",
-                                                                               Config.FALSE))
-        if self.__config.get("customized_ports") == Config.TRUE:
-            CLI.colored_print("PostgreSQL?", CLI.COLOR_SUCCESS)
-            self.__config["postgresql_port"] = CLI.get_response(r"~^\d+$", self.__config.get("postgresql_port", "5432"))
-
-            CLI.colored_print("MongoDB?", CLI.COLOR_SUCCESS)
-            self.__config["mongo_port"] = CLI.get_response(r"~^\d+$", self.__config.get("mongo_port", "27017"))
-
-            CLI.colored_print("Redis (main)?", CLI.COLOR_SUCCESS)
-            self.__config["redis_main_port"] = CLI.get_response(r"~^\d+$", self.__config.get("redis_main_port", "6379"))
-
-            CLI.colored_print("Redis (cache)?", CLI.COLOR_SUCCESS)
-            self.__config["redis_cache_port"] = CLI.get_response(r"~^\d+$",
-                                                                 self.__config.get("redis_cache_port", "6380"))
-        else:
+        def reset_ports():
             self.__config["postgresql_port"] = "5432"
             self.__config["mongo_port"] = "27017"
             self.__config["redis_main_port"] = "6379"
             self.__config["redis_cache_port"] = "6380"
 
+        if not self.multi_servers:
+            CLI.colored_print("Do you want to expose backend container ports "
+                              "(`PostgreSQL`, `MongoDB`, `redis`) ?",
+                              CLI.COLOR_SUCCESS)
+            CLI.colored_print("\t1) Yes")
+            CLI.colored_print("\t2) No")
+            self.__config["expose_backend_ports"] = CLI.get_response(
+                [Config.TRUE, Config.FALSE], self.__config.get("expose_backend_ports",
+                                                               Config.FALSE))
+        else:
+            self.__config["expose_backend_ports"] = Config.TRUE
+
+        if not self.expose_backend_ports:
+            reset_ports()
+            return
+
+        CLI.colored_print("╔═════════════════════════════════════════════════╗",
+                          CLI.COLOR_WARNING)
+        CLI.colored_print("║ WARNING! When exposing backend container ports, ║",
+                          CLI.COLOR_WARNING)
+        CLI.colored_print("║ it's STRONGLY recommended to use a firewall to  ║",
+                          CLI.COLOR_WARNING)
+        CLI.colored_print("║ grant access to frontend containers only.       ║",
+                          CLI.COLOR_WARNING)
+        CLI.colored_print("╚═════════════════════════════════════════════════╝",
+                          CLI.COLOR_WARNING)
+
+        CLI.colored_print("Do you want to customize service ports?",
+                          CLI.COLOR_SUCCESS)
+        CLI.colored_print("\t1) Yes")
+        CLI.colored_print("\t2) No")
+        self.__config["customized_ports"] = CLI.get_response(
+            [Config.TRUE, Config.FALSE], self.__config.get("customized_ports",
+                                                           Config.FALSE))
+
+        if self.__config.get("customized_ports") == Config.FALSE:
+            reset_ports()
+            return
+
+        CLI.colored_print("PostgreSQL?", CLI.COLOR_SUCCESS)
+        self.__config["postgresql_port"] = CLI.get_response(
+            r"~^\d+$", self.__config.get("postgresql_port", "5432"))
+
+        CLI.colored_print("MongoDB?", CLI.COLOR_SUCCESS)
+        self.__config["mongo_port"] = CLI.get_response(
+            r"~^\d+$", self.__config.get("mongo_port", "27017"))
+
+        CLI.colored_print("Redis (main)?", CLI.COLOR_SUCCESS)
+        self.__config["redis_main_port"] = CLI.get_response(
+            r"~^\d+$", self.__config.get("redis_main_port", "6379"))
+
+        CLI.colored_print("Redis (cache)?", CLI.COLOR_SUCCESS)
+        self.__config["redis_cache_port"] = CLI.get_response(
+            r"~^\d+$", self.__config.get("redis_cache_port", "6380"))
+
     def __questions_private_routes(self):
         """
         Asks if configuration uses a DNS for private domain names for communication
@@ -1037,7 +1301,7 @@ class Config:
             ".".join(parts[:-1])
         )
         if not self.multi_servers or \
-                (self.multi_servers and self.__config.get("use_private_dns") == Config.FALSE):
+                (self.multi_servers and not self.use_private_dns):
             self.__config["private_domain_name"] = "{}.private".format(
                 ".".join(parts[:-1])
             )
@@ -1061,7 +1325,15 @@ class Config:
             self.__config["kobocat_raven"] = ""
             self.__config["kpi_raven_js"] = ""
 
-    def __question_reverse_proxy(self):
+    def __questions_redis(self):
+        CLI.colored_print("Redis password?", CLI.COLOR_SUCCESS)
+        self.__config["redis_password"] = CLI.get_response(
+            r"~^.{8,}$",
+            self.__config.get("redis_password"),
+            to_lower=False,
+            error_msg='Too short. 8 characters minimum.')
+
+    def __questions_reverse_proxy(self):
 
         if self.is_secure:
 
@@ -1110,7 +1382,7 @@ class Config:
 
         if self.proxy:
             # When proxy is enabled, public port is 80 or 443.
-            # @TODO Give the user the possibilty to customize it too.
+            # @TODO Give the user the possibility to customize it too.
             self.__config["exposed_nginx_docker_port"] = Config.DEFAULT_NGINX_PORT
             if self.advanced_options:
                 if not self.use_letsencrypt:
@@ -1191,66 +1463,114 @@ class Config:
         if username == self.__config.get("super_user_username") and \
             password != self.__config.get("super_user_password") and \
                 not self.first_time:
-            CLI.colored_print("╔══════════════════════════════════════════════════════╗", CLI.COLOR_WARNING)
-            CLI.colored_print("║ Super user's password has been changed!              ║", CLI.COLOR_WARNING)
-            CLI.colored_print("║ Don't forget to apply these changes in Postgres too. ║", CLI.COLOR_WARNING)
-            CLI.colored_print("║ Super user's password won't be updated if the        ║", CLI.COLOR_WARNING)
-            CLI.colored_print("║ database already exists.                             ║", CLI.COLOR_WARNING)
-            CLI.colored_print("╚══════════════════════════════════════════════════════╝", CLI.COLOR_WARNING)
+
+            _message_lines = [
+                "╔════════════════════════════════════════════════════════════════╗",
+                "║ You have configured a new password for the super user.         ║",
+                "║ This change will *not* take effect if KoBoToolbox has ever     ║",
+                "║ been started before. Please use the web interface to change    ║",
+                "║ passwords for existing users.                                  ║",
+                "║ If you've forgotten your password:                             ║",
+                "║ 1. Enter the KPI container with `./run.py -cf exec kpi bash`;  ║",
+                "║ 2. Create a new super user with `./manage.py createsuperuser`; ║",
+                "║ 3. Type `exit` to leave the KPI container;                     ║",
+                "╚════════════════════════════════════════════════════════════════╝"
+            ]
+            CLI.colored_print('\n'.join(_message_lines), CLI.COLOR_WARNING)
 
         self.__config["super_user_username"] = username
         self.__config["super_user_password"] = password
 
     def __questions_uwsgi(self):
-        CLI.colored_print("Do you want to tweak uWSGI settings?", CLI.COLOR_SUCCESS)
-        CLI.colored_print("\t1) Yes")
-        CLI.colored_print("\t2) No")
-        self.__config["uwsgi_settings"] = CLI.get_response([Config.TRUE, Config.FALSE],
-                                                           self.__config.get("uwsgi_settings", Config.FALSE))
-
-        if self.__config.get("uwsgi_settings") == Config.TRUE:
-            CLI.colored_print("Number of uWSGi workers to start?", CLI.COLOR_SUCCESS)
-            self.__config["workers_start"] = CLI.get_response(
-                r"~^\d+$",
-                self.__config.get("workers_start", "1"))
-            CLI.colored_print("Max uWSGi workers?", CLI.COLOR_SUCCESS)
-            self.__config["workers_max"] = CLI.get_response(
-                r"~^\d+$",
-                self.__config.get("workers_max", "2"))
-
-            CLI.colored_print("Max number of requests per worker?", CLI.COLOR_SUCCESS)
-            self.__config["max_requests"] = CLI.get_response(
-                r"~^\d+$",
-                self.__config.get("max_requests", "512"))
-            CLI.colored_print("Max memory per workers in MB?", CLI.COLOR_SUCCESS)
-            self.__config["soft_limit"] = CLI.get_response(
-                r"~^\d+$",
-                self.__config.get("soft_limit", "128"))
-        else:
-            self.__config["workers_start"] = "1"
-            self.__config["workers_max"] = "2"
-            self.__config["max_requests"] = "512"
-            self.__config["soft_limit"] = "128"
+
+        if not self.dev_mode:
+            CLI.colored_print("Do you want to tweak uWSGI settings?", CLI.COLOR_SUCCESS)
+            CLI.colored_print("\t1) Yes")
+            CLI.colored_print("\t2) No")
+            self.__config["uwsgi_settings"] = CLI.get_response([Config.TRUE, Config.FALSE],
+                                                               self.__config.get("uwsgi_settings", Config.FALSE))
+
+            if self.__config.get("uwsgi_settings") == Config.TRUE:
+                CLI.colored_print("Number of uWSGi workers to start?", CLI.COLOR_SUCCESS)
+                self.__config["workers_start"] = CLI.get_response(
+                    r"~^\d+$",
+                    self.__config.get("workers_start", "1"))
+                CLI.colored_print("Max uWSGi workers?", CLI.COLOR_SUCCESS)
+                self.__config["workers_max"] = CLI.get_response(
+                    r"~^\d+$",
+                    self.__config.get("workers_max", "2"))
+
+                CLI.colored_print("Max number of requests per worker?", CLI.COLOR_SUCCESS)
+                self.__config["max_requests"] = CLI.get_response(
+                    r"~^\d+$",
+                    self.__config.get("max_requests", "512"))
+                CLI.colored_print("Max memory per workers in MB?", CLI.COLOR_SUCCESS)
+                self.__config["soft_limit"] = CLI.get_response(
+                    r"~^\d+$",
+                    self.__config.get("soft_limit", "128"))
+
+                return
+
+        self.__config["workers_start"] = "1"
+        self.__config["workers_max"] = "2"
+        self.__config["max_requests"] = "512"
+        self.__config["soft_limit"] = "128"
 
     def __is_port_allowed(self, port):
         return not (self.block_common_http_ports and port in [Config.DEFAULT_NGINX_PORT,
                                                               Config.DEFAULT_NGINX_HTTPS_PORT])
 
-    def __reset_dev_mode(self, reset_nginx_port=False):
+    def __reset(self, **kwargs):
         """
-        Resets several properties to their default to avoid developer mode.
+        Resets several properties to their default.
         It can be useful, if user changes the type of installation on the same server
-
-        :param reset_nginx_port: bool
         :return: bool
         """
-        self.__config["dev_mode"] = Config.FALSE
-        self.__config["staging_mode"] = Config.FALSE
-        self.__config["kc_path"] = ""
-        self.__config["kpi_path"] = ""
-        self.__config["debug"] = Config.FALSE
-        if reset_nginx_port:
-            self.__config["exposed_nginx_docker_port"] = Config.DEFAULT_NGINX_PORT
+        all = True if not kwargs else False
+        dev_mode = kwargs.get("dev", False)
+        local_install = kwargs.get("local_install", False)
+        private_dns = kwargs.get("private_dns", False)
+        reset_nginx_port = kwargs.get("reset_nginx_port", False)
+
+        if dev_mode or all:
+            self.__config["dev_mode"] = Config.FALSE
+            self.__config["staging_mode"] = Config.FALSE
+            self.__config["kc_path"] = ""
+            self.__config["kpi_path"] = ""
+            self.__config["debug"] = Config.FALSE
+            if reset_nginx_port:
+                self.__config["exposed_nginx_docker_port"] = Config.DEFAULT_NGINX_PORT
+
+        if private_dns or all:
+            self.__config["use_private_dns"] = Config.FALSE
+
+        if local_install or all:
+            self.__config["multi"] = Config.FALSE
+            self.__config["https"] = Config.FALSE
+            self.__config["proxy"] = Config.FALSE
+            self.__config["nginx_proxy_port"] = Config.DEFAULT_NGINX_PORT
+            self.__config["use_letsencrypt"] = Config.FALSE
+
+    def __secure_mongo(self):
+        """
+        Force creations of MongoDB users/passwords when users upgrade from
+        a non secure version of KoBoInstall
+        """
+        # ToDo remove duplicated code with `__questions_mongo`
+
+        if self.__config.get("mongo_secured") != Config.TRUE and not self.first_time:
+            self.__write_upsert_db_users_trigger_file('', 'mongo')
+
+        self.__config["mongo_secured"] = Config.TRUE
+
+    def __upgrade_kc_db(self, config):
+        # TODO Validate whether this method is still needed if we want to keep
+        # retro-compatibility
+        kc_postgres_db = config.pop('postgres_db', None)
+        if kc_postgres_db is not None:
+            config['kc_postgres_db'] = kc_postgres_db
+
+        return config
 
     def __validate_installation(self):
         """
@@ -1295,8 +1615,9 @@ class Config:
                     if response == "no":
                         sys.exit()
                     else:
-                        CLI.colored_print("Administrator privilege escalation is needed to prepare DB", CLI.COLOR_WARNING)
-                        # Write kobo_first_run file to run postgres container's entrypoint flawlessly.
+                        CLI.colored_print("Administrator privilege escalation is needed to prepare DB",
+                                          CLI.COLOR_WARNING)
+                        # Write `kobo_first_run` file to run postgres container's entrypoint flawlessly.
                         os.system("echo $(date) | sudo tee -a {} > /dev/null".format(
                             os.path.join(self.__config["kobodocker_path"], ".vols", "db", "kobo_first_run")
                         ))
@@ -1313,3 +1634,17 @@ class Config:
         CLI.colored_print("║ to remove previously entered value.                           ║", CLI.COLOR_WARNING)
         CLI.colored_print("║ Otherwise choose between choices or type your answer.         ║", CLI.COLOR_WARNING)
         CLI.colored_print("╚═══════════════════════════════════════════════════════════════╝", CLI.COLOR_WARNING)
+
+    def __write_upsert_db_users_trigger_file(self, content, destination):
+        try:
+            trigger_file = os.path.join(self.__config.get("kobodocker_path"),
+                                        destination,
+                                        Config.UPSERT_DB_USERS_TRIGGER_FILE)
+            with open(trigger_file, "w") as f:
+                f.write(content)
+        except (IOError, OSError):
+            CLI.colored_print("Could not write {} file".format(
+                Config.UPSERT_DB_USERS_TRIGGER_FILE), CLI.COLOR_ERROR)
+            return False
+
+        return True
diff --git a/helpers/template.py b/helpers/template.py
index dfe05b8..9cc0b35 100644
--- a/helpers/template.py
+++ b/helpers/template.py
@@ -3,10 +3,16 @@ from __future__ import print_function, unicode_literals
 
 import binascii
 import fnmatch
+import json
 import os
-from string import Template as PyTemplate
 import stat
 import sys
+try:
+    from urllib.parse import quote_plus
+except ImportError:
+    from urllib import quote_plus
+
+from string import Template as PyTemplate
 
 from helpers.cli import CLI
 from helpers.config import Config
@@ -51,22 +57,33 @@ class Template:
             destination_directory = cls.__create_directory(environment_directory,
                                                            root,
                                                            templates_path)
-            cls.__write_templates(template_variables, root, destination_directory, filenames)
+            cls.__write_templates(template_variables,
+                                  root,
+                                  destination_directory,
+                                  filenames)
 
         # kobo-docker
         templates_path = os.path.join(templates_path_parent, "kobo-docker")
         for root, dirnames, filenames in os.walk(templates_path):
             destination_directory = config.get("kobodocker_path")
-            cls.__write_templates(template_variables, root, destination_directory, filenames)
+            cls.__write_templates(template_variables,
+                                  root,
+                                  destination_directory,
+                                  filenames)
 
         # nginx-certbox
         if config_object.use_letsencrypt:
-            templates_path = os.path.join(templates_path_parent, Config.LETSENCRYPT_DOCKER_DIR, "")
+            templates_path = os.path.join(templates_path_parent,
+                                          Config.LETSENCRYPT_DOCKER_DIR, "")
             for root, dirnames, filenames in os.walk(templates_path):
-                destination_directory = cls.__create_directory(config_object.get_letsencrypt_repo_path(),
-                                                               root,
-                                                               templates_path)
-                cls.__write_templates(template_variables, root, destination_directory, filenames)
+                destination_directory = cls.__create_directory(
+                    config_object.get_letsencrypt_repo_path(),
+                    root,
+                    templates_path)
+                cls.__write_templates(template_variables,
+                                      root,
+                                      destination_directory,
+                                      filenames)
 
     @classmethod
     def render_maintenance(cls, config_object):
@@ -82,7 +99,10 @@ class Template:
         for root, dirnames, filenames in os.walk(templates_path):
             filenames = [filename for filename in filenames if 'maintenance' in filename]
             destination_directory = config.get("kobodocker_path")
-            cls.__write_templates(template_variables, root, destination_directory, filenames)
+            cls.__write_templates(template_variables,
+                                  root,
+                                  destination_directory,
+                                  filenames)
 
     @classmethod
     def __create_directory(cls, template_root_directory, path="", base_dir=""):
@@ -96,8 +116,10 @@ class Template:
             try:
                 os.makedirs(destination_directory)
             except OSError:
-                CLI.colored_print("Can not create {}. Please verify permissions!".format(destination_directory),
-                                  CLI.COLOR_ERROR)
+                CLI.colored_print(
+                    "Can not create {}. "
+                    "Please verify permissions!".format(destination_directory),
+                    CLI.COLOR_ERROR)
                 sys.exit(1)
 
         return destination_directory
@@ -107,15 +129,21 @@ class Template:
 
         config = config_object.get_config()
 
+        def _get_value(property_, true_value="", false_value="#",
+                       comparison_value=Config.TRUE):
+            return true_value \
+                if config.get(property_) == comparison_value \
+                else false_value
+
         if config_object.proxy:
             nginx_port = config.get("nginx_proxy_port")
         else:
             nginx_port = config.get("exposed_nginx_docker_port")
 
         return {
-            "PROTOCOL": "https" if config.get("https") == Config.TRUE else "http",
-            "USE_HTTPS": "" if config.get("https") == Config.TRUE else "#",
-            "USE_AWS": "" if config.get("use_aws") == Config.TRUE else "#",
+            "PROTOCOL": _get_value("https", "https", "http"),
+            "USE_HTTPS": _get_value("https"),
+            "USE_AWS": _get_value("use_aws"),
             "AWS_ACCESS_KEY_ID": config.get("aws_access_key", ""),
             "AWS_SECRET_ACCESS_KEY": config.get("aws_secret_key", ""),
             "AWS_BUCKET_NAME": config.get("aws_bucket_name", ""),
@@ -129,8 +157,10 @@ class Template:
             "ENKETO_SUBDOMAIN": config.get("ee_subdomain", ""),
             "KOBO_SUPERUSER_USERNAME": config.get("super_user_username", ""),
             "KOBO_SUPERUSER_PASSWORD": config.get("super_user_password", ""),
-            "ENKETO_API_TOKEN": config.get("enketo_api_token", binascii.hexlify(os.urandom(60))),
-            "DJANGO_SECRET_KEY": config.get("django_secret_key", binascii.hexlify(os.urandom(24))),
+            "ENKETO_API_TOKEN": config.get("enketo_api_token",
+                                           binascii.hexlify(os.urandom(60))),
+            "DJANGO_SECRET_KEY": config.get("django_secret_key",
+                                            binascii.hexlify(os.urandom(24))),
             "KOBOCAT_RAVEN_DSN": config.get("kobocat_raven", ""),
             "KPI_RAVEN_DSN": config.get("kpi_raven", ""),
             "KPI_RAVEN_JS_DSN": config.get("kpi_raven_js", ""),
@@ -138,6 +168,7 @@ class Template:
             "KPI_POSTGRES_DB": config.get("kpi_postgres_db", ""),
             "POSTGRES_USER": config.get("postgres_user", ""),
             "POSTGRES_PASSWORD": config.get("postgres_password", ""),
+            "POSTGRES_PASSWORD_URL_ENCODED": quote_plus(config.get("postgres_password", "")),
             "DEBUG": config.get("debug", False) == Config.TRUE,
             "SMTP_HOST": config.get("smtp_host", ""),
             "SMTP_PORT": config.get("smtp_port", ""),
@@ -147,30 +178,34 @@ class Template:
             "DEFAULT_FROM_EMAIL": config.get("default_from_email", ""),
             "MASTER_BACKEND_IP": config.get("master_backend_ip"),
             "LOCAL_INTERFACE_IP": config.get("local_interface_ip"),
-            "USE_PUBLIC_DNS": "" if config.get("local_installation") == Config.TRUE else "#",
-            "USE_PRIVATE_DNS": "#" if config.get("use_private_dns") == Config.TRUE else "",
-            "USE_DNS": "" if config.get("local_installation") == Config.TRUE or
-                             config.get("use_private_dns") == Config.FALSE else "#",
             "WORKERS_MAX": config.get("workers_max", ""),
             "WORKERS_START": config.get("workers_start", ""),
             "KC_PATH": config.get("kc_path", ""),
             "KPI_PATH": config.get("kpi_path", ""),
-            "USE_KPI_DEV_MODE": "#" if config.get("kpi_path", "") == "" else "",
-            "USE_KC_DEV_MODE": "#" if config.get("kc_path", "") == "" else "",
+            "USE_KPI_DEV_MODE": _get_value("kpi_path",
+                                           true_value="#",
+                                           false_value="",
+                                           comparison_value=""),
+            "USE_KC_DEV_MODE": _get_value("kc_path",
+                                          true_value="#",
+                                          false_value="",
+                                          comparison_value=""),
             "KC_DEV_BUILD_ID": config.get("kc_dev_build_id", ""),
             "KPI_DEV_BUILD_ID": config.get("kpi_dev_build_id", ""),
-            "NGINX_PUBLIC_PORT": config.get("exposed_nginx_docker_port", Config.DEFAULT_NGINX_PORT),
+            "NGINX_PUBLIC_PORT": config.get("exposed_nginx_docker_port",
+                                            Config.DEFAULT_NGINX_PORT),
             "NGINX_EXPOSED_PORT": nginx_port,
             "MAX_REQUESTS": config.get("max_requests", "512"),
             "SOFT_LIMIT": int(config.get("soft_limit", "128")) * 1024 * 1024,
             "POSTGRES_REPLICATION_PASSWORD": config.get("postgres_replication_password"),
             "WSGI_SERVER": "runserver_plus" if config_object.dev_mode else "uWSGI",
             "USE_X_FORWARDED_HOST": "" if config_object.dev_mode else "#",
-            "OVERRIDE_POSTGRES_SETTINGS": "" if config.get("postgres_settings") == Config.TRUE else "#",
+            "OVERRIDE_POSTGRES_SETTINGS": _get_value("postgres_settings"),
             "POSTGRES_APP_PROFILE": config.get("postgres_profile", ""),
             "POSTGRES_RAM": config.get("postgres_ram", ""),
             "POSTGRES_SETTINGS": config.get("postgres_settings_content", ""),
-            "POSTGRES_BACKUP_FROM_SLAVE": "" if config.get("backup_from_master") == Config.FALSE else "#",
+            "POSTGRES_BACKUP_FROM_SLAVE": _get_value("backup_from_master",
+                                                     comparison_value=Config.FALSE),
             "POSTGRES_PORT": config.get("postgresql_port", "5432"),
             "MONGO_PORT": config.get("mongo_port", "27017"),
             "REDIS_MAIN_PORT": config.get("redis_main_port", "6739"),
@@ -179,7 +214,8 @@ class Template:
             "USE_AWS_BACKUP": "" if config_object.aws and
                                     config.get("use_backup") == Config.TRUE and
                                     config.get("aws_backup_bucket_name") != "" else "#",
-            "USE_MEDIA_BACKUP": "" if not config_object.aws and config.get("use_backup") == Config.TRUE else "#",
+            "USE_MEDIA_BACKUP": "" if (not config_object.aws and
+                                       config.get("use_backup") == Config.TRUE) else "#",
             "KOBOCAT_MEDIA_BACKUP_SCHEDULE": config.get("kobocat_media_backup_schedule"),
             "MONGO_BACKUP_SCHEDULE": config.get("mongo_backup_schedule"),
             "POSTGRES_BACKUP_SCHEDULE": config.get("postgres_backup_schedule"),
@@ -193,15 +229,33 @@ class Template:
             "AWS_POSTGRES_BACKUP_MINIMUM_SIZE": config.get("aws_postgres_backup_minimum_size"),
             "AWS_REDIS_BACKUP_MINIMUM_SIZE": config.get("aws_redis_backup_minimum_size"),
             "AWS_BACKUP_UPLOAD_CHUNK_SIZE": config.get("aws_backup_upload_chunk_size"),
-            "AWS_BACKUP_BUCKET_DELETION_RULE_ENABLED": "False" if config.get(
-                "aws_backup_bucket_deletion_rule_enabled") == Config.FALSE else "True",
+            "AWS_BACKUP_BUCKET_DELETION_RULE_ENABLED":  _get_value(
+                "aws_backup_bucket_deletion_rule_enabled", "True", "False"),
             "LETSENCRYPT_EMAIL": config.get("letsencrypt_email"),
             "ENKETO_ENCRYPTION_KEY": config.get("enketo_encryption_key"),
             "MAINTENANCE_ETA": config.get("maintenance_eta", ""),
             "MAINTENANCE_DATE_ISO": config.get("maintenance_date_iso", ""),
             "MAINTENANCE_DATE_STR": config.get("maintenance_date_str", ""),
             "MAINTENANCE_EMAIL": config.get("maintenance_email", ""),
-            "USE_NPM_FROM_HOST": "" if config_object.dev_mode and config.get("npm_container") == Config.FALSE else "#",
+            "USE_NPM_FROM_HOST": "" if (config_object.dev_mode and
+                                        config.get("npm_container") == Config.FALSE) else "#",
+            "DOCKER_PREFIX": config_object.get_prefix("backend"),
+            "USE_BACKEND_NETWORK": _get_value("expose_backend_ports",
+                                              comparison_value=Config.FALSE),
+            "EXPOSE_BACKEND_PORTS": _get_value("expose_backend_ports"),
+            "USE_FAKE_DNS": _get_value("local_installation"),
+            "ADD_BACKEND_EXTRA_HOSTS": "" if (config_object.expose_backend_ports and
+                                              not config_object.use_private_dns) else "#",
+            "USE_EXTRA_HOSTS": "" if (config_object.local_install or
+                                      config_object.expose_backend_ports and
+                                      not config_object.use_private_dns) else "#",
+            "MONGO_ROOT_USERNAME": config.get("mongo_root_username"),
+            "MONGO_ROOT_PASSWORD": config.get("mongo_root_password"),
+            "MONGO_USER_USERNAME": config.get("mongo_user_username"),
+            "MONGO_USER_PASSWORD": config.get("mongo_user_password"),
+            "REDIS_PASSWORD": config.get("redis_password"),
+            "REDIS_PASSWORD_URL_ENCODED": quote_plus(config.get("redis_password")),
+            "REDIS_PASSWORD_JS_ENCODED": json.dumps(config.get("redis_password")),
         }
 
     @staticmethod
diff --git a/helpers/upgrading.py b/helpers/upgrading.py
index 98af363..4ceb554 100644
--- a/helpers/upgrading.py
+++ b/helpers/upgrading.py
@@ -14,8 +14,8 @@ def migrate_single_to_two_databases():
     while KoBoCAT's has user data, then we are migrating from a
     single-database setup
     """
-
-    config = Config().get_config()
+    config_object = Config()
+    config = config_object.get_config()
     backend_role = config.get("backend_server_role", "master")
 
     def _kpi_db_alias_kludge(command):
@@ -31,18 +31,23 @@ def migrate_single_to_two_databases():
 
     kpi_run_command = ["docker-compose",
                        "-f", "docker-compose.frontend.yml",
-                       "-f", "docker-compose.frontend.override.yml"]
-    if config.get("docker_prefix", "") != "":
-        kpi_run_command += ["-p", config.get("docker_prefix")]
-    kpi_run_command += ["run", "--rm", "kpi"]
+                       "-f", "docker-compose.frontend.override.yml",
+                       "-p", config_object.get_prefix("frontend"),
+                       "run", "--rm", "kpi"]
 
     # Make sure Postgres is running
+    # We add this message to users because when AWS backups are activated,
+    # it takes a long time to install the virtualenv in PostgreSQL container,
+    # so the `wait_for_database` below sits there a while.
+    # It makes us think KoBoInstall is frozen.
+    CLI.colored_print("Waiting for PostgreSQL database to be up & running...",
+                      CLI.COLOR_INFO)
     frontend_command = kpi_run_command + _kpi_db_alias_kludge(" ".join([
                            "python", "manage.py",
-                           "wait_for_database", "--retries", "30"
+                           "wait_for_database", "--retries", "45"
                        ]))
     CLI.run_command(frontend_command, config.get("kobodocker_path"))
-    CLI.colored_print("The Postgres database is running!", CLI.COLOR_SUCCESS)
+    CLI.colored_print("The PostgreSQL database is running!", CLI.COLOR_SUCCESS)
 
     frontend_command = kpi_run_command + _kpi_db_alias_kludge(" ".join([
                             "python", "manage.py",
@@ -95,11 +100,8 @@ def migrate_single_to_two_databases():
             "-f",
             "docker-compose.backend.{}.yml".format(backend_role),
             "-f",
-            "docker-compose.backend.{}.override.yml".format(backend_role)
-        ]
-        if config.get("docker_prefix", "") != "":
-            backend_command += ["-p", config.get("docker_prefix")]
-        backend_command += [
+            "docker-compose.backend.{}.override.yml".format(backend_role),
+            "-p", config_object.get_prefix("backend"),
             "exec", "postgres", "bash",
             "/kobo-docker-scripts/master/clone_data_from_kc_to_kpi.sh",
             "--noinput"
diff --git a/readme.md b/readme.md
index f2ffd3a..799ab2d 100644
--- a/readme.md
+++ b/readme.md
@@ -44,16 +44,16 @@ Get version:
 Build kpi and kobocat (dev mode):  
 `$kobo-install> python3 run.py --build`
 
-Run docker commands on frontend containers:
+Run docker commands on frontend containers:  
 `$kobo-install> python run.py --compose-frontend [docker-compose arguments]`
 
-Run docker commands on backend containers:
+Run docker commands on backend containers:  
 `$kobo-install> python run.py --compose-backend [docker-compose arguments]`
 
-Start maintenance mode:
+Start maintenance mode:  
 `$kobo-install> python run.py --maintenance`
 
-Stop maintenance mode:
+Stop maintenance mode:  
 `$kobo-install> python run.py --stop-maintenance`
 
 ## Build the configuration
@@ -73,6 +73,7 @@ User can choose between 2 types of installations:
 |Use HTTPS<sup>1</sup>| **False** (Workstation)<br>**True** (Server)  |  | ✓ (frontend only) |
 |Super user's username| **super_admin** | ✓ | ✓ (frontend only) |
 |Super user's password| **Random string**  | ✓ | ✓ (frontend only) |
+|Activate backups<sup>2</sup>|  **False**  | ✓ | ✓ (backend only) |
 
 ### Advanced Options
 
@@ -85,12 +86,17 @@ User can choose between 2 types of installations:
 |Use DNS for private routes| **No**  |  | ✓ (frontend only) |
 |Master backend IP _(if previous answer is no)_| **Local IP**  |  | ✓ (frontend only) |
 |PostgreSQL DB|  **kobo**  | ✓ | ✓ |
-|PostgreSQL user|  **kobo**  | ✓ | ✓ |
-|PostgreSQL password|  **Autogenerate**  | ✓ | ✓ |
-|PostgreSQL number of connections<sup>2</sup>|  **100**  | ✓ | ✓ (backend only) |
-|PostgreSQL RAM<sup>2</sup>|  **2**  | ✓ | ✓ (backend only) |
-|PostgreSQL Application Profile<sup>2</sup>|  **Mixed**  | ✓ | ✓ (backend only) |
-|PostgreSQL Storage<sup>2</sup>|  **HDD**  | ✓ | ✓ (backend only) |
+|PostgreSQL user's username|  **kobo**  | ✓ | ✓ |
+|PostgreSQL user's password|  **Autogenerate**  | ✓ | ✓ |
+|PostgreSQL number of connections<sup>3</sup>|  **100**  | ✓ | ✓ (backend only) |
+|PostgreSQL RAM<sup>3</sup>|  **2**  | ✓ | ✓ (backend only) |
+|PostgreSQL Application Profile<sup>3</sup>|  **Mixed**  | ✓ | ✓ (backend only) |
+|PostgreSQL Storage<sup>3</sup>|  **HDD**  | ✓ | ✓ (backend only) |
+|MongoDB super user's username|  **root**  | ✓ | ✓ |
+|MongoDB super user's password|  **Autogenerate**  | ✓ | ✓ |
+|MongoDB user's username|  **kobo**  | ✓ | ✓ |
+|MongoDB user's password|  **Autogenerate**  | ✓ | ✓ |
+|Redis password|  **Autogenerate**  | ✓ | ✓ |
 |Use AWS storage|  **No**  | ✓ | ✓ (frontend only) |
 |uWGI workers|  **start: 2, max: 4**  | ✓ | ✓ (frontend only) |
 |uWGI memory limit|  **128 MB**  | ✓ | ✓ (frontend only) |
@@ -104,24 +110,37 @@ User can choose between 2 types of installations:
 <sup>1)</sup> _HTTPS certificates must be installed on a Reverse Proxy. 
 `KoBoInstall` can install one and use `Let's Encrypt` to generate certificates thanks to [nginx-certbot project](https://github.com/wmnnd/nginx-certbot "")_
 
-<sup>2)</sup> _Custom settings are provided by [PostgreSQL Configuration Tool API](https://github.com/sebastianwebber/pgconfig-api "")_
+<sup>2)</sup> _If AWS credentials are provided, backups are sent to configured bucket_
+
+<sup>3)</sup> _Custom settings are provided by [PostgreSQL Configuration Tool API](https://github.com/sebastianwebber/pgconfig-api "")_
 
 ℹ  Intercom App ID [must now](https://github.com/kobotoolbox/kpi/pull/2285) be configured through "Per user settings" in the Django admin interface of KPI.
 
 ## Requirements
 
-- Linux / macOS
-- Python 2.7/3.5+ <sup>Python2 support will be dropped in a future release</sup>
+- Linux <sup>4</sup> / macOS <sup>5</sup>
+- Python 2.7/3.5+ <sup>_Python2 support will be dropped in a future release_</sup>
 - [Docker](https://www.docker.com/get-started "") & [Docker Compose](https://docs.docker.com/compose/install/ "")
-- Available TCP Ports:
+- Available TCP Ports: <sup>6</sup>
+
+    1. 80 NGINX
+    1. 443 NGINX (if you use KoBoInstall with LetsEncrypt proxy) 
+    2. Additional ports when `expose ports` advanced option has been selected
+        1. 5432 PostgreSQL
+        3. 6379-6380 redis
+        4. 27017 MongoDB
 
-    1. 80 NginX
-    2. 5432 PostgreSQL
-    3. 5672 RabbitMQ
-    4. 6379-6380 redis
-    5. 27017 MongoDB
+    _**WARNING:**_
+    
+    - _If you use a firewall, be sure to open traffic publicly on NGINX port, otherwise KoBoInstall cannot work_
+    - _By default, additional ports are not exposed except when using multi servers configuration. If you choose to expose them, **be sure to not expose them publicly** (e.g. use a firewall and allow traffic between frontend and backend containers only. NGINX port still has to stay publicly opened though)._
     
-    _N.B.: Ports can be customized in advanced options._
+<sup>4)</sup> _It has been tested with Ubuntu 14.04, 16.04 and 18.04_
+
+<sup>5)</sup> _Docker on macOS is slow. First boot usually takes a while to be ready. You may have to answer `Yes` once or twice to question `Wait for another 600 seconds?` when prompted_
+
+<sup>6)</sup> _These are defaults but can be customized with advanced options_
+
 
 ## Tests
 
diff --git a/run.py b/run.py
index 4e27352..ebeaea2 100755
--- a/run.py
+++ b/run.py
@@ -45,43 +45,48 @@ def run(force_setup=False):
 
 
 if __name__ == "__main__":
-    if len(sys.argv) > 2:
-        if sys.argv[1] == "-cf" or sys.argv[1] == "--compose-frontend":
-            Command.compose_frontend(sys.argv[2:])
-        elif sys.argv[1] == "-cb" or sys.argv[1] == "--compose-backend":
-            Command.compose_backend(sys.argv[2:])
-        else:
-            CLI.colored_print("Bad syntax. Try 'run.py --help'", CLI.COLOR_ERROR)
-    elif len(sys.argv) == 2:
-        if sys.argv[1] == "-h" or sys.argv[1] == "--help":
-            Command.help()
-        elif sys.argv[1] == "-u" or sys.argv[1] == "--update":
-            Command.update()
-        elif sys.argv[1] == "--upgrade":
-            # "update" was called "upgrade" in a previous release; accept
-            # either "update" or "upgrade" here to ease the transition
-            Command.update()
-        elif sys.argv[1] == "-i" or sys.argv[1] == "--info":
-            Command.info(0)
-        elif sys.argv[1] == "-s" or sys.argv[1] == "--setup":
-            run(force_setup=True)
-        elif sys.argv[1] == "-S" or sys.argv[1] == "--stop":
-            Command.stop()
-        elif sys.argv[1] == "-l" or sys.argv[1] == "--logs":
-            Command.logs()
-        elif sys.argv[1] == "-b" or sys.argv[1] == "--build":
-            Command.build()
-        elif sys.argv[1] == "-bkf" or sys.argv[1] == "--build-kpi":
-            Command.build("kf")
-        elif sys.argv[1] == "-bkc" or sys.argv[1] == "--build-kobocat":
-            Command.build("kc")
-        elif sys.argv[1] == "-v" or sys.argv[1] == "--version":
-            Command.version()
-        elif sys.argv[1] == "-m" or sys.argv[1] == "--maintenance":
-            Command.configure_maintenance()
-        elif sys.argv[1] == "-sm" or sys.argv[1] == "--stop-maintenance":
-            Command.stop_maintenance()
+    try:
+
+        if len(sys.argv) > 2:
+            if sys.argv[1] == "-cf" or sys.argv[1] == "--compose-frontend":
+                Command.compose_frontend(sys.argv[2:])
+            elif sys.argv[1] == "-cb" or sys.argv[1] == "--compose-backend":
+                Command.compose_backend(sys.argv[2:])
+            else:
+                CLI.colored_print("Bad syntax. Try 'run.py --help'", CLI.COLOR_ERROR)
+        elif len(sys.argv) == 2:
+            if sys.argv[1] == "-h" or sys.argv[1] == "--help":
+                Command.help()
+            elif sys.argv[1] == "-u" or sys.argv[1] == "--update":
+                Command.update()
+            elif sys.argv[1] == "--upgrade":
+                # "update" was called "upgrade" in a previous release; accept
+                # either "update" or "upgrade" here to ease the transition
+                Command.update()
+            elif sys.argv[1] == "-i" or sys.argv[1] == "--info":
+                Command.info(0)
+            elif sys.argv[1] == "-s" or sys.argv[1] == "--setup":
+                run(force_setup=True)
+            elif sys.argv[1] == "-S" or sys.argv[1] == "--stop":
+                Command.stop()
+            elif sys.argv[1] == "-l" or sys.argv[1] == "--logs":
+                Command.logs()
+            elif sys.argv[1] == "-b" or sys.argv[1] == "--build":
+                Command.build()
+            elif sys.argv[1] == "-bkf" or sys.argv[1] == "--build-kpi":
+                Command.build("kf")
+            elif sys.argv[1] == "-bkc" or sys.argv[1] == "--build-kobocat":
+                Command.build("kc")
+            elif sys.argv[1] == "-v" or sys.argv[1] == "--version":
+                Command.version()
+            elif sys.argv[1] == "-m" or sys.argv[1] == "--maintenance":
+                Command.configure_maintenance()
+            elif sys.argv[1] == "-sm" or sys.argv[1] == "--stop-maintenance":
+                Command.stop_maintenance()
+            else:
+                CLI.colored_print("Bad syntax. Try 'run.py --help'", CLI.COLOR_ERROR)
         else:
-            CLI.colored_print("Bad syntax. Try 'run.py --help'", CLI.COLOR_ERROR)
-    else:
-        run()
+            run()
+
+    except KeyboardInterrupt:
+        CLI.colored_print("\nUser interrupted execution", CLI.COLOR_INFO)
diff --git a/templates/kobo-deployments/enketo_express/config.json.tpl b/templates/kobo-deployments/enketo_express/config.json.tpl
index 7a04d1e..6ad3d15 100644
--- a/templates/kobo-deployments/enketo_express/config.json.tpl
+++ b/templates/kobo-deployments/enketo_express/config.json.tpl
@@ -41,11 +41,13 @@
     "redis": {
         "cache": {
             "host": "redis-cache.${PRIVATE_DOMAIN_NAME}",
-            "port": "${REDIS_CACHE_PORT}"
+            "port": "${REDIS_CACHE_PORT}",
+            "password": ${REDIS_PASSWORD_JS_ENCODED}
         },
         "main": {
             "host": "redis-main.${PRIVATE_DOMAIN_NAME}",
-            "port": "${REDIS_MAIN_PORT}"
+            "port": "${REDIS_MAIN_PORT}",
+            "password": ${REDIS_PASSWORD_JS_ENCODED}
         }
     },
     "google": {
diff --git a/templates/kobo-deployments/envfiles/databases.txt.tpl b/templates/kobo-deployments/envfiles/databases.txt.tpl
index 584841e..1880fc2 100644
--- a/templates/kobo-deployments/envfiles/databases.txt.tpl
+++ b/templates/kobo-deployments/envfiles/databases.txt.tpl
@@ -1,11 +1,16 @@
 #--------------------------------------------------------------------------------
 # MONGO
 #--------------------------------------------------------------------------------
-# These `KOBO_POSTGRES_` settings only affect the postgres container itself and the
+# These `KOBO_MONGO_` settings only affect the mongo container itself and the
 # `wait_for_mongo.bash` init script that runs within the kpi and kobocat.
 # Please see kobocat.txt to set container variables
 KOBO_MONGO_PORT=${MONGO_PORT}
 KOBO_MONGO_HOST=mongo.${PRIVATE_DOMAIN_NAME}
+MONGO_INITDB_ROOT_USERNAME=${MONGO_ROOT_USERNAME}
+MONGO_INITDB_ROOT_PASSWORD=${MONGO_ROOT_PASSWORD}
+MONGO_INITDB_DATABASE=formhub
+KOBO_MONGO_USERNAME=${MONGO_USER_USERNAME}
+KOBO_MONGO_PASSWORD=${MONGO_USER_PASSWORD}
 
 # Default MongoDB backup schedule is weekly at 01:00 AM UTC on Sunday.
 ${USE_BACKUP}MONGO_BACKUP_SCHEDULE=${MONGO_BACKUP_SCHEDULE}
@@ -26,9 +31,8 @@ KC_POSTGRES_DB=${KC_POSTGRES_DB}
 KPI_POSTGRES_DB=${KPI_POSTGRES_DB}
 
 # Postgres database used by kpi and kobocat Django apps
-KC_DATABASE_URL=postgis://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres.${PRIVATE_DOMAIN_NAME}:${POSTGRES_PORT}/${KC_POSTGRES_DB}
-KPI_DATABASE_URL=postgis://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres.${PRIVATE_DOMAIN_NAME}:${POSTGRES_PORT}/${KPI_POSTGRES_DB}
-
+KC_DATABASE_URL=postgis://${POSTGRES_USER}:${POSTGRES_PASSWORD_URL_ENCODED}@postgres.${PRIVATE_DOMAIN_NAME}:${POSTGRES_PORT}/${KC_POSTGRES_DB}
+KPI_DATABASE_URL=postgis://${POSTGRES_USER}:${POSTGRES_PASSWORD_URL_ENCODED}@postgres.${PRIVATE_DOMAIN_NAME}:${POSTGRES_PORT}/${KPI_POSTGRES_DB}
 
 # Replication
 KOBO_POSTGRES_REPLICATION_USER=kobo_replication
@@ -45,5 +49,5 @@ ${USE_BACKUP}POSTGRES_BACKUP_SCHEDULE=${POSTGRES_BACKUP_SCHEDULE}
 # Default Redis backup schedule is weekly at 03:00 AM UTC on Sunday.
 ${USE_BACKUP}REDIS_BACKUP_SCHEDULE=${REDIS_BACKUP_SCHEDULE}
 
-REDIS_SESSION_URL=redis://redis-cache.${PRIVATE_DOMAIN_NAME}:${REDIS_CACHE_PORT}/2
-
+REDIS_SESSION_URL=redis://:${REDIS_PASSWORD_URL_ENCODED}@redis-cache.${PRIVATE_DOMAIN_NAME}:${REDIS_CACHE_PORT}/2
+REDIS_PASSWORD=${REDIS_PASSWORD}
diff --git a/templates/kobo-deployments/envfiles/kobocat.txt.tpl b/templates/kobo-deployments/envfiles/kobocat.txt.tpl
index 3951e29..078a246 100644
--- a/templates/kobo-deployments/envfiles/kobocat.txt.tpl
+++ b/templates/kobo-deployments/envfiles/kobocat.txt.tpl
@@ -5,18 +5,18 @@ ${USE_X_FORWARDED_HOST}USE_X_FORWARDED_HOST=True
 DJANGO_SETTINGS_MODULE=onadata.settings.kc_environ
 ENKETO_VERSION=Express
 
-KOBOCAT_BROKER_URL=redis://redis-main.${PRIVATE_DOMAIN_NAME}:${REDIS_MAIN_PORT}/2
+KOBOCAT_BROKER_URL=redis://:${REDIS_PASSWORD_URL_ENCODED}@redis-main.${PRIVATE_DOMAIN_NAME}:${REDIS_MAIN_PORT}/2
 KOBOCAT_CELERY_LOG_FILE=/srv/logs/celery.log
 
 # Default KoBoCAT media backup schedule is weekly at 12:00 AM UTC on Sunday.
 ${USE_MEDIA_BACKUP}KOBOCAT_MEDIA_BACKUP_SCHEDULE=${KOBOCAT_MEDIA_BACKUP_SCHEDULE}
 
-# Mongo values come from mongo.txt
 KOBOCAT_MONGO_HOST=mongo.${PRIVATE_DOMAIN_NAME}
 KOBOCAT_MONGO_PORT=${MONGO_PORT}
-#KOBOCAT_MONGO_NAME=$$KOBO_MONGO_DB_NAME
-#KOBOCAT_MONGO_USER=$$KOBO_MONGO_USER
-#KOBOCAT_MONGO_PASS=$$KOBO_MONGO_PASS
+KOBOCAT_MONGO_NAME=formhub
+KOBOCAT_MONGO_USER=${MONGO_USER_USERNAME}
+KOBOCAT_MONGO_PASS=${MONGO_USER_PASSWORD}
+
 
 # Dev: One or more mappings from PyDev remote debugging machine file paths to `kobocat` container
 #   file paths (see https://github.com/kobotoolbox/kobocat/blob/master/docker/setup_pydev.bash).
diff --git a/templates/kobo-deployments/envfiles/kpi.txt.tpl b/templates/kobo-deployments/envfiles/kpi.txt.tpl
index 3901595..511bab2 100644
--- a/templates/kobo-deployments/envfiles/kpi.txt.tpl
+++ b/templates/kobo-deployments/envfiles/kpi.txt.tpl
@@ -4,9 +4,12 @@ ${USE_X_FORWARDED_HOST}USE_X_FORWARDED_HOST=True
 
 ENKETO_VERSION=Express
 KPI_PREFIX=/
-KPI_BROKER_URL=redis://redis-main.${PRIVATE_DOMAIN_NAME}:${REDIS_MAIN_PORT}/1
+KPI_BROKER_URL=redis://:${REDIS_PASSWORD_URL_ENCODED}@redis-main.${PRIVATE_DOMAIN_NAME}:${REDIS_MAIN_PORT}/1
 
 KPI_MONGO_HOST=mongo.${PRIVATE_DOMAIN_NAME}
 KPI_MONGO_PORT=${MONGO_PORT}
+KPI_MONGO_NAME=formhub
+KPI_MONGO_USER=${MONGO_USER_USERNAME}
+KPI_MONGO_PASS=${MONGO_USER_PASSWORD}
 
 DJANGO_LANGUAGE_CODES=en ar es fr hi ku pl pt zh-hans
diff --git a/templates/kobo-docker/docker-compose.backend.master.override.yml.tpl b/templates/kobo-docker/docker-compose.backend.master.override.yml.tpl
index dc52986..2b891e9 100644
--- a/templates/kobo-docker/docker-compose.backend.master.override.yml.tpl
+++ b/templates/kobo-docker/docker-compose.backend.master.override.yml.tpl
@@ -8,17 +8,33 @@ services:
     ${OVERRIDE_POSTGRES_SETTINGS}  - ../kobo-deployments/postgres/master/postgres.conf:/kobo-docker-scripts/master/postgres.conf
     ${POSTGRES_BACKUP_FROM_SLAVE}environment:
     ${POSTGRES_BACKUP_FROM_SLAVE}  - POSTGRES_BACKUP_FROM_SLAVE=True
-    ports:
-      - ${POSTGRES_PORT}:5432
+    ${EXPOSE_BACKEND_PORTS}ports:
+    ${EXPOSE_BACKEND_PORTS}  - ${POSTGRES_PORT}:5432
+    ${USE_BACKEND_NETWORK}networks:
+    ${USE_BACKEND_NETWORK}  kobo-be-network:
+    ${USE_BACKEND_NETWORK}    aliases:
+    ${USE_BACKEND_NETWORK}      - postgres.${PRIVATE_DOMAIN_NAME}
 
   mongo:
-    ports:
-      - ${MONGO_PORT}:27017
+    ${EXPOSE_BACKEND_PORTS}ports:
+    ${EXPOSE_BACKEND_PORTS}  - ${MONGO_PORT}:27017
+    ${USE_BACKEND_NETWORK}networks:
+    ${USE_BACKEND_NETWORK}  kobo-be-network:
+    ${USE_BACKEND_NETWORK}    aliases:
+    ${USE_BACKEND_NETWORK}      - mongo.${PRIVATE_DOMAIN_NAME}
 
   redis_main:
-    ports:
-      - ${REDIS_MAIN_PORT}:6379
+    ${EXPOSE_BACKEND_PORTS}ports:
+    ${EXPOSE_BACKEND_PORTS}  - ${REDIS_MAIN_PORT}:6379
+    ${USE_BACKEND_NETWORK}networks:
+    ${USE_BACKEND_NETWORK}  kobo-be-network:
+    ${USE_BACKEND_NETWORK}    aliases:
+    ${USE_BACKEND_NETWORK}      - redis-main.${PRIVATE_DOMAIN_NAME}
 
   redis_cache:
-    ports:
-      - ${REDIS_CACHE_PORT}:6380
+    ${EXPOSE_BACKEND_PORTS}ports:
+    ${EXPOSE_BACKEND_PORTS}  - ${REDIS_CACHE_PORT}:6380
+    ${USE_BACKEND_NETWORK}networks:
+    ${USE_BACKEND_NETWORK}  kobo-be-network:
+    ${USE_BACKEND_NETWORK}    aliases:
+    ${USE_BACKEND_NETWORK}      - redis-cache.${PRIVATE_DOMAIN_NAME}
diff --git a/templates/kobo-docker/docker-compose.frontend.override.yml.tpl b/templates/kobo-docker/docker-compose.frontend.override.yml.tpl
index 5c2520a..6cbf663 100644
--- a/templates/kobo-docker/docker-compose.frontend.override.yml.tpl
+++ b/templates/kobo-docker/docker-compose.frontend.override.yml.tpl
@@ -14,14 +14,19 @@ services:
       - NGINX_PUBLIC_PORT=${NGINX_PUBLIC_PORT}
       - KC_UWSGI_MAX_REQUESTS=${MAX_REQUESTS}
       - KC_UWSGI_CHEAPER_RSS_LIMIT_SOFT=${SOFT_LIMIT}
-    ${USE_DNS}extra_hosts:
-      ${USE_PUBLIC_DNS}- ${KOBOFORM_SUBDOMAIN}.${PUBLIC_DOMAIN_NAME}:${LOCAL_INTERFACE_IP}
-      ${USE_PUBLIC_DNS}- ${KOBOCAT_SUBDOMAIN}.${PUBLIC_DOMAIN_NAME}:${LOCAL_INTERFACE_IP}
-      ${USE_PUBLIC_DNS}- ${ENKETO_SUBDOMAIN}.${PUBLIC_DOMAIN_NAME}:${LOCAL_INTERFACE_IP}
-      ${USE_PRIVATE_DNS}- postgres.${PRIVATE_DOMAIN_NAME}:${MASTER_BACKEND_IP}
-      ${USE_PRIVATE_DNS}- mongo.${PRIVATE_DOMAIN_NAME}:${MASTER_BACKEND_IP}
-      ${USE_PRIVATE_DNS}- redis-main.${PRIVATE_DOMAIN_NAME}:${MASTER_BACKEND_IP}
-      ${USE_PRIVATE_DNS}- redis-cache.${PRIVATE_DOMAIN_NAME}:${MASTER_BACKEND_IP}
+    ${USE_EXTRA_HOSTS}extra_hosts:
+      ${USE_FAKE_DNS}- ${KOBOFORM_SUBDOMAIN}.${PUBLIC_DOMAIN_NAME}:${LOCAL_INTERFACE_IP}
+      ${USE_FAKE_DNS}- ${KOBOCAT_SUBDOMAIN}.${PUBLIC_DOMAIN_NAME}:${LOCAL_INTERFACE_IP}
+      ${USE_FAKE_DNS}- ${ENKETO_SUBDOMAIN}.${PUBLIC_DOMAIN_NAME}:${LOCAL_INTERFACE_IP}
+      ${ADD_BACKEND_EXTRA_HOSTS}- postgres.${PRIVATE_DOMAIN_NAME}:${MASTER_BACKEND_IP}
+      ${ADD_BACKEND_EXTRA_HOSTS}- mongo.${PRIVATE_DOMAIN_NAME}:${MASTER_BACKEND_IP}
+      ${ADD_BACKEND_EXTRA_HOSTS}- redis-main.${PRIVATE_DOMAIN_NAME}:${MASTER_BACKEND_IP}
+      ${ADD_BACKEND_EXTRA_HOSTS}- redis-cache.${PRIVATE_DOMAIN_NAME}:${MASTER_BACKEND_IP}
+    ${USE_BACKEND_NETWORK}networks:
+    ${USE_BACKEND_NETWORK}  kobo-be-network:
+    ${USE_BACKEND_NETWORK}    aliases:
+    ${USE_BACKEND_NETWORK}      - kobocat
+    ${USE_BACKEND_NETWORK}      - kobocat.docker.container
 
   kpi:
     ${USE_KPI_DEV_MODE}build: ${KPI_PATH}
@@ -36,28 +41,33 @@ services:
       - KPI_UWSGI_CHEAPER_RSS_LIMIT_SOFT=${SOFT_LIMIT}
       ${USE_HTTPS}- SECURE_PROXY_SSL_HEADER=HTTP_X_FORWARDED_PROTO, https
       ${USE_NPM_FROM_HOST}- FRONTEND_DEV_MODE=host
-    ${USE_DNS}extra_hosts:
-      ${USE_PUBLIC_DNS}- ${KOBOFORM_SUBDOMAIN}.${PUBLIC_DOMAIN_NAME}:${LOCAL_INTERFACE_IP}
-      ${USE_PUBLIC_DNS}- ${KOBOCAT_SUBDOMAIN}.${PUBLIC_DOMAIN_NAME}:${LOCAL_INTERFACE_IP}
-      ${USE_PUBLIC_DNS}- ${ENKETO_SUBDOMAIN}.${PUBLIC_DOMAIN_NAME}:${LOCAL_INTERFACE_IP}
-      ${USE_PRIVATE_DNS}- postgres.${PRIVATE_DOMAIN_NAME}:${MASTER_BACKEND_IP}
-      ${USE_PRIVATE_DNS}- mongo.${PRIVATE_DOMAIN_NAME}:${MASTER_BACKEND_IP}
-      ${USE_PRIVATE_DNS}- redis-main.${PRIVATE_DOMAIN_NAME}:${MASTER_BACKEND_IP}
-      ${USE_PRIVATE_DNS}- redis-cache.${PRIVATE_DOMAIN_NAME}:${MASTER_BACKEND_IP}
+    ${USE_EXTRA_HOSTS}extra_hosts:
+      ${USE_FAKE_DNS}- ${KOBOFORM_SUBDOMAIN}.${PUBLIC_DOMAIN_NAME}:${LOCAL_INTERFACE_IP}
+      ${USE_FAKE_DNS}- ${KOBOCAT_SUBDOMAIN}.${PUBLIC_DOMAIN_NAME}:${LOCAL_INTERFACE_IP}
+      ${USE_FAKE_DNS}- ${ENKETO_SUBDOMAIN}.${PUBLIC_DOMAIN_NAME}:${LOCAL_INTERFACE_IP}
+      ${ADD_BACKEND_EXTRA_HOSTS}- postgres.${PRIVATE_DOMAIN_NAME}:${MASTER_BACKEND_IP}
+      ${ADD_BACKEND_EXTRA_HOSTS}- mongo.${PRIVATE_DOMAIN_NAME}:${MASTER_BACKEND_IP}
+      ${ADD_BACKEND_EXTRA_HOSTS}- redis-main.${PRIVATE_DOMAIN_NAME}:${MASTER_BACKEND_IP}
+      ${ADD_BACKEND_EXTRA_HOSTS}- redis-cache.${PRIVATE_DOMAIN_NAME}:${MASTER_BACKEND_IP}
+    ${USE_BACKEND_NETWORK}networks:
+    ${USE_BACKEND_NETWORK}  kobo-be-network:
+    ${USE_BACKEND_NETWORK}    aliases:
+    ${USE_BACKEND_NETWORK}      - kpi
+    ${USE_BACKEND_NETWORK}      - kpi.docker.container
 
   nginx:
     environment:
       - NGINX_PUBLIC_PORT=${NGINX_PUBLIC_PORT}
     ports:
       - ${NGINX_EXPOSED_PORT}:80
-    ${USE_DNS}extra_hosts:
-      ${USE_PUBLIC_DNS}- ${KOBOFORM_SUBDOMAIN}.${PUBLIC_DOMAIN_NAME}:${LOCAL_INTERFACE_IP}
-      ${USE_PUBLIC_DNS}- ${KOBOCAT_SUBDOMAIN}.${PUBLIC_DOMAIN_NAME}:${LOCAL_INTERFACE_IP}
-      ${USE_PUBLIC_DNS}- ${ENKETO_SUBDOMAIN}.${PUBLIC_DOMAIN_NAME}:${LOCAL_INTERFACE_IP}
-      ${USE_PRIVATE_DNS}- postgres.${PRIVATE_DOMAIN_NAME}:${MASTER_BACKEND_IP}
-      ${USE_PRIVATE_DNS}- mongo.${PRIVATE_DOMAIN_NAME}:${MASTER_BACKEND_IP}
-      ${USE_PRIVATE_DNS}- redis-main.${PRIVATE_DOMAIN_NAME}:${MASTER_BACKEND_IP}
-      ${USE_PRIVATE_DNS}- redis-cache.${PRIVATE_DOMAIN_NAME}:${MASTER_BACKEND_IP}
+    ${USE_EXTRA_HOSTS}extra_hosts:
+      ${USE_FAKE_DNS}- ${KOBOFORM_SUBDOMAIN}.${PUBLIC_DOMAIN_NAME}:${LOCAL_INTERFACE_IP}
+      ${USE_FAKE_DNS}- ${KOBOCAT_SUBDOMAIN}.${PUBLIC_DOMAIN_NAME}:${LOCAL_INTERFACE_IP}
+      ${USE_FAKE_DNS}- ${ENKETO_SUBDOMAIN}.${PUBLIC_DOMAIN_NAME}:${LOCAL_INTERFACE_IP}
+      ${ADD_BACKEND_EXTRA_HOSTS}- postgres.${PRIVATE_DOMAIN_NAME}:${MASTER_BACKEND_IP}
+      ${ADD_BACKEND_EXTRA_HOSTS}- mongo.${PRIVATE_DOMAIN_NAME}:${MASTER_BACKEND_IP}
+      ${ADD_BACKEND_EXTRA_HOSTS}- redis-main.${PRIVATE_DOMAIN_NAME}:${MASTER_BACKEND_IP}
+      ${ADD_BACKEND_EXTRA_HOSTS}- redis-cache.${PRIVATE_DOMAIN_NAME}:${MASTER_BACKEND_IP}
     networks:
       kobo-fe-network:
         aliases:
@@ -65,12 +75,26 @@ services:
           - ${KOBOCAT_SUBDOMAIN}.${INTERNAL_DOMAIN_NAME}
           - ${ENKETO_SUBDOMAIN}.${INTERNAL_DOMAIN_NAME}
 
-  ${USE_DNS}enketo_express:
-    ${USE_DNS}extra_hosts:
-      ${USE_PUBLIC_DNS}- ${KOBOFORM_SUBDOMAIN}.${PUBLIC_DOMAIN_NAME}:${LOCAL_INTERFACE_IP}
-      ${USE_PUBLIC_DNS}- ${KOBOCAT_SUBDOMAIN}.${PUBLIC_DOMAIN_NAME}:${LOCAL_INTERFACE_IP}
-      ${USE_PUBLIC_DNS}- ${ENKETO_SUBDOMAIN}.${PUBLIC_DOMAIN_NAME}:${LOCAL_INTERFACE_IP}
-      ${USE_PRIVATE_DNS}- postgres.${PRIVATE_DOMAIN_NAME}:${MASTER_BACKEND_IP}
-      ${USE_PRIVATE_DNS}- mongo.${PRIVATE_DOMAIN_NAME}:${MASTER_BACKEND_IP}
-      ${USE_PRIVATE_DNS}- redis-main.${PRIVATE_DOMAIN_NAME}:${MASTER_BACKEND_IP}
-      ${USE_PRIVATE_DNS}- redis-cache.${PRIVATE_DOMAIN_NAME}:${MASTER_BACKEND_IP}
+  enketo_express:
+    # `DUMMY_ENV` is only there to avoid extra complex condition to override
+    # `enketo_express` section or not. It allows to always this section whatever
+    # `USE_EXTRA_HOSTS` and `USE_BACKEND_NETWORK` values are.
+    environment:
+      - DUMMY_ENV=True
+    ${USE_EXTRA_HOSTS}extra_hosts:
+      ${USE_FAKE_DNS}- ${KOBOFORM_SUBDOMAIN}.${PUBLIC_DOMAIN_NAME}:${LOCAL_INTERFACE_IP}
+      ${USE_FAKE_DNS}- ${KOBOCAT_SUBDOMAIN}.${PUBLIC_DOMAIN_NAME}:${LOCAL_INTERFACE_IP}
+      ${USE_FAKE_DNS}- ${ENKETO_SUBDOMAIN}.${PUBLIC_DOMAIN_NAME}:${LOCAL_INTERFACE_IP}
+      ${ADD_BACKEND_EXTRA_HOSTS}- postgres.${PRIVATE_DOMAIN_NAME}:${MASTER_BACKEND_IP}
+      ${ADD_BACKEND_EXTRA_HOSTS}- mongo.${PRIVATE_DOMAIN_NAME}:${MASTER_BACKEND_IP}
+      ${ADD_BACKEND_EXTRA_HOSTS}- redis-main.${PRIVATE_DOMAIN_NAME}:${MASTER_BACKEND_IP}
+      ${ADD_BACKEND_EXTRA_HOSTS}- redis-cache.${PRIVATE_DOMAIN_NAME}:${MASTER_BACKEND_IP}
+    ${USE_BACKEND_NETWORK}networks:
+    ${USE_BACKEND_NETWORK}  kobo-be-network:
+    ${USE_BACKEND_NETWORK}    aliases:
+    ${USE_BACKEND_NETWORK}      - enketo_express
+
+${USE_BACKEND_NETWORK}networks:
+${USE_BACKEND_NETWORK}  kobo-be-network:
+${USE_BACKEND_NETWORK}    external:
+${USE_BACKEND_NETWORK}      name: ${DOCKER_PREFIX}_kobo-be-network

Secure backend ports 
By default PostgreSQL,  Redis, Mongo ports are listening on `0.0.0.0`. 
It should be restricted to private ip. 

For references:
https://github.com/kobotoolbox/kobo-docker/issues/249
diff --git a/tests/test_config.py b/tests/test_config.py
index be117d6..54ae200 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -3,9 +3,8 @@ from __future__ import unicode_literals
 
 import json
 import pytest
-import random
+import os
 import shutil
-import string
 import tempfile
 import time
 
@@ -28,6 +27,12 @@ def reset_config(config_object):
     config_object.__config = config_dict
 
 
+def write_trigger_upsert_db_users(*args):
+    content = args[1]
+    with open("/tmp/upsert_db_users", "w") as f:
+        f.write(content)
+
+
 def test_read_config(overrides=None):
 
     config_dict = dict(Config.get_config_template())
@@ -168,7 +173,7 @@ def test_proxy_letsencrypt():
                                                "test@test.com",
                                                Config.TRUE,
                                                Config.DEFAULT_NGINX_PORT])  # Use default options
-        config_object._Config__question_reverse_proxy()
+        config_object._Config__questions_reverse_proxy()
         assert config_object.proxy
         assert config_object.use_letsencrypt
         assert config_object.block_common_http_ports
@@ -187,7 +192,7 @@ def test_proxy_no_letsencrypt_advanced():
 
         with patch("helpers.cli.CLI.colored_input") as mock_colored_input:
             mock_colored_input.side_effect = iter([Config.FALSE, Config.FALSE, proxy_port])
-            config_object._Config__question_reverse_proxy()
+            config_object._Config__questions_reverse_proxy()
             assert config_object.proxy
             assert not config_object.use_letsencrypt
             assert not config_object.block_common_http_ports
@@ -201,7 +206,7 @@ def test_proxy_no_letsencrypt():
     assert config_object.use_letsencrypt
 
     with patch.object(CLI, "colored_input", return_value=Config.FALSE) as mock_ci:
-        config_object._Config__question_reverse_proxy()
+        config_object._Config__questions_reverse_proxy()
         assert config_object.proxy
         assert not config_object.use_letsencrypt
         assert config_object.block_common_http_ports
@@ -219,7 +224,7 @@ def test_proxy_no_letsencrypt_retains_custom_nginx_proxy_port():
             CLI, "colored_input",
             new=classmethod(lambda cls, message, color, default: default)
     ) as mock_ci:
-        config_object._Config__question_reverse_proxy()
+        config_object._Config__questions_reverse_proxy()
         assert(config_object.get_config().get("nginx_proxy_port")
                == str(CUSTOM_PROXY_PORT))
 
@@ -236,7 +241,7 @@ def test_no_proxy_no_ssl():
         assert not config_object.is_secure
 
         with patch.object(CLI, "colored_input", return_value=Config.FALSE) as mock_ci_2:
-            config_object._Config__question_reverse_proxy()
+            config_object._Config__questions_reverse_proxy()
             assert not config_object.proxy
             assert not config_object.use_letsencrypt
             assert not config_object.block_common_http_ports
@@ -258,7 +263,7 @@ def test_proxy_no_ssl_advanced():
         proxy_port = Config.DEFAULT_NGINX_PORT
         with patch("helpers.cli.CLI.colored_input") as mock_colored_input_1:
             mock_colored_input_1.side_effect = iter([Config.TRUE, Config.FALSE, proxy_port])
-            config_object._Config__question_reverse_proxy()
+            config_object._Config__questions_reverse_proxy()
             assert config_object.proxy
             assert not config_object.use_letsencrypt
             assert not config_object.block_common_http_ports
@@ -268,7 +273,7 @@ def test_proxy_no_ssl_advanced():
         proxy_port = Config.DEFAULT_PROXY_PORT
         with patch("helpers.cli.CLI.colored_input") as mock_colored_input_2:
             mock_colored_input_2.side_effect = iter([Config.TRUE, Config.TRUE, proxy_port])
-            config_object._Config__question_reverse_proxy()
+            config_object._Config__questions_reverse_proxy()
             assert config_object.proxy
             assert not config_object.use_letsencrypt
             assert config_object.block_common_http_ports
@@ -302,6 +307,7 @@ def test_create_directory():
     shutil.rmtree(destination_path)
 
 
+@patch('helpers.config.Config.write_config', new=lambda *a, **k: None)
 def test_maintenance():
     config_object = test_read_config()
 
@@ -326,3 +332,204 @@ def test_maintenance():
         expected_str = 'Tuesday,&nbsp;January&nbsp;01&nbsp;at&nbsp;02:00&nbsp;GMT'
         assert config.get('maintenance_date_str') == expected_str
 
+
+def test_exposed_ports():
+    config_object = test_read_config()
+    with patch.object(CLI, "colored_input", return_value=Config.TRUE) as mock_ci:
+        # Choose multi servers options
+        config_object._Config__questions_multi_servers()
+
+        with patch("helpers.cli.CLI.colored_input") as mock_ci:
+            # Choose to customize ports
+            mock_ci.side_effect = iter([Config.TRUE, "5532", "27117", "6479", "6480"])
+            config_object._Config__questions_ports()
+
+            assert config_object._Config__config["postgresql_port"] == "5532"
+            assert config_object._Config__config["mongo_port"] == "27117"
+            assert config_object._Config__config["redis_main_port"] == "6479"
+            assert config_object._Config__config["redis_cache_port"] == "6480"
+            assert config_object.expose_backend_ports
+
+    with patch.object(CLI, "colored_input", return_value=Config.FALSE) as mock_ci_1:
+        # Choose to single server
+        config_object._Config__questions_multi_servers()
+
+        with patch.object(CLI, "colored_input", return_value=Config.FALSE) as mock_ci_2:
+            # Choose to not expose ports
+            config_object._Config__questions_ports()
+
+            assert config_object._Config__config["postgresql_port"] == "5432"
+            assert config_object._Config__config["mongo_port"] == "27017"
+            assert config_object._Config__config["redis_main_port"] == "6379"
+            assert config_object._Config__config["redis_cache_port"] == "6380"
+            assert not config_object.expose_backend_ports
+
+
+@patch('helpers.config.Config.write_config', new=lambda *a, **k: None)
+def test_force_secure_mongo():
+    config_object = test_read_config()
+    config_ = config_object.get_config()
+
+    with patch("helpers.cli.CLI.colored_input") as mock_ci:
+        # We need to run it like if user has already run the setup once to
+        # force MongoDB to "upsert" users.
+        config_object._Config__first_time = False
+        # Run with no advanced options
+        mock_ci.side_effect = iter([
+            config_["kobodocker_path"],
+            Config.TRUE,  # Confirm path
+            config_["advanced"],
+            config_["local_installation"],
+            config_["public_domain_name"],
+            config_["kpi_subdomain"],
+            config_["kc_subdomain"],
+            config_["ee_subdomain"],
+            Config.FALSE,  # Do you want to use HTTPS?
+            config_.get("smtp_host", ""),
+            config_.get("smtp_port", "25"),
+            config_.get("smtp_user", ""),
+            "test@test.com",
+            config_["super_user_username"],
+            config_["super_user_password"],
+            config_["use_backup"]
+        ])
+        new_config = config_object.build()
+        assert new_config.get("mongo_secured") == Config.TRUE
+
+
+@patch('helpers.config.Config._Config__write_upsert_db_users_trigger_file',
+       new=write_trigger_upsert_db_users)
+def test_secure_mongo_advanced_options():
+    config_object = test_read_config()
+    with patch("helpers.cli.CLI.colored_input") as mock_ci:
+        mock_ci.side_effect = iter([
+            "root",
+            "root_password",
+            "mongo_kobo_user",
+            "mongo_password"
+        ])
+        config_object._Config__questions_mongo()
+        assert not os.path.exists("/tmp/upsert_db_users")
+
+
+@patch('helpers.config.Config._Config__write_upsert_db_users_trigger_file',
+       new=write_trigger_upsert_db_users)
+def test_update_mongo_passwords():
+    config_object = test_read_config()
+    with patch("helpers.cli.CLI.colored_input") as mock_ci:
+        config_object._Config__first_time = False
+        config_object._Config__config["mongo_root_username"] = 'root'
+        config_object._Config__config["mongo_user_username"] = 'user'
+        mock_ci.side_effect = iter([
+            "root",
+            "root_password",
+            "user",
+            "mongo_password"
+        ])
+        config_object._Config__questions_mongo()
+        assert os.path.exists("/tmp/upsert_db_users")
+        assert os.path.getsize("/tmp/upsert_db_users") == 0
+        os.remove("/tmp/upsert_db_users")
+
+
+@patch('helpers.config.Config._Config__write_upsert_db_users_trigger_file',
+       new=write_trigger_upsert_db_users)
+def test_update_mongo_usernames():
+    config_object = test_read_config()
+    with patch("helpers.cli.CLI.colored_input") as mock_ci:
+        config_object._Config__first_time = False
+        config_object._Config__config["mongo_root_username"] = 'root'
+        config_object._Config__config["mongo_user_username"] = 'user'
+        mock_ci.side_effect = iter([
+            "admin",
+            "root_password",
+            "another_user",
+            "mongo_password",
+            Config.TRUE  # Delete users
+        ])
+        config_object._Config__questions_mongo()
+        assert os.path.exists("/tmp/upsert_db_users")
+        with open("/tmp/upsert_db_users", "r") as f:
+            content = f.read()
+        expected_content = "user\tformhub\nroot\tadmin"
+        assert content == expected_content
+        os.remove("/tmp/upsert_db_users")
+
+
+@patch('helpers.config.Config._Config__write_upsert_db_users_trigger_file',
+       new=write_trigger_upsert_db_users)
+def test_update_postgres_password():
+    """
+    Does **NOT** test if user is updated in PostgreSQL but the file creation
+    (and its content) used to trigger the action by PostgreSQL container.
+
+    When password changes, file must contain `<user><TAB><deletion boolean>`
+    Users should not be deleted if they already exist.
+    """
+    config_object = test_read_config()
+    with patch("helpers.cli.CLI.colored_input") as mock_ci:
+        config_object._Config__first_time = False
+        config_object._Config__config["postgres_user"] = 'user'
+        config_object._Config__config["postgres_password"] = 'password'
+        mock_ci.side_effect = iter([
+            "kobocat",
+            "koboform",
+            "user",
+            "user_password",
+            Config.FALSE  # Tweak settings
+        ])
+        config_object._Config__questions_postgres()
+        assert os.path.exists("/tmp/upsert_db_users")
+        with open("/tmp/upsert_db_users", "r") as f:
+            content = f.read()
+        expected_content = "user\tfalse"
+        assert content == expected_content
+        os.remove("/tmp/upsert_db_users")
+
+
+@patch('helpers.config.Config._Config__write_upsert_db_users_trigger_file',
+       new=write_trigger_upsert_db_users)
+def test_update_postgres_username():
+    """
+    Does **NOT** test if user is updated in PostgreSQL but the file creation
+    (and its content) used to trigger the action by PostgreSQL container.
+
+    When username changes, file must contain `<user><TAB><deletion boolean>`
+    """
+    config_object = test_read_config()
+    with patch("helpers.cli.CLI.colored_input") as mock_ci:
+        config_object._Config__first_time = False
+        config_object._Config__config["postgres_user"] = 'user'
+        config_object._Config__config["postgres_password"] = 'password'
+        mock_ci.side_effect = iter([
+            "kobocat",
+            "koboform",
+            "another_user",
+            "password",
+            Config.TRUE,  # Delete user
+            Config.FALSE  # Tweak settings
+        ])
+        config_object._Config__questions_postgres()
+        assert os.path.exists("/tmp/upsert_db_users")
+        with open("/tmp/upsert_db_users", "r") as f:
+            content = f.read()
+        expected_content = "user\ttrue"
+        assert content == expected_content
+        os.remove("/tmp/upsert_db_users")
+
+
+def test_update_postgres_db_name_from_single_database():
+    """
+    Simulate upgrade from single database to two databases.
+    With two databases, KoBoCat has its own database. We ensure that
+    `kc_postgres_db` gets `postgres_db` value.
+    """
+    config_object = test_read_config()
+    config = config_object.get_config()
+    old_db_name = "postgres_db_kobo"
+    config_object._Config__config["postgres_db"] = old_db_name
+    assert config.get("kc_postgres_db") == config_object.get_config_template()["kc_postgres_db"]
+    assert "postgres_db" in config
+    config = config_object._Config__upgrade_kc_db(config)
+    assert config.get("kc_postgres_db") == old_db_name
+    assert "postgres_db" not in config

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest pytest-cov pytest-xdist pytest-mock pytest-asyncio pytest-bdd pytest-benchmark pytest-randomly responses mock hypothesis freezegun trustme requests-mock requests tomlkit
apt-get update apt-get install -y gcc
3.9
requirements/base.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs==25.3.0
certifi==2025.1.31
cffi==1.17.1
charset-normalizer==3.4.1
coverage==7.8.0
cryptography==44.0.2
exceptiongroup==1.2.2
execnet==2.1.1
freezegun==1.5.1
gherkin-official==29.0.0
hypothesis==6.130.5
idna==3.10
importlib_metadata==8.6.1
iniconfig==2.1.0
-e git+https://github.com/kobotoolbox/kobo-install.git@12b45f83054b79f0e0ad847050192dca1f596ff9#egg=KoBoInstall
Mako==1.3.9
MarkupSafe==3.0.2
mock==5.2.0
packaging==24.2
parse==1.20.2
parse_type==0.6.4
pluggy==1.5.0
py-cpuinfo==9.0.0
pycparser==2.22
pytest==8.3.5
pytest-asyncio==0.26.0
pytest-bdd==8.1.0
pytest-benchmark==5.1.0
pytest-cov==6.0.0
pytest-mock==3.14.0
pytest-randomly==3.16.0
pytest-xdist==3.6.1
python-dateutil==2.9.0.post0
PyYAML==6.0.2
requests==2.32.3
requests-mock==1.12.1
responses==0.25.7
six==1.17.0
sortedcontainers==2.4.0
tomli==2.2.1
tomlkit==0.13.2
trustme==1.2.1
typing_extensions==4.13.0
urllib3==2.3.0
zipp==3.21.0

name: kobo-install
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - attrs==25.3.0
      - certifi==2025.1.31
      - cffi==1.17.1
      - charset-normalizer==3.4.1
      - coverage==7.8.0
      - cryptography==44.0.2
      - exceptiongroup==1.2.2
      - execnet==2.1.1
      - freezegun==1.5.1
      - gherkin-official==29.0.0
      - hypothesis==6.130.5
      - idna==3.10
      - importlib-metadata==8.6.1
      - iniconfig==2.1.0
      - mako==1.3.9
      - markupsafe==3.0.2
      - mock==5.2.0
      - packaging==24.2
      - parse==1.20.2
      - parse-type==0.6.4
      - pluggy==1.5.0
      - py-cpuinfo==9.0.0
      - pycparser==2.22
      - pytest==8.3.5
      - pytest-asyncio==0.26.0
      - pytest-bdd==8.1.0
      - pytest-benchmark==5.1.0
      - pytest-cov==6.0.0
      - pytest-mock==3.14.0
      - pytest-randomly==3.16.0
      - pytest-xdist==3.6.1
      - python-dateutil==2.9.0.post0
      - pyyaml==6.0.2
      - requests==2.32.3
      - requests-mock==1.12.1
      - responses==0.25.7
      - six==1.17.0
      - sortedcontainers==2.4.0
      - tomli==2.2.1
      - tomlkit==0.13.2
      - trustme==1.2.1
      - typing-extensions==4.13.0
      - urllib3==2.3.0
      - zipp==3.21.0
prefix: /opt/conda/envs/kobo-install

tests/test_config.py::test_secure_mongo_advanced_options tests/test_config.py::test_exposed_ports tests/test_config.py::test_update_postgres_password tests/test_config.py::test_update_mongo_passwords tests/test_config.py::test_proxy_no_letsencrypt_retains_custom_nginx_proxy_port tests/test_config.py::test_proxy_no_letsencrypt tests/test_config.py::test_no_proxy_no_ssl tests/test_config.py::test_update_postgres_username tests/test_config.py::test_proxy_no_letsencrypt_advanced tests/test_config.py::test_proxy_no_ssl_advanced tests/test_config.py::test_proxy_letsencrypt tests/test_config.py::test_update_postgres_db_name_from_single_database tests/test_config.py::test_update_mongo_usernames
tests/test_config.py::test_force_secure_mongo
tests/test_config.py::test_create_directory tests/test_config.py::test_dev_mode tests/test_config.py::test_maintenance tests/test_config.py::test_port_allowed tests/test_config.py::test_staging_mode tests/test_config.py::test_server_roles_questions tests/test_config.py::test_advanced_options tests/test_config.py::test_use_https tests/test_config.py::test_installation tests/test_config.py::test_read_config
templates/kobo-deployments/enketo_express/config.json.tpl templates/kobo-docker/docker-compose.frontend.override.yml.tpl helpers/template.py readme.md helpers/config.py templates/kobo-docker/docker-compose.backend.master.override.yml.tpl templates/kobo-deployments/envfiles/kobocat.txt.tpl templates/kobo-deployments/envfiles/databases.txt.tpl helpers/upgrading.py templates/kobo-deployments/envfiles/kpi.txt.tpl helpers/command.py helpers/cli.py run.py
templates/kobo-deployments/enketo_express/config.json.tpl templates/kobo-docker/docker-compose.frontend.override.yml.tpl helpers/template.py readme.md helpers/config.py templates/kobo-docker/docker-compose.backend.master.override.yml.tpl templates/kobo-deployments/envfiles/kobocat.txt.tpl templates/kobo-deployments/envfiles/databases.txt.tpl helpers/upgrading.py templates/kobo-deployments/envfiles/kpi.txt.tpl helpers/command.py helpers/cli.py run.py