Using (Fast)CGI for non-PHP websites: Difference between revisions

From ULYSSIS documentation
 
(49 intermediate revisions by 6 users not shown)
Line 1: Line 1:
You're not stuck with PHP if you want to make a dynamic website. You can use all sorts of frameworks and programming languages with ULYSSIS, as long as it has CGI or FastCGI support.
You're not stuck with PHP if you want to make a dynamic website. You can use all sorts of frameworks and programming languages with ULYSSIS, as long as it has CGI or FastCGI support.


If you want to use a certain programming language or framework, and you can't get it to work, don't hesitate to contact us at [mailto:ulyssis@ulyssis.org ulyssis@ulyssis.org].
If you want to use a certain programming language or framework, and you can't get it to work, don't hesitate to contact us at [mailto:ulyssis@ulyssis.org ulyssis@ulyssis.org]. We will of course not write any code for you, but we can give you some pointers or directions.


==CGI==
==CGI==


CGI is a very easy and simple way to create a dynamic website. We use Apache's [http://httpd.apache.org/docs/2.4/mod/mod_cgid.html mod_cgid] to provide support for CGI.
CGI is a simple way to create a dynamic website. We use Apache's [http://httpd.apache.org/docs/2.4/mod/mod_cgid.html mod_cgid] to provide support for CGI. Note, however, that CGI is inefficient and
in general we recommend you to use a framework together with FastCGI. There are many frameworks, ranging from very simple and lightweight, to more complex and feature rich.
 
If, for example, you want Apache to interpret all files with the extension <code>.cgi</code> as CGI scripts, use the following <tt>.htaccess</tt> file (to be placed in your <code>www</code> folder):
<pre>Options +ExecCGI
AddHandler cgi-script .cgi</pre>
 
Here's an example of a very simple Python CGI script (called <code>hello.cgi</code>):
<pre>#!/usr/bin/env python3
 
print('Content-Type: text/plain\n')
print('Hello world!')</pre>
 
CGI scripts need to be executable, otherwise they won't work:
<pre>chmod +x hello.cgi</pre>


==FastCGI==
==FastCGI==
Line 11: Line 25:
We use Apache's [http://httpd.apache.org/mod_fcgid/mod/mod_fcgid.html mod_fcgid] to provide support for FastCGI.
We use Apache's [http://httpd.apache.org/mod_fcgid/mod/mod_fcgid.html mod_fcgid] to provide support for FastCGI.


In order to use FastCGI, you will generally need a starter script specific to your framework and application. Just do a web search for "''framework'' mod_fcgid", where you ''framework'' is the framework you're using, and you will find good instructions. There's a Django example below.
In order to use FastCGI, you will generally need a starter script specific to your programming languae, framework or application. Just do a web search for "''keyword'' mod_fcgid", where ''keyword'' is the programming language, framework or application you're planning to use, and you will often find good instructions. We've included some examples below


You'll want to treat this starter script as a <tt>mod_fcgid</tt> script, and usually, you'll want to redirect everything to this script, so you can use an <tt>.htaccess</tt> file that looks like this:
You'll want to treat this starter script as a <code>mod_fcgid</code> script, and usually, you'll want to redirect everything to this script, so you can use an <code>.htaccess</code> file that looks like this:
<pre>Options +ExecCGI
<syntaxhighlight lang="apache">Options +ExecCGI
AddHandler fcgid-script .fcgi
AddHandler fcgid-script .fcgi
RewriteEngine On
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ starter.fcgi/$1 [QSA,L]</pre>
RewriteRule ^(.*)$ starter.fcgi/$1 [QSA,L]</syntaxhighlight>
Replace ''starter.fcgi'' with the name of your starter script.
Replace ''starter.fcgi'' with the name of your starter script. Don't forget to make your starter script executable:
<pre>chmod +x starter.fcgi</pre>
The starter script must be located in the same folder as the <code>.htaccess</code> file.
 
{{warning|Warning!|Do not place sensitive files inside the web accessible folder. Make sure they are located in a folder outside the document root, or they will be downloadable through the webserver.}}
It is adviced to only place the fcgi starter script, <code>.htaccess</code> and static files (images, stylesheets) inside the document root.
 
=== Startup time and persistency ===
 
It is important to keep in mind that our Apache webworkers will only run your start script when a request is made. As soon as that first request has been received, it will try and keep a few instances of your application around for future use. This of course affects how you restart your application after changes (see below), but also means that applications with a longer start up time (for example a language such as Python combined with a larger framework) will experience an initial slow request. This slow startup is obviously not a factor for faster languages such as Haskell, C++ or Go.
 
