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 theuserline in step 2<generated password>: The password generated belownon_root,my-project: The user Django runs as and the project directory, used by the optional queue worker service underdjango-rqusage
1. Install
sudo apt update
sudo apt install -y redis-server2. Lock it down
Generate a password (hex, so it's safe to use in a connection URL):
openssl rand -hex 32Edit 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
# 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 +infoWARNING
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:
sudo chown redis:redis /etc/redis/redis.conf
sudo chmod 640 /etc/redis/redis.conf3. Apply
sudo systemctl enable redis-server # Start automatically after reboot
sudo systemctl restart redis-serverIMPORTANT
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.
# 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:
pip install django-redis django-rqAdd the Redis credentials to the project's .env file:
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:
| Database | Used for | Safe to clear? |
|---|---|---|
/1 | default cache: disposable data | Yes |
/2 | django-rq queues (Optional - if you don't need Redis queues) | No |
/3 | auth cache: OTPs, reset tokens, rate limits | No |
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:
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:
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
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:
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:
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:
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, notuser. The arguments are serialized into Redis, so aUserobject 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 withUser.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:
# `emails` and `default` are the queues from RQ_QUEUES, in priority order: emails first
python manage.py rqworker emails default --with-schedulerIn production, run it as a service. The user and paths below match the Django with Nginx guide:
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 rqworkerRestart the worker after every deploy, like Gunicorn, so it runs the new code:
sudo systemctl restart rqworker
sudo journalctl -u rqworker -n 50 # worker logs
python manage.py rqstats # jobs queued, running and failed per queue