Easily create, setup, enable and disable nginx VHOSTS

Automatically provision new nginx sites on Arch Linux or Ubuntu (Script with tutorial)

Easily create, setup, enable and disable nginx VHOSTS

So one thing I hate as a developer is re-configuring everything a hundred times every time I decide I want to launch a new side project or test app.

So to make life easy I developed a bash script that does everything for me.

What it does:

  1. Create nginx sites using a basic template, this one’s very common to laravel applications since that’s my default setup, but I’ve found it usually works for most things, if not you may need to edit a little.

    The create method does the following:

    1. First it checks to make sure $root is set, $root means you’ve provided a directory in the terminal.
      eg: {shell} $ ng create site.test /var/www/site.test/public {/shell}
    2. Then it checks if the file already exists in $sitesAvailable/$domain.
    3. Then it checks to make sure the root directory exists if it doesn’t you probably have a typo and it’ll let you know and exit.
    4. Then it outputs the nginx conf to the new vhost file in /etc/nginx/sites-available.
    5. Then if $useHosts is enabled it’ll also update /etc/hosts
    6. Then it sets the chown properly for the folder so that it works with nginx.
    7. Then it links the file in sites-enabled.
    8. Then it generates a wildcard self-signed ssl for all sites in the sites-available folder. This is reused for convenience, but can be disabled by setting $useSSL toggle to false.
    9. Then we restart nginx and god willing we’re good to go and you can visit yourdomain.test.
  2. Delete. This simply undoes everything that create did, it does NOT touch your files though, so if you want your project files deleted too you’ll need to {shell} $ rm -rf /path/to/project {/shell}.
  3. Enable. This will enable a domain in sites-available (setup a symbolic link). If no domain is provided it’ll enable ALL sites in sites-available.
  4. Disable. This will disable a domain in sites-available (unlink the symbolic link). You can choose to disable all or a single vhost.

    All: {shell}$ ng disable all {/shell}
    Single: {shell} $ng disable somesite.test {/shell}

I’m thinking eventually adding mysql/postgres support to create a dbuser locally as well, personally virtualbox/docker eat a lot of memory for me, and I’ve been working and had my pc freeze, being able to just use nginx/mysql natively for dev is just a personal preference.

It of course depends on the size and scope of the project, if I’m working on an enterprise level app with lots of moving parts like elasticsearch, beanstalkd, supervisor, crons, etc… I’ll definitely go with docker most likely simply so that I can test all parts as if it’s a production environment. For simple side project ideas/scratching an itch, this config works fine.

Hope this helps, let me know if you have thoughts/suggestions on the script.

You can download from here or view the entire source below:

#!/bin/bash
# MIT License

# Copyright (c) 2017 Patrick Curl | SoaringHost
# https://github.com/patrickcurl/ngTool

# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:

# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

### Set Language
TEXTDOMAIN=virtualhost

### Set default parameters
action=$1
domain=$2
root=$3

### CONFIG SETTINGS
# Put owner/group that nginx must run under for your system.
# The default on ubuntu would be user: www-data group: www-data.
# This is my personal settings for Arch linux.
owner=patrick #owner for chmoding -- this is the owner nginx uses.
group=users #group for chmoding -- this is the group nginx uses.
sitesEnabled='/etc/nginx/sites-enabled/' #sites-enabled path
sitesAvailable='/etc/nginx/sites-available/' #sites-available path
useHosts=false # make true if you do not have a global .test wildcard setup via dnsmasq or other.
useSSL=true
sslConfig='/etc/nginx/ssl/ssl_servers.cfg'
sslKey='/etc/nginx/ssl/nginx.key'
sslCert='/etc/nginx/ssl/nginx.crt'
# Change this to match what your system uses.
#arch
restart='systemctl restart nginx'
#ubuntu
# restart=service nginx restart
### END CONFIG SETTINGS

regenerate_ssls(){
	sitesAvailable = $1
	sslConfig = $2
	sslKey = $3
	sslCert = $4
    domains=`sed -n -e 's/^.*server_name //p' $sitesAvailable* | tr " " "\n" | sed '/^\s*$/d' | cut -d ';' -f1 | sort -u`
    cat <<EOT> $sslConfig
[ req ]
req_extensions     = req_ext
distinguished_name = req_distinguished_name
prompt             = no

[req_distinguished_name]
commonName=localhost.test

[req_ext]
subjectAltName   = @alt_names

[alt_names]
EOT
    count=1;
    for domain in $domains; do
        if [ "$domain" != '_' ]
        then
            let count++;
            dns="DNS.$count = $domain";
            echo $dns >> $sslConfig;
        fi
    done
    sudo openssl req -x509 -config $sslConfig -extensions req_ext -nodes -days 730 -newkey rsa:2048 -sha256 -keyout $sslKey -out $sslCert
}