Sadly, Apache is unable to have FastCGI processes persist beyond a reload or restart. This means in practice that every night around 6h25 when the logs are rotated, which requires Apache to be reloaded, all running fcgi processes will be gracefully terminated. The same thing happens when we make changes to the Apache configuration when a new website or user is added.
 
For those who have a very high startup time and don't generate enough traffic to not experience issues with this behaviour, it's possible to use a [[Managing_Cron_jobs | cronjob]] to visit the website every night at 6h30 or 7h, or even hourly.


===Restarting your application===
===Restarting your application===


If you've changed your application, you will need to restart it for these changes to take effect. In order to do that, you have to set the modification date of the starter script to a more recent date:
If you've changed your application, you will need to restart it for these changes to take effect. However, because you're editing the application on a different server than where it is running, you do not have access to the running process. You'll have to create some sort of mechanism to restart the process.
 
In the Django and Go examples below, the starter script has been written so that when it is changed, the application is automatically restarted within 1 second. The same mechanism can be applied to other Python sites. You can change the modification date without actually changing the contents of the starter script by using <code>touch</code>:
<pre>touch starter.fcgi</pre>
<pre>touch starter.fcgi</pre>
Replace ''starter.fcgi'' with the name of your starter script.
Replace <code>starter.fcgi</code> with the name of your starter script.
 
===Example: Django===


