diff --git a/README.md b/README.md
index 2280a00..fbd3c66 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,8 @@ Features:
 * Supports Cloudflare warp+
 * Install with a single command
 * Telegram bot for user management
+* Create backup from users and configuration
+* Restore users and configuration from backup
 
 Supported OS:
 * Ubuntu 22.04
@@ -76,7 +78,7 @@ Help message of the script:
 Usage: reality-ezpz.sh [-t|--transport=tcp|http|grpc|ws|tuic|hysteria2] [-d|--domain=<domain>] [--server=<server>] [--regenerate] [--default]
   [-r|--restart] [--enable-safenet=true|false] [--port=<port>] [-c|--core=xray|sing-box]
   [--enable-warp=true|false] [--warp-license=<license>] [--security=reality|letsencrypt|selfsigned] [-m|--menu] [--show-server-config] 
-  [--add-user=<username>] [--lists-users] [--show-user=<username>] [--delete-user=<username>] [-u|--uninstall]
+  [--add-user=<username>] [--lists-users] [--show-user=<username>] [--delete-user=<username>] [--backup] [--restore=<url|file>] [-u|--uninstall]
 
   -t, --transport <tcp|http|grpc|ws|tuic|hysteria2> Transport protocol (tcp, http, grpc, ws, tuic, hysteria2, default: tcp)
   -d, --domain <domain>     Domain to use as SNI (default: www.google.com)
