Skip to content

Install Redis on Debian (Django Server) ​

This guide installs Redis on a Debian server that also runs a Django application, with Redis listening on localhost only. Django uses Redis for its cache as well as for data that must not be lost, such as auth keys and queued jobs (e.g. account emails), so Redis is configured to persist to disk and never evict keys.

NOTE

Replace the following placeholders with your own values:

  • your-redis-username: A name you choose for the Redis ACL user, created by the user line in step 2
  • <generated password>: The password generated below
  • non_root, my-project: The user Django runs as and the project directory, used by the optional queue worker service under django-rq usage

1. Install ​

shell
sudo apt update
sudo apt install -y redis-server

2. Lock it down ​

Generate a password (hex, so it's safe to use in a connection URL):

shell
openssl rand -hex 32

Edit the config (sudo vi /etc/redis/redis.conf) and set the following. Redis doesn't allow comments after a value, so keep every comment on its own line:

TIP

There are many comment lines (starts with #) and blank lines in this file, so to just see what all are set, use this command: sudo grep -vE '^\s*(#|$)' /etc/redis/redis.conf

txt
# Listen on localhost only
bind 127.0.0.1 -::1
protected-mode yes

# Size to what the server can spare
maxmemory 128mb
# Never delete keys to free memory: auth keys and queued jobs must survive.
# When full, writes fail with an error instead.
maxmemory-policy noeviction

# Persist every write to disk; at most ~1s of writes is lost on a crash
appendonly yes
appendfsync everysec
# No periodic snapshots; the append-only file is enough
save ""

# Disable the default (passwordless) user
user default off

# Application user: full access to keys, no admin or dangerous commands, except
# FLUSHDB (Django's `cache.clear()` needs it) and INFO (to check memory usage)
user your-redis-username on ><generated password> ~* &* +@all -@admin -@dangerous +flushdb +info

WARNING

With noeviction, Redis refuses writes once it reaches maxmemory, and the application gets errors instead. Give every cache entry a timeout (Django's default is 300 seconds) so the cache can't fill Redis, and raise maxmemory value if the server has room.

The password is stored in cleartext in this file, so lock it down:

shell
sudo chown redis:redis /etc/redis/redis.conf
sudo chmod 640 /etc/redis/redis.conf

3. Apply ​

shell
sudo systemctl enable redis-server # Start automatically after reboot
sudo systemctl restart redis-server

IMPORTANT

Always manage Redis through systemctl. Running redis-server by hand skips /etc/redis/redis.conf, so that instance has no password, no memory limit and no persistence.

4. Verify ​

Enter Redis Password when prompted.

shell
# PONG - the ACL user only exists in redis.conf, so this also confirms the config was loaded
redis-cli --user your-redis-username --askpass ping

# NOPERM - the application user can't run admin commands
redis-cli --user your-redis-username --askpass config get maxmemory

# Only 127.0.0.1 / [::1], never 0.0.0.0
sudo ss -ltnp | grep 6379

# Persistence: the key must survive a restart
redis-cli --user your-redis-username --askpass set setup-check ok
sudo systemctl restart redis-server
redis-cli --user your-redis-username --askpass getdel setup-check   # "ok"

# Memory usage, well below maxmemory
redis-cli --user your-redis-username --askpass info memory | grep -E 'used_memory_human|maxmemory_human'

5. Connect Django ​

Install the packages in the project's virtual environment:

shell
pip install django-redis django-rq

Add the Redis credentials to the project's .env file:

txt
REDIS_USERNAME=your-redis-username
REDIS_PASSWORD=<generated password>

Each kind of data gets its own Redis database. cache.clear() runs FLUSHDB, which empties only the database of the cache it's called on, so clearing the disposable cache can't wipe auth keys or queued jobs:

DatabaseUsed forSafe to clear?
/1default cache: disposable dataYes
/2django-rq queues (Optional - if you don't need Redis queues)No
/3auth cache: OTPs, reset tokens, rate limitsNo

TIP

Redis queues are useful for things like account-related emails, which may slow down API response times. With a queue in place, the email job is enqueued in Redis, the API returns immediately, and a worker process sends the email moments later. Because the job is persisted before the response is sent, it survives restarts and can be retried if the mail server is down.

Settings ​

Update settings.py:

python
import os
from urllib.parse import quote

INSTALLED_APPS = [
    # ...
    "django_rq",
]

REDIS_URL = "redis://{}:{}@127.0.0.1:6379".format(
    quote(os.getenv("REDIS_USERNAME", ""), safe=""),
    quote(os.getenv("REDIS_PASSWORD", ""), safe=""),
)

# /2 is not a cache: it's reserved for the django-rq queues. Skip it if not needed.
RQ_QUEUES = {
    "default": {"URL": f"{REDIS_URL}/2", "DEFAULT_TIMEOUT": 360},
    "emails": {"URL": f"{REDIS_URL}/2", "DEFAULT_TIMEOUT": 60},
}

CACHES = {
    # Disposable data, safe to clear at any time
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": f"{REDIS_URL}/1",
        # Every entry expires, so the cache can't fill Redis under noeviction
        "TIMEOUT": 300,
    },
    # Auth keys that must not be cleared along with the cache
    "auth": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": f"{REDIS_URL}/3",
        "TIMEOUT": 600,
    },
}

WARNING

Never pass timeout=None (no expiry) to a cache call unless something else deletes the key, and never call caches["auth"].clear(). Under noeviction, keys that never expire stay in Redis until it's full.

Check the connection ​

Restart the web server, then confirm both cache aliases, and the queue if you set one up, from python manage.py shell:

python
from uuid import uuid4
from django.core.cache import caches

# Check Redis authentication and each cache alias.
# The keys expire in 60s, so a failure here leaves nothing behind.
for alias in ("default", "auth"):
    key = f"setup-check:{uuid4().hex}"
    caches[alias].set(key, "working", timeout=60)
    assert caches[alias].get(key) == "working", f"{alias} cache read/write failed"
    caches[alias].delete(key)

print("Redis cache: OK")

# Confirm the queue's connection. Skip the rest if you're not using django-rq.
from django_rq import get_queue

assert get_queue("emails").connection.ping() is True
print("Redis queue: OK")

6. Manage the service ​

shell
sudo systemctl status redis-server
sudo systemctl restart redis-server
sudo journalctl -u redis-server -n 50    # recent logs (also /var/log/redis/redis-server.log)
redis-cli --user your-redis-username --askpass info memory | grep used_memory_human # Memory usage, enter Redis password when prompted

(Reference) Usage ​

With this setup in place, you access default database of Redis using Django by default if you don't specify anything explicitly. For example, the following code uses it:

python
from django.core.cache import cache

cache.add(key, value, timeout)
cache_value = cache.get(key)
assert cache_value == value
cache.delete(key)

django-rq usage ​

Define a job, for example in accounts/tasks.py:

python
from django.contrib.auth import get_user_model
from django.core.mail import send_mail
from django_rq import job
from rq import Retry

@job(
    # Queue this job goes to, one of the names in RQ_QUEUES
    "emails",
    # Retry 3 times, 10s / 1min / 5min apart (e.g. if the SMTP server is briefly down)
    retry=Retry(max=3, interval=[10, 60, 300]),
    # Don't keep finished jobs, and keep failed ones for 7 days (RQ's default is a year)
    result_ttl=0,
    failure_ttl=7 * 24 * 3600,
)
def send_welcome_email(user_id):
    user = get_user_model().objects.get(pk=user_id)
    send_mail("Welcome", "Thanks for signing up.", None, [user.email])

Enqueue it from a view (or a signal) with .delay(), which returns immediately and leaves the work to the worker:

python
from django.db import transaction

from accounts.tasks import send_welcome_email

def signup(request):
    ...
    with transaction.atomic():
        user = form.save()
        # Enqueued only after this block commits successfully
        transaction.on_commit(lambda: send_welcome_email.delay(user.pk))
    return redirect("home")

Two details in that call are worth explaining:

  • Pass user.pk, not user. The arguments are serialized into Redis, so a User object would be sent as a stale copy of the row as it looked at enqueue time. The job re-fetches the row instead, and gets the current data.
  • Wrap it in transaction.on_commit(). A worker is a separate process with its own database connection, so it can pick the job up in the milliseconds before the view's transaction commits. It then queries for a row that isn't visible yet and fails with User.DoesNotExist. on_commit() holds the callback until the transaction commits, and drops it entirely if the transaction rolls back — so no email goes out for a signup that failed.

WARNING

Don't forget the .delay(). The @job decorator doesn't replace the function, it only attaches .delay() to it, so send_welcome_email(user.pk) is an ordinary call, and Redis is never touched, that raises nothing and runs the job inline in the request thread.

Run a worker to process the jobs. --with-scheduler is needed for the retry intervals. For a quick test, run it in the foreground:

shell
# `emails` and `default` are the queues from RQ_QUEUES, in priority order: emails first
python manage.py rqworker emails default --with-scheduler

In production, run it as a service. The user and paths below match the Django with Nginx guide:

shell
sudo tee /etc/systemd/system/rqworker.service > /dev/null <<'EOF'
[Unit]
Description=RQ worker for Django
After=network.target redis-server.service
Wants=redis-server.service

[Service]
User=non_root
Group=non_root
WorkingDirectory=/home/non_root/my-project
ExecStart=/home/non_root/my-project/.venv/bin/python manage.py rqworker emails default --with-scheduler
# On stop, systemd sends SIGTERM, which lets the current job finish first
TimeoutStopSec=90
Restart=on-failure

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now rqworker

Restart the worker after every deploy, like Gunicorn, so it runs the new code:

shell
sudo systemctl restart rqworker
sudo journalctl -u rqworker -n 50    # worker logs
python manage.py rqstats             # jobs queued, running and failed per queue