I very recently re-deployed the Trac application on my server. The first time I did this, I used the included tracd
server, but I had a very hard time configuring it on a non-root path with Nginx. So I decided to use Gunicorn, which is also reported to give better performance (is it true? I did not test).
The configuration is a front-end server running Nginx coupled with a back-end server running Gunicorn + Trac and the database which will be PostgreSQL. Static files will be deported at the end to the front-end server for performance. I will not go too much into details in the process of installing the components, as they were packaged for the distribution I use (ArchLinux) and my environment did not need a strong separation into a virtual environment. So these steps are rather easy and better explained on the official website.
Setting up the Trac environment
First of all, let’s setup the trac environment. We create a user on the back-end server that will run the Gunicorn + Trac services and get permission on the content. With this approach, it is easier to manage permissions and handle administration tasks on the projects. This user also needs an access to the database. The next step is to create the Trac project. We use the home directory as our workspace.
# useradd -m -s /usr/bin/false -U trac # This will create /home/trac # passwd -l trac # psql -U postgres postgres postgres=# CREATE USER trac; postgres=# CREATE DATABASE trac OWNER = trac; postgres=# \q # su -s /usr/bin/bash - trac $ mkdir repo # Note: now we are in /home/trac with the trac user $ trac-admin repo/PROJECT initenv Project Name [My Project]> ... Database connection string > postgres://trac@localhost/trac?schema=PROJECT $ trac-admin repo/PROJECT permission add admin TRAC_ADMIN
Configure Gunicorn
Now, we are ready to deploy. We need to expose the WSGI application of Trac to Gunicorn, which is basically trac.web.main.dispatch_request
. There is just a small tweak to do in the environment, as Gunicorn moves the REMOTE_USER
header to HTTP_REMOTE_USER
which Trac doesn’t expect. This is needed for authentication to work. So here is the script, to put into /home/trac/tracwsgi.py
or somewhere else you like.
import os import sys os.environ['TRAC_ENV'] = '/home/trac/repo/PROJECT' # Single Project # os.environ['TRAC_ENV_PARENT_DIR'] = '/home/trac/repo' # Multi Projects os.environ['PYTHON_EGG_CACHE'] = '/home/trac/eggcache' import trac.web.main def application(environ, start_response): environ['REMOTE_USER'] = environ.get('HTTP_REMOTE_USER') return trac.web.main.dispatch_request(environ, start_response)
Don’t forget then to create the appropriate egg cache directory:
$ mkdir eggcache
You may also choose to delete the line in the Python script if your eggs are already unpacked by your package manager when installed. Then, we must configure Gunicorn, for example in /home/trac/gunicorn.py
:
import os.path bind = "0.0.0.0:9000" # Change this to your liking ("127.0.0.1:8000" for example) chdir = os.path.dirname(os.path.realpath(__file__)) # Or the path to tracwsgi.py if different errorlog="-" loglevel="info" proc_name="trac-gunicorn" enable_stdio_inheritance=True forwarded_allow_ips = "x.x.x.x" # The IP of the front-end server if not localhost, needed for SSL
Finally, we need a Systemd service file (trac-gunicorn.service
):
[Unit] Description=Trac (GUnicorn) [Service] Type=forking User=trac Group=trac RuntimeDirectory=trac PIDFile=%t/trac/gunicorn.pid WorkingDirectory=/home/trac ExecStart=/usr/bin/gunicorn-python2 -p %t/trac/gunicorn.pid -D -c /home/trac/gunicorn.py tracwsgi:application ExecReload=/bin/kill -s HUP $MAINPID ExecStop=/bin/kill -s TERM $MAINPID PrivateTmp=true [Install] WantedBy=multi-user.target
Inside the project configuration (repo/PROJECT/conf/trac.ini
), you may want to change the option logging/log_type
to stderr
and adjust the log_level
so that Trac logs to journald. And we’re good to go for the backend.
# systemctl start trac-gunicorn # systemctl status -l trac-gunicorn # systemctl enable trac-gunicorn
Configure Nginx for Single Project
The configuration here is rather simple. It is assumed that Trac will be reachable from http[s]://www.example.com/prefix
:
http { ... server { ... location ^~ /prefix { # IP of the back-end proxy_pass http://x.x.x.x:9000; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_set_header SCRIPT_NAME /prefix; proxy_set_header REMOTE_USER $remote_user; proxy_set_header X-Forwarded-Proto $scheme; # To get authentication to work location ~ /prefix/login { auth_basic "Trac"; auth_basic_user_file /path/to/htpasswd; # This directive is not yet inherited (coming soon) proxy_pass http://x.x.x.x:9000; } # Placeholder for static files } } }
After reloading Nginx, you should be able to get into Trac with your browser! If you prefer to force your users to authenticate before accessing the site, remove the nested location
bloc (/prefix/login
) and place the auth
directives in the parent bloc (/prefix
). You can of course also use other kinds of authentication mechanisms that work with Nginx (I use LDAP).
Let’s now offload the static files to the front-end. On the back-end server, collect the files:
# su -s /usr/bin/bash - trac $ trac-admin repo/PROJECT deploy statics
Inside the created statics
folder, there is a directory called htdocs
, containing all the static files. This is the folder we need to transfer to the front-end (for example using tar
and scp
). Let’s say we want to have the statics in /www/assets
. The easiest way would be to place the htdocs
folder into /www/assets/prefix
and rename that folder to chrome
. That way, the folder structure will match the URLs (e.g. prefix/chrome/common/js/trac.js
). In case it is not possible (or you don’t want to) you’ll have to play with the rewrite action to make the URLs point to the right file path. Once done, we need to direct Nginx to catch every request to prefix/chrome
and return a local file. Add these lines inside the location ^~ /prefix
bloc where the placeholder comment shows:
location ~ /prefix/chrome { root /www/assets; expires 5d; }