function restart_nginx(){
	$restart
	echo -e $"Nginx restarted!"
}

function version { echo "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }'; }

function check_version(){
	echo -e "Checking for updates.\n";
	CURRENT_VERSION=`cat VERSION`
	echo -e $"$CURRENT_VERSION"
	NEW_VERSION="`wget -qO- https://raw.githubusercontent.com/patrickcurl/ngtool/master/VERSION`"
	if [ $(version $CURRENT_VERSION) -lt $(version $NEW_VERSION) ]; then
		echo -e $"There is an update available (Current: $CURRENT_VERSION | New: $NEW_VERSION) to update do the following:\n"
		SCRIPT="$(readlink --canonicalize-existing "$0")"
		SCRIPT_PATH="$(dirname $SCRIPT)"
		echo -e $"cd $SCRIPT_PATH && git pull origin master\n"
		#echo -e "There is an update available, please run: ng-self-update or cd to the directory you installed it to and run: git pull origin master"
	else
		echo -e $"You already have the latest version";
	fi
	# diff VERSION < (curl https://raw.githubusercontent.com/patrickcurl/ngTool/master/VERSION)
}



check_version

if [ "$(whoami)" != 'root' ]; then
	echo $"You have no permission to run $0 as non-root user. Use sudo"
		exit 1;
fi

if [ "$action" != "create" ] && [ "$action" != "delete" ] && [ "$action" != "enable" ] && [ "$action" != "disable" ]
	then
		echo $"You need to prompt for action (create, delete or enable or disable) -- Lower-case only"
		exit;
fi

while [ "$domain" == "" ]
do
	if [ "$action" == "create" ] ||  [ "$action" == "delete" ]; then
		echo -e $"Please provide domain. e.g. mysite.dev"
		read domain
	else
		break
	fi
done



### if root dir starts with '/', don't use /var/www as default starting point
if [ "$action" == "create" ]; then
	echo -e $"Creating $domain...\n"
	while [ "$root" == "" ]
	do
		echo -e $"Please provide a full directory e.g. /home/user/projects/myproject/public"
		read root
	done
	### check if domain already exists
	if [ -e $sitesAvailable$domain ]; then
		echo -e $"This domain already exists.\nPlease Try Another one"
		exit;
	fi

	### check if directory exists or not
	if ! [ -d $root ]; then
		### create the directory
		mkdir $root
		### give permission to root dir
		chmod 755 root
		### write test file in the new domain dir
		if ! echo "<?php echo phpinfo(); ?>" > $root/phpinfo.php
			then
				echo $"ERROR: Not able to write in file $root/phpinfo.php. Please check permissions."
				exit;
		else
				echo $"Added content to $root/phpinfo.php."
		fi
	fi

	### create virtual host rules file
	if ! echo "server {
		listen   80;
		listen [::]:80;
		listen 443 ssl http2;
		listen [::]:443 ssl http2;
		root $root;
		index index.php index.html index.htm;
		server_name $domain;

		ssl_certificate     $sslCert;
		ssl_certificate_key $sslKey;

		# serve static files directly
		location ~* \.(jpg|jpeg|gif|css|png|js|ico|html)\$ {
			access_log off;
			expires max;
		}


		# catch all
		error_page 404 /index.php;

		location / {

                try_files \$uri \$uri/ /index.php?\$query_string;

                # A bunch of perm page redirects from my old
                # site structure for SEO purposes. Not interesting.

                # include /etc/nginx/templates/redirects;

        }

		if (!-d \$request_filename) {
        	rewrite ^/(.+)/\$ /\$1 permanent;
	    }


	    location ~* \.php\$ {
	        try_files \$uri /index.php =404;
	        # Server PHP config.
	        fastcgi_pass                    unix:/run/php-fpm/php-fpm.sock;
	        fastcgi_index                   index.php;
	        fastcgi_split_path_info         ^(.+\.php)(/.+)\$;

	        # Typical vars in here, nothing interesting.

	        include                         /etc/nginx/fastcgi_params;
	        fastcgi_param                   SCRIPT_FILENAME \$document_root\$fastcgi_script_name;

	    }

	    location ~ /\.ht {

	            # Hells no, we usin nginx up in this mutha. (deny .htaccess)
	            deny all;

	    }

	}" > $sitesAvailable$domain
	then
		echo -e $"There is an ERROR create $domain file"
		exit;
	else
		echo -e $"\nNew Virtual Host Created\n"
	fi
	if [ "$useHosts" == true ]; then
		### Add domain in /etc/hosts
		if ! echo "127.0.0.1	$domain" >> /etc/hosts
			then
				echo $"ERROR: Not able write in /etc/hosts"
				exit;
		else
				echo -e $"Host added to /etc/hosts file \n"
		fi
	fi
	chown -R $owner:$group $root

	### enable website
	ln -s $sitesAvailable$domain $sitesEnabled$domain

	### SETUP SSLS for all existing domains.

	### restart Nginx
	if [ "$useSSL" == true ]; then
		regenerate_ssls $sitesAvailable $sslConfig $sslKey $sslCert
	fi

	restart_nginx

	### show the finished message
	echo -e $"Complete! \nYou now have a new Virtual Host \nYour new host is: http://$domain \nAnd its located at $root"
	exit;
