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.

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/.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_CSRF_TRUSTED_ORIGINS=https://comics.example.com

DJANGO_DEFAULT_FROM_EMAIL=comics@example.com
# Sending email, alternative 1: Using a local SMTP server
DJANGO_EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
# Sending email, alternative 2: Using the Mailgun API (which has a free tier)
MAILGUN_API_KEY=your-mailgun-api-key
MAILGUN_API_URL=https://api.eu.mailgun.net/v3
MAILGUN_SENDER_DOMAIN=comics.example.com

DJANGO_MEDIA_ROOT=/srv/comics.example.com/htdocs/media/
DJANGO_MEDIA_URL=https://comics.example.com/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/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:

cd /srv/comics.example.com/app
uv sync --extra server

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

[Unit]
Description=comics
After=network.target

[Install]
WantedBy=multi-user.target

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

WorkingDirectory=/srv/comics.example.com/app
ExecStart=uv run 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

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 /media {
        root /srv/comics.example.com/htdocs;
        expires max;
    }

    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:

uv run comics 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 get_releases 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
1 * * * * comics-user cd /srv/comics.example.com/app && uv run comics get_releases -v0
1 3 * * * comics-user cd /srv/comics.example.com/app && uv run comics 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.