**********
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:
.. code-block:: text
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:
.. code-block:: sh
cd /srv/comics.example.com/app
uv sync --extra server
Then you need to start Gunicorn, for example with a systemd service:
.. code-block:: ini
[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:
.. code-block:: nginx
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:
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``:
.. code-block:: sh
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.