fi

if [ "$action" == "delete" ]; then
	echo -e $"Deleting $domain...\n"
	### check whether domain already exists
	if ! [ -e $sitesAvailable$domain ]; then
		echo -e $"This domain does not exists.\nPlease Try Another one"
		exit;
	else
		if [ "$useHosts" == true ]; then
			### Delete domain in /etc/hosts
			newhost=${domain//./\\.}
			sed -i "/$newhost/d" /etc/hosts
		fi
		### disable website
		rm $sitesEnabled$domain

		### restart Nginx
		#service nginx restart
		restart_nginx
		### Delete virtual host rules files
		rm $sitesAvailable$domain
		echo -e $"Complete!\nYou just removed Virtual Host $domain. \nThe root folder still exists. You'll need to manually delete it."
		exit;
	fi
fi

if [ "$action" == "enable" ]; then
	if [ "$domain" == "" ]; then
		echo -e $"No domain supplied, re-enabling all domains";
		FILES=$sitesAvailable*
		for f in $FILES
		do
			file=`basename $f`;
			if ! [ -f "$sitesEnabled$file" ]; then
				# echo -e $"$file already linked, skipping."
			# else
		    	echo -e $"Enabling: $file"
		    	`sudo ln -s $sitesAvailable$file $sitesEnabled$file`
		    fi
		done
		echo -e $"Complete! Nginx sites are now enabled."
		exit;
	else
		echo -e $"Enabling $domain...\n"
		if ! [ -f "$sitesAvailable$domain" ]; then
			echo -e $"Domain file missing from $sitesAvailable, please add it then try again."
			exit;
		fi
		if [ -f "$sitesEnabled$domain" ]; then
			echo -e $"Domain is already enabled, no need to renable."
			exit;
		else
			`sudo ln -s $sitesAvailable$domain $sitesEnabled$domain`
			echo -e $"Domain $domain has been enabled. Happy coding!"
			restart_nginx
			exit;
		fi
	fi
fi

if [ "$action" == "disable" ]; then
	echo -e $"Disabling $domain...\n"
	if [ "$domain" == "" ]; then
		echo -e $"No domain supplied, disabling requires a domain! If you really want to disable ALL domains then try: ng disable all";
		exit;
	fi

	if [ "$domain" == "all" ]; then
		FILES=$sitesEnabled*
		for f in $FILES
		do
			file=`basename $f`
			if [ -f "$f" ]; then
				`rm -rf $f`
				if [ -f "$f" ]; then
					echo -e $"Error: $file was unable to be disabled"
				else
					echo -e $"$file as been disabled successfully."
					restart_nginx
				fi
			else
				echo -e $"File does not exist, skipping"
			fi
		done
	fi
	if [ "$domain" != "all" ] && [ "$domain" != "" ]; then
		file="$sitesEnabled$domain"
		if [ -f "$file" ]; then
			`rm -rf $file`
			if ! [ -f "$file" ]; then
				echo -e $"Site $domain has been disabled!"
				restart_nginx
			else
				echo -e $"Something went wrong, unable to disable site!"
			fi
		else
			echo -e $"File does not exist. Did you spell it right?"
		fi
	fi
fi