If you want to make a website using [https://www.djangoproject.com/ Django], we recommend that you use a [http://virtualenv.readthedocs.org/ virtualenv]. This tool is already preinstalled on our shell servers.
=== Examples ===
==== Python and Django ====


You can use the following steps to get up and running with Django at ULYSSIS:
If you want to make a website using Python and [https://www.djangoproject.com/ Django], we recommend that you use a virtual environment. You can use the following steps to get up and running with Django on your ULYSSIS hosting account.


<ol>
<ol>
<li>[[Accessing shell servers over SSH|Log in to a shell server]]</li>
<li>[[Accessing shell servers over SSH|Log in to a shell server]]</li>
<li>Create a new virtualenv, you can create one in <tt>~/.venv</tt>, for example:
<li>Create a new virtualenv, you can create one in <code>~/.venv</code>, for example:
<pre>virtualenv ~/.venv</pre>
<pre>python3 -m venv ~/.venv</pre>
</li>
</li>
<li>Activate this virtualenv:
<li>Activate this virtualenv:
Line 42: Line 70:
</li>
</li>
<li>
<li>
Install Django inside of this virtualenv:
Install Django and flup for FastCGI inside of this virtualenv:
<pre>pip install django</pre>
<pre>pip install django flup6</pre>
</li>
</li>
<li>
<li>
Create a new Django project:
Create a new Django project:
<pre>django-admin.py startproject mysite</pre>
<pre>django-admin startproject mysite</pre>
Replace ''mysite'' with your project's desired name.
Replace ''mysite'' with your project's desired name.
</li>
</li>
<li>
<li>
Install flup for FastCGI:
Edit the allowed hosts in your Django project's settings, in <code>mysite/mysite/settings.py</code>. Look for the line <code>ALLOWED_HOSTS = []</code> and replace it with:
<pre>pip install flup</pre>
<pre>
ALLOWED_HOSTS = [ 'username.ulyssis.be', 'username.studentenweb.org' ]
</pre>
Use your ULYSSIS username in place of ''username''.
</li>
</li>
<li>
<li>
Put an <tt>.htaccess</tt> file in your site's directory, e.g. the <tt>www</tt> folder:
Put an <code>.htaccess</code> file in your site's directory, e.g. the <code>www</code> folder:
<pre>Options +ExecCGI
<pre>Options +ExecCGI
AddHandler fcgid-script .fcgi
AddHandler fcgid-script .fcgi
Line 63: Line 94:
</li>
</li>
<li>
<li>
Create the starter script (replace ''user'' with org if you are an organization, and ''username'' with your ULYSSIS username):
Create the starter script (replace <code>user</code> with org if you are an organization, and <code>username</code> with your ULYSSIS username):
<pre>#!/home/user/username/.venv/bin/python
<syntaxhighlight lang="python">#!/home/user/username/.venv/bin/python


import sys, os
import sys, os, os.path
from threading import Thread


sys.path.insert(0, '/home/user/username/mysite')
this_file = os.path.realpath(__file__)


os.chdir('/home/user/username/mysite')
site_dir = '/home/user/username/mysite'
sys.path.insert(0, site_dir)
os.chdir(site_dir)


os.environ['DJANGO_SETTINGS_MODULE'] = 'mysite.settings'
os.environ['DJANGO_SETTINGS_MODULE'] = 'mysite.settings'


from django.core.servers.fastcgi import runfastcgi
def stat_thread():
runfastcgi(method="threaded", daemonize="false")</pre>
    import time, os, signal
Call this script <tt>mysite.fcgi</tt>.
    start_mtime = os.stat(this_file).st_mtime
    while True:
        cur_mtime = os.stat(this_file).st_mtime
        if cur_mtime != start_mtime:
            os.kill(os.getpid(), signal.SIGTERM)
        time.sleep(1)
 
Thread(target=stat_thread).start()
 
from django.core.servers.basehttp import get_internal_wsgi_application
from flup.server.fcgi import WSGIServer
WSGIServer(get_internal_wsgi_application(), debug=False).run()
</syntaxhighlight>
Call this script <code>mysite.fcgi</code>.
</li>
</li>
<li>
<li>
Line 88: Line 135:


This guide was based on, and you can find more information in the official Django documentation:
This guide was based on, and you can find more information in the official Django documentation:
* [https://docs.djangoproject.com/en/1.6/intro/tutorial01/ The Django tutorial]
* [https://docs.djangoproject.com/en/stable/intro/tutorial01/ The Django tutorial]
* [https://docs.djangoproject.com/en/1.6/howto/deployment/fastcgi/#apache-shared-hosting Running Django on a shared-hosting provider with Apache]
* [https://flask.palletsprojects.com/en/1.1.x/deploying/fastcgi/ Flask documentation for FastCGI]
 
==== Go ====
To make a website or web application in Go and host it on your ULYSSIS hosting account, you can use the standard library ''fcgi'' package.
 
<ol>
<li> [[Accessing shell servers over SSH|Log in to a shell server]]</li>
<li> Put an <code>.htaccess</code> file in your site's directory (e.g. the <code>www</code> folder) with a reference to your executable. Here we've chosen <code>mysite.fcgi</code>, but you can adapt this to your liking.
<pre>Options +ExecCGI
AddHandler fcgid-script .fcgi
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ mysite.fcgi/$1 [QSA,L]</pre>
</li>
<li> Write your code, and compile the binary to <code>mysite.fcgi</code>. Below you can find a basic example of how to register two handlers (of which one is the fallback), then start the FastCGI client, and finally have a function (which is called as a goroutine) to check if the binary has been replaced so old processes don't stick around.
<syntaxhighlight lang="go">package main
 
import (
"fmt"
"net/http"
"net/http/fcgi"
"os"
"path/filepath"
"time"
)
 
func uri_interesting(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "<h1>Oh, seems like this link isn't that interesting after all</h1>")
fmt.Fprintln(w, "<p>Back to the <a href=\"/\">Homepage</a></p>")
}
 
func uri_default(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
w.WriteHeader(http.StatusNotFound)
fmt.Fprintln(w, "<h1>This page doesn't exist</h1>")
fmt.Fprintln(w, "<p>Back to the <a href=\"/\">Homepage</a></p>")
} else {
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "<h1>Welcome to this testwebsite!</h1>")
fmt.Fprintln(w, "<p>This website is written in Go but doesn't really do much.")
fmt.Fprintln(w, "Feel free to visit this <a href=\"/interesting/link\">Interesting link</a> or this <a href=\"/non-existent/link\">non-existent link</a>.</p>")
}
}
 
func main() {
go check_selfreplacement()
http.HandleFunc("/interesting/link", uri_interesting)
http.HandleFunc("/", uri_default)
if err := fcgi.Serve(nil, nil); err != nil {
panic(err)
}
}
 
func check_selfreplacement() {
fcgi_location, _ := os.Executable()
fcgi_location, _ = filepath.EvalSymlinks(fcgi_location)
start_stat, _ := os.Stat(fcgi_location)
for {
current_stat, _ := os.Stat(fcgi_location)
if start_stat.ModTime() != current_stat.ModTime() {
os.Exit(0)
}
time.Sleep(1 * time.Second)
}
}
</syntaxhighlight>
If you want to give this example a try, save it as <code>mysite.fcgi.go</code> and then use <code>go build mysite.fcgi.go</code> to compile it.
</li>
<li>Visit your website to verify everything works</li>
</ol>
 
[[Category:Webserver]]

Latest revision as of 23:53, 14 April 2024

You're not stuck with PHP if you want to make a dynamic website. You can use all sorts of frameworks and programming languages with ULYSSIS, as long as it has CGI or FastCGI support.

If you want to use a certain programming language or framework, and you can't get it to work, don't hesitate to contact us at ulyssis@ulyssis.org. We will of course not write any code for you, but we can give you some pointers or directions.

CGI

CGI is a simple way to create a dynamic website. We use Apache's mod_cgid to provide support for CGI. Note, however, that CGI is inefficient and in general we recommend you to use a framework together with FastCGI. There are many frameworks, ranging from very simple and lightweight, to more complex and feature rich.

If, for example, you want Apache to interpret all files with the extension .cgi as CGI scripts, use the following .htaccess file (to be placed in your www folder):

Options +ExecCGI
AddHandler cgi-script .cgi

Here's an example of a very simple Python CGI script (called hello.cgi):

#!/usr/bin/env python3

print('Content-Type: text/plain\n')
print('Hello world!')

CGI scripts need to be executable, otherwise they won't work:

chmod +x hello.cgi

FastCGI

We use Apache's mod_fcgid to provide support for FastCGI.

In order to use FastCGI, you will generally need a starter script specific to your programming languae, framework or application. Just do a web search for "keyword mod_fcgid", where keyword is the programming language, framework or application you're planning to use, and you will often find good instructions. We've included some examples below

You'll want to treat this starter script as a mod_fcgid script, and usually, you'll want to redirect everything to this script, so you can use an .htaccess file that looks like this:

Options +ExecCGI
AddHandler fcgid-script .fcgi
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ starter.fcgi/$1 [QSA,L]

Replace starter.fcgi with the name of your starter script. Don't forget to make your starter script executable:

chmod +x starter.fcgi

The starter script must be located in the same folder as the .htaccess file.

WarningWarning!

Do not place sensitive files inside the web accessible folder. Make sure they are located in a folder outside the document root, or they will be downloadable through the webserver.


It is adviced to only place the fcgi starter script, .htaccess and static files (images, stylesheets) inside the document root.

Startup time and persistency

It is important to keep in mind that our Apache webworkers will only run your start script when a request is made. As soon as that first request has been received, it will try and keep a few instances of your application around for future use. This of course affects how you restart your application after changes (see below), but also means that applications with a longer start up time (for example a language such as Python combined with a larger framework) will experience an initial slow request. This slow startup is obviously not a factor for faster languages such as Haskell, C++ or Go.

Sadly, Apache is unable to have FastCGI processes persist beyond a reload or restart. This means in practice that every night around 6h25 when the logs are rotated, which requires Apache to be reloaded, all running fcgi processes will be gracefully terminated. The same thing happens when we make changes to the Apache configuration when a new website or user is added.

For those who have a very high startup time and don't generate enough traffic to not experience issues with this behaviour, it's possible to use a cronjob to visit the website every night at 6h30 or 7h, or even hourly.

Restarting your application

If you've changed your application, you will need to restart it for these changes to take effect. However, because you're editing the application on a different server than where it is running, you do not have access to the running process. You'll have to create some sort of mechanism to restart the process.

In the Django and Go examples below, the starter script has been written so that when it is changed, the application is automatically restarted within 1 second. The same mechanism can be applied to other Python sites. You can change the modification date without actually changing the contents of the starter script by using touch:

touch starter.fcgi

Replace starter.fcgi with the name of your starter script.

Examples

Python and Django

If you want to make a website using Python and Django, we recommend that you use a virtual environment. You can use the following steps to get up and running with Django on your ULYSSIS hosting account.

  1. Log in to a shell server
  2. Create a new virtualenv, you can create one in ~/.venv, for example:
    python3 -m venv ~/.venv
  3. Activate this virtualenv:
    . ~/.venv/bin/activate
  4. Install Django and flup for FastCGI inside of this virtualenv:
    pip install django flup6
  5. Create a new Django project:
    django-admin startproject mysite

    Replace mysite with your project's desired name.

  6. Edit the allowed hosts in your Django project's settings, in mysite/mysite/settings.py. Look for the line ALLOWED_HOSTS = [] and replace it with:
    ALLOWED_HOSTS = [ 'username.ulyssis.be', 'username.studentenweb.org' ]
    

    Use your ULYSSIS username in place of username.

  7. Put an .htaccess file in your site's directory, e.g. the www folder:
    Options +ExecCGI
    AddHandler fcgid-script .fcgi
    RewriteEngine On
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^(.*)$ mysite.fcgi/$1 [QSA,L]
  8. Create the starter script (replace user with org if you are an organization, and username with your ULYSSIS username):
    #!/home/user/username/.venv/bin/python
    
    import sys, os, os.path
    from threading import Thread
    
    this_file = os.path.realpath(__file__)
    
    site_dir = '/home/user/username/mysite'
    sys.path.insert(0, site_dir)
    os.chdir(site_dir)
    
    os.environ['DJANGO_SETTINGS_MODULE'] = 'mysite.settings'
    
    def stat_thread():
        import time, os, signal
        start_mtime = os.stat(this_file).st_mtime
        while True:
            cur_mtime = os.stat(this_file).st_mtime
            if cur_mtime != start_mtime:
                os.kill(os.getpid(), signal.SIGTERM)
            time.sleep(1)
    
    Thread(target=stat_thread).start()
    
    from django.core.servers.basehttp import get_internal_wsgi_application
    from flup.server.fcgi import WSGIServer
    WSGIServer(get_internal_wsgi_application(), debug=False).run()
    

    Call this script mysite.fcgi.

  9. Make the starter script executable:
    chmod +x mysite.fcgi
  10. Go to username.ulyssis.be (or whatever URL you chose to use), and it should show you the default "Welcome to Django" page.

This guide was based on, and you can find more information in the official Django documentation:

Go

To make a website or web application in Go and host it on your ULYSSIS hosting account, you can use the standard library fcgi package.

  1. Log in to a shell server
  2. Put an .htaccess file in your site's directory (e.g. the www folder) with a reference to your executable. Here we've chosen mysite.fcgi, but you can adapt this to your liking.
    Options +ExecCGI
    AddHandler fcgid-script .fcgi
    RewriteEngine On
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^(.*)$ mysite.fcgi/$1 [QSA,L]
  3. Write your code, and compile the binary to mysite.fcgi. Below you can find a basic example of how to register two handlers (of which one is the fallback), then start the FastCGI client, and finally have a function (which is called as a goroutine) to check if the binary has been replaced so old processes don't stick around.
    package main
    
    import (
    	"fmt"
    	"net/http"
    	"net/http/fcgi"
    	"os"
    	"path/filepath"
    	"time"
    )
    
    func uri_interesting(w http.ResponseWriter, r *http.Request) {
    	w.WriteHeader(http.StatusOK)
    	fmt.Fprintln(w, "<h1>Oh, seems like this link isn't that interesting after all</h1>")
    	fmt.Fprintln(w, "<p>Back to the <a href=\"/\">Homepage</a></p>")
    }
    
    func uri_default(w http.ResponseWriter, r *http.Request) {
    	if r.URL.Path != "/" {
    		w.WriteHeader(http.StatusNotFound)
    		fmt.Fprintln(w, "<h1>This page doesn't exist</h1>")
    		fmt.Fprintln(w, "<p>Back to the <a href=\"/\">Homepage</a></p>")
    	} else {
    		w.WriteHeader(http.StatusOK)
    		fmt.Fprintln(w, "<h1>Welcome to this testwebsite!</h1>")
    		fmt.Fprintln(w, "<p>This website is written in Go but doesn't really do much.")
    		fmt.Fprintln(w, "Feel free to visit this <a href=\"/interesting/link\">Interesting link</a> or this <a href=\"/non-existent/link\">non-existent link</a>.</p>")
    	}
    }
    
    func main() {
    	go check_selfreplacement()
    	http.HandleFunc("/interesting/link", uri_interesting)
    	http.HandleFunc("/", uri_default)
    	if err := fcgi.Serve(nil, nil); err != nil {
    		panic(err)
    	}
    }
    
    func check_selfreplacement() {
    	fcgi_location, _ := os.Executable()
    	fcgi_location, _ = filepath.EvalSymlinks(fcgi_location)
    	start_stat, _ := os.Stat(fcgi_location)
    	for {
    		current_stat, _ := os.Stat(fcgi_location)
    		if start_stat.ModTime() != current_stat.ModTime() {
    			os.Exit(0)
    		}
    		time.Sleep(1 * time.Second)
    	}
    }
    

    If you want to give this example a try, save it as mysite.fcgi.go and then use go build mysite.fcgi.go to compile it.

  4. Visit your website to verify everything works