Redis - install and use

How to install and use Redis on Joes Home Server and NextJS API endpoints

Install Redis on a Docker Image

Create a ubuntu-home-server/stacks/04-redis/docker-compose.yml file:

services:
  redis:
    image: redis:7-alpine
    container_name: redis
    restart: unless-stopped
    command:
      - redis-server
      - --appendonly
      - "yes"
      - --requirepass
      - "${REDIS_PASSWORD}"
    volumes:
      - redis_data:/data
    networks:
      - edge
  redisinsight:
    image: redis/redisinsight:latest
    container_name: redisinsight
    restart: unless-stopped
    volumes:
      - redisinsight_data:/data
    networks:
      - edge
volumes:
  redis_data:
  redisinsight_data:
networks:
  edge:
    external: true

Also create a ubuntu-home-server/stacks/04-redis/.env file:

# Note this is an .env build arg, not a stack.env runtime arg!
REDIS_PASSWORD=yourpassword

Although not designed to be super secure, it's a good to lock it down a little bit. You should never be using Redis for secure or PII data anyhow!

To start the Redis server and RedisInsight GUI run:

cd ~/Develop/ubuntu-home-server/stacks/04-redis && docker compose up -d

Access the RedisInsight GUI

The Insights GUI doesn't have any security by default, so we have used CloudFlare Tunnels to view it using https://admin-redisinsight.joemore.dev/ - but also with CloudFlare we have added a Zero Trust --> Access Controls --> Applications rule to secure admin-*.joemore.dev domains using a Zero Trust --> Access Controls --> Policies policy that only allows access to a list of emails.

Enter your email and if it's on the list you'll get a code, and then you can access any admin-*.joemore.dev admin tool!

Adding LAN Access for localhost on another machine

Add a 2nd file: stacks/04-redis/docker-compose.lan.yml

services:
  redis:
    ports:
      - "192.168.0.101:6379:6379"

Then run:

cd ~/Develop/ubuntu-home-server/stacks/04-redis
docker compose -f docker-compose.yml -f docker-compose.lan.yml up -d

This will start the Redis server and allow access from another machine on the LAN.

A Quick think about a good Key Structure

website:{<siteSlug>}:<namespace>:<thing>:<id>

# Eg.g Rate limiting
website:{joemore.dev}:rate_limiting:ip:203.0.113.10
website:{3dnames.co}:rate_limiting:ip:212.0.78.106

# Eg.g Caching
website:{joemore.dev}:cache:user:uuid-1234
website:{3dnames.co}:cache:user:uuid-1234

Note these aren't strict rules, but a good starting point!

Use Redis in a NextJS website

Update your Local .env.local or Servers .env file with the following:

## Local redis vars
REDIS_HOST=192.168.0.101
REDIS_PORT=6379
REDIS_PASSWORD=Your_long_Password_Here
REDIS_DB_ID=1 # Each website should have it's own DB ID
REDIS_KEY_PREFIX=website:{joemore.dev}

## Server redis vars
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=Your_long_Password_Here
REDIS_DB_ID=1 # Each website should have it's own DB ID
REDIS_KEY_PREFIX=website:{joemore.dev}

Check out this live demo!

Redis API Test

Endpoint: /api/redis-test

IDLELast action: -

Response

No response yet. Click GET/POST/DELETE.

Tip: If you protect this page with Cloudflare Access, you can safely keep it deployed as a diagnostics tool.

How to build the above API endpoint

Install the Redis client: yarn add ioredis and add our helper file:

Helper File - src/lib/redis.ts

// src/lib/redis.ts
import Redis from "ioredis";

function getRedisUrl() {
  // Prefer a full URL if provided
  const url = process.env.REDIS_URL;
  if (url && url.trim().length > 0) return url;

  // Otherwise build from pieces
  const host = process.env.REDIS_HOST ?? "redis"; // docker default
  const port = Number(process.env.REDIS_PORT ?? "6379");
  const password = process.env.REDIS_PASSWORD;
  const db = process.env.REDIS_DB_ID ?? "0";

  // Build redis:// URL (include password only if present)
  if (password && password.length > 0) {
    return `redis://:${encodeURIComponent(password)}@${host}:${port}/${db}`;
  }
  return `redis://${host}:${port}/${db}`;
}

const redisUrl = getRedisUrl();

declare global {
  // eslint-disable-next-line no-var
  var __redis: Redis | undefined;
}