@@ -100,6 +102,8 @@ Usage: reality-ezpz.sh [-t|--transport=tcp|http|grpc|ws|tuic|hysteria2] [-d|--do
       --list-users          List all users
       --show-user <username> Shows the config and QR code of the user
       --delete-user <username> Delete the user
+      --backup              Backup users and configuration and upload it to keep.sh
+      --restore <url|file>  Restore backup from URL or file
   -h, --help                Display this help message
 ```
 
@@ -280,6 +284,30 @@ bash <(curl -sL https://bit.ly/realityez) -c xray
 ```
 Valid options are `xray` and `sing-box`.
 
+### Create backup
+You can create a backup from users and configuration and upload it to free.keep.sh by using `--backup` option:
+```
+bash <(curl -sL https://bit.ly/realityez) --backup
+```
+This command will give you a URL to download you backup file. The URL is only valid for 24h.
+
+### Restore backup
+You can restore a previously created backup file with `--restore` option.
+
+You need to give the path or URL of the backup file to restore:
+```
+bash <(curl -sL https://bit.ly/realityez) --restore /path/to/backup.tar.gz
+```
+or
+```
+bash <(curl -sL https://bit.ly/realityez) --restore "https://www.example.com/backup.tar.gz"
+```
+
+You can migrate users and configuration from one server to another by:
+
+1. Create backup in the old server and copy the URL of backup file
+1. Restore the URL of backup file in the new server
+
 ### Text-based user interface (TUI)
 You can also use the TUI for changing the configuration of the service.
 
diff --git a/reality-ezpz.sh b/reality-ezpz.sh
index 2dba7c2..ea4553c 100644
--- a/reality-ezpz.sh
+++ b/reality-ezpz.sh
@@ -98,13 +98,15 @@ regex[ip]="^([0-9]{1,3}\.){3}[0-9]{1,3}$"
 regex[tgbot_token]="^[0-9]{8,10}:[a-zA-Z0-9_-]{35}$"
 regex[tgbot_admins]="^[a-zA-Z][a-zA-Z0-9_]{4,31}(,[a-zA-Z][a-zA-Z0-9_]{4,31})*$"
 regex[domain_port]="^[a-zA-Z0-9]+([-.][a-zA-Z0-9]+)*\.[a-zA-Z]{2,}(:[1-9][0-9]*)?$"
+regex[file_path]="^[a-zA-Z0-9_/.-]+$"
+regex[url]="^(http|https)://([a-zA-Z0-9.-]+\.[a-zA-Z]{2,}|[0-9]{1,3}(\.[0-9]{1,3}){3})(:[0-9]{1,5})?(/.*)?$"
 
 function show_help {
   echo ""
   echo "Usage: reality-ezpz.sh [-t|--transport=tcp|http|grpc|ws|tuic|hysteria2] [-d|--domain=<domain>] [--server=<server>] [--regenerate] [--default]
   [-r|--restart] [--enable-safenet=true|false] [--port=<port>] [-c|--core=xray|sing-box]
   [--enable-warp=true|false] [--warp-license=<license>] [--security=reality|letsencrypt|selfsigned] [-m|--menu] [--show-server-config] 
-  [--add-user=<username>] [--lists-users] [--show-user=<username>] [--delete-user=<username>] [-u|--uninstall]"
+  [--add-user=<username>] [--lists-users] [--show-user=<username>] [--delete-user=<username>] [--backup] [--restore=<url|file>] [-u|--uninstall]"
   echo ""
   echo "  -t, --transport <tcp|http|grpc|ws|tuic|hysteria2> Transport protocol (tcp, http, grpc, ws, tuic, hysteria2, default: ${defaults[transport]})"
   echo "  -d, --domain <domain>     Domain to use as SNI (default: ${defaults[domain]})"
@@ -128,13 +130,15 @@ function show_help {
   echo "      --list-users          List all users"
   echo "      --show-user <username> Shows the config and QR code of the user"
   echo "      --delete-user <username> Delete the user"
+  echo "      --backup              Backup users and configuration and upload it to keep.sh"
+  echo "      --restore <url|file>  Restore backup from URL or file"
   echo "  -h, --help                Display this help message"
   return 1
 }
 
 function parse_args {
   local opts
-  opts=$(getopt -o t:d:ruc:mh --long transport:,domain:,server:,regenerate,default,restart,uninstall,enable-safenet:,port:,warp-license:,enable-warp:,core:,security:,menu,show-server-config,add-user:,list-users,show-user:,delete-user:,enable-tgbot:,tgbot-token:,tgbot-admins:,help -- "$@")
+  opts=$(getopt -o t:d:ruc:mh --long transport:,domain:,server:,regenerate,default,restart,uninstall,enable-safenet:,port:,warp-license:,enable-warp:,core:,security:,menu,show-server-config,add-user:,list-users,show-user:,delete-user:,backup,restore:,enable-tgbot:,tgbot-token:,tgbot-admins:,help -- "$@")
   if [[ $? -ne 0 ]]; then
     return 1
   fi
@@ -312,6 +316,18 @@ function parse_args {
         args[delete_user]="$2"
         shift 2
         ;;
+      --backup)
+        args[backup]=true
+        shift
+        ;;
+      --restore)
+        args[restore]="$2"
+        if [[ ! ${args[restore]} =~ ${regex[file_path]} ]] && [[ ! ${args[restore]} =~ ${regex[url]} ]]; then
+          echo "Invalid: Backup file path or URL is not valid."
+          return 1
+        fi
+        shift 2
+        ;;
       -h|--help)
         return 1
         ;;
@@ -333,7 +349,49 @@ function parse_args {
   if [[ -n ${args[warp_license]} ]]; then
     args[warp]=ON
   fi
+}
 
+function backup {
+  local backup_name
+  local backup_file_url
+  local exit_code
+  backup_name=reality-ezpz-backup-$(date +%Y-%m-%d_%H-%M-%S).tar.gz
+  tar -czf "/tmp/${backup_name}" -C "${config_path}" ./
+  if ! backup_file_url=$(curl -fsS --upload-file "/tmp/${backup_name}" https://free.keep.sh); then
+    rm -f "/tmp/${backup_name}"
+    echo "Error in uploading backup file" >&2
+    return 1
+  fi
+  rm -f "/tmp/${backup_name}"
+  echo "${backup_file_url}"
+  return
+}
+
+function restore {
+  local backup_file=$1
+  local temp_file
+  if [[ ! -r ${backup_file} ]]; then
+    temp_file=$(mktemp -u)
+    if ! curl -fSsL "${backup_file}" -o "${temp_file}"; then
+      echo "Cannot download or find backup file" >&2
+      return 1
+    fi
+    backup_file="${temp_file}"
+  fi
+  if ! tar -tzf "${backup_file}" | grep -q config; then
+    echo "The provided file is not a reality-ezpz backup file." >&2
+    rm -f "${temp_file}"
+    return 1
+  fi
+  rm -rf "${config_path}"
+  mkdir -p "${config_path}"
+  if ! tar -xzf "${backup_file}" -C "${config_path}"; then
+    echo "Error in backup restore." >&2
+    rm -f "${temp_file}"
+    return 1
+  fi
+  rm -f "${temp_file}"
+  return
 }
 
 function dict_expander {
@@ -707,7 +765,7 @@ services:
       BOT_ADMIN: ${config[tgbot_admins]}
     volumes:
     - /var/run/docker.sock:/var/run/docker.sock
-    - ../:/opt/reality-ezpz
+    - ../:${config_path}
     - /etc/docker/:/etc/docker/
     networks:
     - tgbot
@@ -853,7 +911,7 @@ EOF
 function generate_tgbot_dockerfile {
   cat >"${path[tgbot_dockerfile]}" << EOF
 FROM ${image[python]}
-WORKDIR /opt/reality-ezpz/tgbot
+WORKDIR ${config_path}/tgbot
 RUN apk add --no-cache docker-cli-compose curl bash newt libqrencode sudo openssl jq
 RUN pip install --no-cache-dir python-telegram-bot==13.5
 CMD [ "python", "./tgbot.py" ]
@@ -1336,7 +1394,7 @@ function upgrade {
   local warp_id
   if [[ -e "${HOME}/reality/config" ]]; then
     ${docker_cmd} --project-directory "${HOME}/reality" down --remove-orphans --timeout 2
-    mv -f "${HOME}/reality" /opt/reality-ezpz
+    mv -f "${HOME}/reality" ${config_path}
   fi
   uuid=$(grep '^uuid=' "${path[config]}" 2>/dev/null | cut -d= -f2 || true)
   if [[ -n $uuid ]]; then
@@ -1442,7 +1500,7 @@ function add_user_menu {
       break
     fi
     view_user_menu "${username}"
-  done   
+  done
 }
 
 function delete_user_menu {
@@ -1654,6 +1712,8 @@ function configuration_menu {
       "10" "Restart Services" \
       "11" "Regenerate Keys" \
       "12" "Restore Defaults" \
+      "13" "Create Backup" \
+      "14" "Restore Backup" \
       3>&1 1>&2 2>&3)
     if [[ $? -ne 0 ]]; then
       break
@@ -1695,6 +1755,12 @@ function configuration_menu {
       12 )
         restore_defaults_menu
         ;;
+      13 )
+        backup_menu
+        ;;
+      14 )
+        restore_backup_menu
+        ;;
     esac
   done
 }
@@ -2030,6 +2096,64 @@ function config_tgbot_menu {
   config[tgbot_admins]=$old_tgbot_admins
 }
 
+function backup_menu {
+  local result
+  whiptail \
+    --clear \
+    --backtitle "$BACKTITLE" \
+    --title "Backup" \
+    --yesno "Do you want to create a backup from users and configuration?" \
+    $HEIGHT $WIDTH \
+    3>&1 1>&2 2>&3
+  if [[ $? -ne 0 ]]; then
+    return
+  fi
+  if result=$(backup 2>&1); then
+    echo "Backup has been create and uploaded successfully."
+    echo "You can download the backup file from here:"
+    echo "${result}"
+    echo ""
+    echo "The URL is valid for 24h."
+    echo
+    echo "Press Enter to return ..."
+    read
+  else
+    message_box "Backup Failed" "${result}"
+  fi
+}
+
+function restore_backup_menu {
+  local backup_file
+  local result
+  while true; do
+    backup_file=$(whiptail \
+      --clear \
+      --backtitle "$BACKTITLE" \
+      --title "Restore Backup" \
+      --inputbox "Enter backup file path or URL" \
+      $HEIGHT $WIDTH \
+      3>&1 1>&2 2>&3)
+    if [[ $? -ne 0 ]]; then
+      break
+    fi
+    if [[ ! $backup_file =~ ${regex[file_path]} ]] && [[ ! $backup_file =~ ${regex[url]} ]]; then
+      message_box "Invalid Backup path of URL" "Backup file path or URL is not valid."
+      continue
+    fi
+    if result=$(restore ${backup_file} 2>&1); then
+      parse_config_file
+      parse_users_file
+      build_config
+      update_config_file
+      update_users_file
+      message_box "Backup Restore Successful" "Backup has been restored successfully."
+      break
+    else
+      message_box "Backup Restore Failed" "${result}"
+    fi
+  done
+}
+
 function restart_docker_compose {
   ${docker_cmd} --project-directory ${config_path} -p ${compose_project} down --remove-orphans --timeout 2 || true
   ${docker_cmd} --project-directory ${config_path} -p ${compose_project} up --build -d --remove-orphans --build
@@ -2294,6 +2418,19 @@ if [[ $EUID -ne 0 ]]; then
     echo "This script must be run as root."
     exit 1
 fi
+if [[ ${args[backup]} == true ]]; then
+  if backup_url=$(backup); then
+    echo "Backup created successfully. You can download the backup file from this address:"
+    echo "${backup_url}"
+    echo "The URL is valid for 24h."
+  fi
+fi
+if [[ -n ${args[restore]} ]]; then
+  if restore ${args[restore]}; then
+    args[restart]=true
+    echo "Backup has been restored successfully."
+  fi
+fi
 generate_file_list
 install_packages
 install_docker