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.
OK! Server setup is now done! Lets see how we can use Redis in a NextJS API endpoint!
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
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,
});
}