export const redis =
  global.__redis ??
  new Redis(redisUrl, {
    maxRetriesPerRequest: 2,
    enableReadyCheck: true,
    lazyConnect: false,
    keyPrefix: process.env.REDIS_KEY_PREFIX
      ? `${process.env.REDIS_KEY_PREFIX}:`
      : undefined,
  });

if (process.env.NODE_ENV !== "production") global.__redis = redis;

// ---------- Key helpers ----------

export const redisKeyPrefix =
  process.env.REDIS_KEY_PREFIX && process.env.REDIS_KEY_PREFIX.length > 0
    ? `${process.env.REDIS_KEY_PREFIX}:`
    : "";

export function withRedisKeyPrefix(key: string) {
  return `${redisKeyPrefix}${key}`;
}

export function siteKey(siteSlug: string, ...parts: (string | number)[]) {
  return `site:{${siteSlug}}:${parts.join(":")}`;
}

export async function cacheGetOrSet<T>(
  key: string,
  ttlSeconds: number,
  compute: () => Promise<T>
): Promise<{ value: T; hit: boolean }> {
  const existing = await redis.get(key);
  if (existing) return { value: JSON.parse(existing) as T, hit: true };

  const value = await compute();
  await redis.set(key, JSON.stringify(value), "EX", ttlSeconds);
  return { value, hit: false };
}

export async function rateLimit(
  key: string,
  limit: number,
  windowSeconds: number
): Promise<{ allowed: boolean; remaining: number; resetIn: number }> {
  const count = await redis.incr(key);
  if (count === 1) await redis.expire(key, windowSeconds);

  const ttl = await redis.ttl(key);
  const remaining = Math.max(0, limit - count);

  return {
    allowed: count <= limit,
    remaining,
    resetIn: ttl > 0 ? ttl : windowSeconds,
  };
}

Our API Endpoint - src/app/api/redis-test/route.ts

import { redis, withRedisKeyPrefix } from "@/lib/redis";
import { NextResponse } from "next/server";

export const runtime = "nodejs"; // Redis client needs Node runtime

const RAW_KEY = "test:record:v1";
const KEY = withRedisKeyPrefix(RAW_KEY);

/*
We end up with something, like this:
./website
./website/{jemore.dev}
./website/{jemore.dev}/
./website/{jemore.dev}/test
./website/{jemore.dev}/test/record
./website/{jemore.dev}/test/record/v1 --> JSON Data in here!
*/

type TestRecord = {
  id: string;
  createdAt: string;
  updatedAt: string;
  updateCount: number;
  note: string;
};

async function readRecord(): Promise<TestRecord | null> {
  const raw = await redis.get(RAW_KEY);
  return raw ? (JSON.parse(raw) as TestRecord) : null;
}

// Read the record
export async function GET() {
  const record = await readRecord();
  const ttl = await redis.ttl(RAW_KEY);

  return NextResponse.json({
    ok: true,
    key: KEY,
    exists: !!record,
    ttlSeconds: ttl, // -1 means no TTL, -2 means key missing
    record,
  });
}

// Create or update the record
export async function POST(req: Request) {
  const body = (await req.json().catch(() => ({}))) as Partial<{
    note: string;
    ttlSeconds: number;
  }>;

  const now = new Date().toISOString();
  const existing = await readRecord();

  const next: TestRecord = existing
    ? {
        ...existing,
        updatedAt: now,
        updateCount: (existing.updateCount ?? 0) + 1,
        note: typeof body.note === "string" ? body.note : existing.note,
      }
    : {
        id: "redis-test",
        createdAt: now,
        updatedAt: now,
        updateCount: 1,
        note: typeof body.note === "string" ? body.note : "Created by redis-test endpoint",
      };

  const ttlSeconds = typeof body.ttlSeconds === "number" ? body.ttlSeconds : 3600; // default 1h
  await redis.set(RAW_KEY, JSON.stringify(next), "EX", ttlSeconds);

  return NextResponse.json({
    ok: true,
    action: existing ? "updated" : "created",
    key: KEY,
    ttlSeconds,
    record: next,
  });
}

// Delete the record
export async function DELETE() {
  const existed = await redis.exists(RAW_KEY);
  const deletedCount = await redis.del(RAW_KEY);

  return NextResponse.json({
    ok: true,
    key: KEY,
    existed: existed === 1,
    deleted: deletedCount === 1,
  });
}

Was this page helpful?