Deployment

The following example documents one way to deploy Comics. As Comics is a standard Django project with an additional batch job for crawling, it may be deployed in just about any way a Django project may be deployed. Please refer to Django’s deployment documentation for further details.

In the following examples we assume that we are deploying Comics at http://comics.example.com/, using Nginx, Gunicorn, and PostgreSQL. The Django application and batch job is both running as the user comics-user. The static media files, like comic images, are served from http://comics.example.com/static/.

Database

Comics should theoretically work with any database supported by Django. Though, development is mostly done on SQLite and PostgreSQL. For production use, PostgreSQL is the recommended choice.

Note

If you are going to use SQLite in a deployment with Nginx and so on, you need to ensure that the user the web server will be running as has write access to the directory the SQLite database file is located in.

Example .env

In the following examples, we assume the Comics source code is unpacked at /srv/comics.example.com/app/comics.

To change settings, you should not change the settings files shipped with Comics, but instead override the settings using environment variables, or by creating a file named /srv/comics.example.com/app/comics/.env. You must at least set DJANGO_SECRET_KEY and database settings, unless you use SQLite.

A full set of environment variables for a production deployment may look like this:

DJANGO_SECRET_KEY=replace-this-with-a-long-random-value
DJANGO_ADMIN=comics@example.com
DJANGO_DEFAULT_FROM_EMAIL=comics@example.com

DJANGO_MEDIA_ROOT=/srv/comics.example.com/htdocs/static/media/
DJANGO_MEDIA_URL=https://comics.example.com/static/media/
DJANGO_STATIC_ROOT=/srv/comics.example.com/htdocs/static/
DJANGO_STATIC_URL=https://comics.example.com/static/

DATABASE_URL=postgres://comics:topsecret-password@localhost:5432/comics

CACHE_URL=memcache://127.0.0.1:11211

COMICS_LOG_FILENAME=/srv/comics.example.com/app/log/comics.log
COMICS_SITE_TITLE=comics.example.com
COMICS_INVITE_MODE=true

Of course, you should change most, if not all, of these settings to fit your own installation.

If your are not running a memcached server, remove CACHE_URL variable from your environment. Comics does not require a cache, but responses are significantly faster with a cache available.

Example Gunicorn setup

Comics is a WSGI app and can be run with any WSGI server, for example Gunicorn. Gunicorn is a Python program, so you can simply install it in Comics’ own virtualenv:

source /srv/comics.example.com/app/venv/bin/activate
python -m pip install gunicorn

Then you need to start Gunicorn, for example with a systemd service:

[Unit]
Description=gunicorn-comics
After=network.target

[Install]
WantedBy=multi-user.target

[Service]
User=comics-user
Group=comics-user
Restart=always

ExecStart=/srv/comics.example.com/app/venv/bin/gunicorn --bind=127.0.0.1:8000 --workers=9 --access-logfile=/srv/comics.example.com/htlogs/gunicorn-access.log --error-logfile=/srv/comics.example.com/htlogs/gunicorn-error.log comics.wsgi
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s TERM $MAINPID

WorkingDirectory=/srv/comics.example.com/app/comics
Environment=PYTHONPATH='/srv/comics.example.com/app/comics'

PrivateTmp=true

Example Nginx vhost

The web server Nginx can be used in front of Gunicorn to terminate HTTPS connections and effectively serve static files.

The following is an example of a complete Nginx vhost:

server {
    server_name comics.example.com;
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    access_log /srv/comics.example.com/htlogs/nginx-access.log;
    error_log /srv/comics.example.com/htlogs/nginx-error.log error;

    ssl_certificate /etc/letsencrypt/live/comics.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/comics.example.com/privkey.pem;

    location /static {
        root /srv/comics.example.com/htdocs;
        expires max;

        location ~* \/fonts\/ {
            add_header Access-Control-Allow-Origin *;
        }
    }

    location / {
        proxy_pass_header Server;
        proxy_set_header Host $http_host;
        proxy_redirect off;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Scheme $scheme;
        proxy_connect_timeout 10;
        proxy_read_timeout 30;
        proxy_pass http://localhost:8000/;
    }
}

For details, please refer to the documentation of the Nginx project.

Collecting static files

When you’re not running in development mode, you’ll need to collect the static files from all apps into the STATIC_ROOT. To do this, run:

python manage.py collectstatic

You have to rerun this command every time you deploy changes to graphics, CSS and JavaScript. For more details, see the Django documentation on staticfiles.

Example cronjob

To get new comics releases, you should run comics_getreleases regularly. In addition, you should run clearsessions to clear expired user sessions. One way is to use cron e.g. by placing the following in /etc/cron.d/comics:

MAILTO=comics@example.com
PYTHONPATH=/srv/comics.example.com/app/comics
1 * * * * comics-user python /srv/comics.example.com/app/comics/manage.py comics_getreleases -v0
1 3 * * * comics-user python /srv/comics.example.com/app/comics/manage.py clearsessions -v0

If you have installed Comics’ dependencies in a virtualenv instead of globally, the cronjob must also activate the virtualenv. This can be done by using the python interpreter from the virtualenv:

MAILTO=comics@example.com
PYTHONPATH=/srv/comics.example.com/app/comics
1 * * * * comics-user /srv/comics.example.com/app/venv/bin/python /srv/comics.example.com/app/comics/manage.py comics_getreleases -v0
1 3 * * * comics-user /srv/comics.example.com/app/venv/bin/python /srv/comics.example.com/app/comics/manage.py clearsessions -v0

By setting MAILTO any exceptions raised by the comic crawlers will be sent by mail to the given mail address. 1 * * * * specifies that the command should be run 1 minute past every hour.