#!/bin/bash # set -x # ****************************************************************************** # ***************** GLOBAL VARIABLES START ********************************* RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' MAGENTA='\033[0;35m' CYAN='\033[0;36m' ORANGE='\033[0;33m' BOLD='\033[1m' NC='\033[0m' NOCO_HOME="${HOME}/.nocodb/" # ***************** GLOBAL VARIABLES END *********************************** # ****************************************************************************** # ****************************************************************************** # ***************** HELPER FUNCTIONS START ********************************* # Function to URL encode special characters in a string urlencode() { local string="$1" local strlen=${#string} local encoded="" local pos c o for (( pos=0 ; pos/dev/null 2>&1 } # install package based on platform install_package() { if command_exists yum; then sudo yum install -y "$1" elif command_exists apt; then sudo apt install -y "$1" elif command_exists brew; then brew install "$1" else echo "Package manager not found. Please install $1 manually." fi } # Function to check if sudo is required for Docker command check_for_docker_sudo() { if docker ps >/dev/null 2>&1; then echo "n" else echo "y" fi } # Function to read a number from the user read_number() { local number read -rp "$1" number # Ensure the input is a number or empty while ! [[ $number =~ ^[0-9]+$ ]] && [ -n "$number" ] ; do read -rp "Please enter a valid number: " number done echo "$number" } # Function to read a number within a range from the user read_number_range() { local number local min local max # Check if there are 3 arguments if [ "$#" -ne 3 ]; then number=$(read_number) min=$1 max=$2 else number=$(read_number "$1") min=$2 max=$3 fi # Ensure the input is in the specified range while [[ -n "$number" && ($number -lt $min || $number -gt $max) ]]; do number=$(read_number "Please enter a number between $min and $max: ") done echo "$number" } check_if_docker_is_running() { if ! $DOCKER_COMMAND ps >/dev/null 2>&1; then echo "+-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-+" echo -e "| ${BOLD}${YELLOW}Warning ! ${NC} |" echo "| Docker is not running. Most of the commands will not work without Docker. |" echo "| Use the following command to start Docker: |" echo -e "| ${BLUE} sudo systemctl start docker ${NC} |" echo "+-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-+" fi } # ***************** HELPER FUNCTIONS END *********************************** # ****************************************************************************** # ****************************************************************************** # ***************** Existing Install Test ************************************ IS_DOCKER_REQUIRE_SUDO=$(check_for_docker_sudo) DOCKER_COMMAND=$([ "$IS_DOCKER_REQUIRE_SUDO" = "y" ] && echo "sudo docker" || echo "docker") NOCO_FOUND=false # Check if $NOCO_HOME exists as directory or symbolic link if [ -d "$NOCO_HOME" ] || [ -L "$NOCO_HOME" ]; then NOCO_FOUND=true elif $DOCKER_COMMAND ps -a --format '{{.Names}}' | grep -q nocodb; then echo "Found NocoDB running, but was unable to auto locate the installation directory." echo "Please provide the path to the NocoDB installation directory: " read -r CUSATOM_NOCO_HOME if [ -d "$CUSATOM_NOCO_HOME" ]; then echo "NocoDB installation directory is required." exit 1 fi ln -s "$CUSATOM_NOCO_HOME" "$NOCO_HOME" NOCO_FOUND=true fi cd "$NOCO_HOME" || exit # Check if nocodb is already installed if [ "$NOCO_FOUND" = true ]; then echo "NocoDB is already installed. And running." echo "Do you want to reinstall NocoDB? [Y/N] (default: N): " read -r REINSTALL if [ "$REINSTALL" != "Y" ] && [ "$REINSTALL" != "y" ]; then management_menu() exit 0 else echo "Reinstalling NocoDB..." $DOCKER_COMMAND compose down fi fi # ****************************************************************************** # ******************** SYSTEM REQUIREMENTS CHECK START ************************* # Check if the following requirements are met: # a. docker, jq installed # b. port mapping check : 80,443 are free or being used by nginx container REQUIRED_PORTS=(80 443) echo "** Performing nocodb system check and setup. This step may require sudo permissions" # pre install wget if not found if ! command_exists wget; then echo "wget is not installed. Setting up for installation..." install_package wget fi # d. Check if required tools are installed echo " | Checking if required tools (docker, lsof) are installed..." for tool in docker lsof openssl; do if ! command_exists "$tool"; then echo "$tool is not installed. Setting up for installation..." if [ "$tool" = "docker" ]; then wget -qO- https://get.docker.com/ | sh elif [ "$tool" = "lsof" ]; then install_package lsof fi fi done # e. Check if NocoDB is already installed and its expected version # echo "Checking if NocoDB is already installed and its expected version..." # Replace the following command with the actual command to check NocoDB installation and version # Example: nocodb_version=$(command_to_get_nocodb_version) # echo "NocoDB version: $nocodb_install_version" # f. Port mapping check echo " | Checking port accessibility..." for port in "${REQUIRED_PORTS[@]}"; do if lsof -Pi :$port -sTCP:LISTEN -t >/dev/null; then echo " | WARNING: Port $port is in use. Please make sure it is free." >&2 else echo " | Port $port is free." fi done echo "** System check completed successfully. **" # Define an array to store the messages to be printed at the end message_arr=() # extract public ip address PUBLIC_IP=$(dig +short myip.opendns.com @resolver1.opendns.com) # Check if the public IP address is not empty, if empty then use the localhost if [ -z "$PUBLIC_IP" ]; then PUBLIC_IP="localhost" fi # generate a folder for the docker-compose file which is not existing and do the setup within the folder # Define the folder name FOLDER_NAME="nocodb_$(date +"%Y%m%d_%H%M%S")" # prompt for custom folder name and if left empty skip #echo "Enter a custom folder name or press Enter to use the default folder name ($FOLDER_NAME): " #read CUSTOM_FOLDER_NAME message_arr+=("Setup folder: $FOLDER_NAME") if [ -n "$CUSTOM_FOLDER_NAME" ]; then FOLDER_NAME="$CUSTOM_FOLDER_NAME" fi # ******************** SYSTEM REQUIREMENTS CHECK END ************************** # ****************************************************************************** # ******************** INPUTS FROM USER START ******************************** # ****************************************************************************** echo "Enter the IP address or domain name for the NocoDB instance (default: $PUBLIC_IP): " read -r DOMAIN_NAME echo "Show Advanced Options [Y/N] (default: N): " read -r ADVANCED_OPTIONS if [ "$ADVANCED_OPTIONS" == "Y" ]; then ADVANCED_OPTIONS="y" fi if [ -n "$DOMAIN_NAME" ]; then if [ "$ADVANCED_OPTIONS" == "y" ]; then echo "Do you want to configure SSL [Y/N] (default: N): " read -r SSL_ENABLED message_arr+=("SSL: ${SSL_ENABLED}") fi else DOMAIN_NAME="$PUBLIC_IP" fi message_arr+=("Domain: $PUBLIC_IP") if [ "$ADVANCED_OPTIONS" == "y" ]; then echo "Choose Community or Enterprise Edition [CE/EE] (default: CE): " read -r EDITION fi if [ -n "$EDITION" ] && { [ "$EDITION" = "EE" ] || [ "$EDITION" = "ee" ]; }; then echo "Enter the NocoDB license key: " read LICENSE_KEY if [ -z "$LICENSE_KEY" ]; then echo "License key is required for Enterprise Edition installation" exit 1 fi fi if [ "$ADVANCED_OPTIONS" == "y" ]; then echo "Do you want to enabled Redis for caching [Y/N] (default: Y): " read -r REDIS_ENABLED fi if [ -z "$REDIS_ENABLED" ] || { [ "$REDIS_ENABLED" != "N" ] && [ "$REDIS_ENABLED" != "n" ]; }; then message_arr+=("Redis: Enabled") else message_arr+=("Redis: Disabled") fi if [ "$ADVANCED_OPTIONS" == "y" ]; then echo "Do you want to enabled Watchtower for automatic updates [Y/N] (default: Y): " read -r WATCHTOWER_ENABLED fi if [ -z "$WATCHTOWER_ENABLED" ] || { [ "$WATCHTOWER_ENABLED" != "N" ] && [ "$WATCHTOWER_ENABLED" != "n" ]; }; then message_arr+=("Watchtower: Enabled") else message_arr+=("Watchtower: Disabled") fi if [ "$ADVANCED_OPTIONS" = "y" ] ; then NUM_CORES=$(nproc || sysctl -n hw.ncpu || echo 1) echo "How many instances of NocoDB do you want to run (Maximum: ${NUM_CORES}) ? (default: 1): " NUM_INSTANCES=$(read_number_range 1 "$NUM_CORES") fi if [ -z "$NUM_INSTANCES" ]; then NUM_INSTANCES=1 fi message_arr+=("Number of instances: $NUM_INSTANCES") # ****************************************************************************** # *********************** INPUTS FROM USER END ******************************** # ****************************************************************************** # *************************** SETUP START ************************************* # Generate a strong random password for PostgreSQL STRONG_PASSWORD=$(openssl rand -base64 48 | tr -dc 'a-zA-Z0-9!@#$%^&*()-_+=' | head -c 32) REDIS_PASSWORD=$(openssl rand -base64 48 | tr -dc 'a-zA-Z0-9' | head -c 24) # Encode special characters in the password for JDBC URL usage ENCODED_PASSWORD=$(urlencode "$STRONG_PASSWORD") IMAGE="nocodb/nocodb:latest"; # Determine the Docker image to use based on the edition if [ -n "$EDITION" ] && { [ "$EDITION" = "EE" ] || [ "$EDITION" = "ee" ]; }; then IMAGE="nocodb/nocodb-ee:latest" DATABASE_URL="DATABASE_URL=postgres://postgres:${ENCODED_PASSWORD}@db:5432/nocodb" else # use NC_DB url until the issue with DATABASE_URL is resolved(encoding) DATABASE_URL="NC_DB=pg://db:5432?d=nocodb&user=postgres&password=${ENCODED_PASSWORD}" fi message_arr+=("Docker image: $IMAGE") DEPENDS_ON="" # Add Redis service if enabled if [ -z "$REDIS_ENABLED" ] || { [ "$REDIS_ENABLED" != "N" ] && [ "$REDIS_ENABLED" != "n" ]; }; then DEPENDS_ON="- redis" fi # Write the Docker Compose file with the updated password cat < docker-compose.yml services: nocodb: image: ${IMAGE} env_file: docker.env deploy: mode: replicated replicas: ${NUM_INSTANCES} depends_on: - db ${DEPENDS_ON} restart: unless-stopped volumes: - ./nocodb:/usr/app/data labels: - "com.centurylinklabs.watchtower.enable=true" networks: - nocodb-network db: image: postgres:16.1 env_file: docker.env volumes: - ./postgres:/var/lib/postgresql/data restart: unless-stopped healthcheck: interval: 10s retries: 10 test: "pg_isready -U \"\$\$POSTGRES_USER\" -d \"\$\$POSTGRES_DB\"" timeout: 2s networks: - nocodb-network nginx: image: nginx:latest volumes: - ./nginx:/etc/nginx/conf.d EOF if [ "$SSL_ENABLED" = 'y' ] || [ "$SSL_ENABLED" = 'Y' ]; then cat <> docker-compose.yml - webroot:/var/www/certbot - ./letsencrypt:/etc/letsencrypt - letsencrypt-lib:/var/lib/letsencrypt EOF fi cat <> docker-compose.yml ports: - "80:80" - "443:443" depends_on: - nocodb restart: unless-stopped networks: - nocodb-network EOF if [ "$SSL_ENABLED" = 'y' ] || [ "$SSL_ENABLED" = 'Y' ]; then cat <> docker-compose.yml certbot: image: certbot/certbot volumes: - ./letsencrypt:/etc/letsencrypt - letsencrypt-lib:/var/lib/letsencrypt - webroot:/var/www/certbot entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait \$\${!}; done;'" depends_on: - nginx restart: unless-stopped networks: - nocodb-network EOF fi if [ -z "$REDIS_ENABLED" ] || { [ "$REDIS_ENABLED" != "N" ] && [ "$REDIS_ENABLED" != "n" ]; }; then cat <> docker-compose.yml redis: image: redis:latest restart: unless-stopped env_file: docker.env command: - /bin/sh - -c - redis-server --requirepass "\$\${REDIS_PASSWORD}" volumes: - redis:/data healthcheck: test: [ "CMD", "redis-cli", "-a", "\$\${REDIS_PASSWORD}", "--raw", "incr", "ping" ] networks: - nocodb-network EOF fi if [ -z "$WATCHTOWER_ENABLED" ] || { [ "$WATCHTOWER_ENABLED" != "N" ] && [ "$WATCHTOWER_ENABLED" != "n" ]; }; then cat <> docker-compose.yml watchtower: image: containrrr/watchtower volumes: - /var/run/docker.sock:/var/run/docker.sock command: --schedule "0 2 * * 6" --cleanup restart: unless-stopped networks: - nocodb-network EOF fi if [ "$SSL_ENABLED" = 'y' ] || [ "$SSL_ENABLED" = 'Y' ]; then cat <> docker-compose.yml volumes: letsencrypt-lib: webroot: EOF fi # add the cache volume if [ -z "$REDIS_ENABLED" ] || { [ "$REDIS_ENABLED" != "N" ] && [ "$REDIS_ENABLED" != "n" ]; }; then # check ssl enabled if [ "$SSL_ENABLED" = 'y' ] || [ "$SSL_ENABLED" = 'Y' ]; then cat <> docker-compose.yml redis: EOF else cat <> docker-compose.yml volumes: redis: EOF fi fi # Create the network cat <> docker-compose.yml networks: nocodb-network: driver: bridge EOF # Write the docker.env file cat < docker.env POSTGRES_DB=nocodb POSTGRES_USER=postgres POSTGRES_PASSWORD=${STRONG_PASSWORD} $DATABASE_URL NC_LICENSE_KEY=${LICENSE_KEY} EOF # add redis env if enabled if [ -z "$REDIS_ENABLED" ] || { [ "$REDIS_ENABLED" != "N" ] && [ "$REDIS_ENABLED" != "n" ]; }; then cat <> docker.env REDIS_PASSWORD=${REDIS_PASSWORD} NC_REDIS_URL=redis://:${REDIS_PASSWORD}@redis:6379/0 EOF fi mkdir -p ./nginx # Create nginx config with the provided domain name cat > ./nginx/default.conf <> ./nginx/default.conf <> ./nginx/default.conf <> ./nginx/default.conf <> ./nginx/default.conf < ./nginx-post-config/default.conf < ./update.sh <