Migrate services to rspace.online domains and fix Traefik routing

- Add separate routers for rspace.online aliases (collab, ipfs, ipfs-api)
  instead of || syntax to avoid priority conflicts
- Fix healthcheck quoting for Node.js v23 TypeScript-style parsing
- Update IPFS client defaults from jeffemmett.com to rspace.online
- Add rspace.online CORS origins to collab-server

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-31 18:19:14 -07:00
parent b19d2c1ad7
commit 6291b2cb2d
2 changed files with 33 additions and 13 deletions

View File

@ -3,7 +3,9 @@
#
# Deploy: scp to Netcup /opt/apps/collab-server/, docker compose up -d
# Requires: Traefik proxy network
# DNS: collab.jeffemmett.com, ipfs.jeffemmett.com, ipfs-api.jeffemmett.com
# DNS: collab.jeffemmett.com / collab.rspace.online
# ipfs.jeffemmett.com / ipfs.rspace.online
# ipfs-api.jeffemmett.com / ipfs-api.rspace.online
services:
collab-server:
@ -17,7 +19,7 @@ services:
NODE_ENV: production
MONGODB_URI: mongodb://collab-mongo:27017/collaboration
REDIS_ENABLED: "false"
CORS_ORIGINS: "https://rnotes.jeffemmett.com,https://rspace.jeffemmett.com,http://localhost:3000,http://localhost:5173"
CORS_ORIGINS: "https://rnotes.jeffemmett.com,https://rspace.jeffemmett.com,https://collab.rspace.online,https://ipfs.rspace.online,https://collab.jeffemmett.com,http://localhost:3000,http://localhost:5173"
SERVER_DID: "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
RATE_LIMIT_WINDOW_MS: 900000
RATE_LIMIT_MAX: 100
@ -26,14 +28,21 @@ services:
- collab-internal
labels:
- "traefik.enable=true"
# Primary domain
- "traefik.http.routers.collab.rule=Host(`collab.jeffemmett.com`)"
- "traefik.http.routers.collab.entrypoints=web"
- "traefik.http.routers.collab.service=collab"
# rspace.online alias
- "traefik.http.routers.collab-rspace.rule=Host(`collab.rspace.online`)"
- "traefik.http.routers.collab-rspace.entrypoints=web"
- "traefik.http.routers.collab-rspace.service=collab"
# Shared service
- "traefik.http.services.collab.loadbalancer.server.port=5001"
depends_on:
collab-mongo:
condition: service_started
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://localhost:5001/health').then(r => process.exit(r.ok ? 0 : 1))"]
test: ["CMD-SHELL", "node -e \"fetch('http://localhost:5001/health').then(r=>process.exit(r.ok?0:1))\""]
interval: 30s
timeout: 5s
retries: 3
@ -60,19 +69,30 @@ services:
- collab-internal
labels:
- "traefik.enable=true"
# IPFS Gateway (public, read-only)
# IPFS Gateway — primary domain
- "traefik.http.routers.ipfs-gw.rule=Host(`ipfs.jeffemmett.com`)"
- "traefik.http.routers.ipfs-gw.entrypoints=web"
- "traefik.http.routers.ipfs-gw.service=ipfs-gw"
# IPFS Gateway — rspace.online alias
- "traefik.http.routers.ipfs-gw-rspace.rule=Host(`ipfs.rspace.online`)"
- "traefik.http.routers.ipfs-gw-rspace.entrypoints=web"
- "traefik.http.routers.ipfs-gw-rspace.service=ipfs-gw"
# Shared gateway service
- "traefik.http.services.ipfs-gw.loadbalancer.server.port=8080"
# IPFS API (private, Headscale-only access via IP allowlist)
# IPFS API — primary domain
- "traefik.http.routers.ipfs-api.rule=Host(`ipfs-api.jeffemmett.com`)"
- "traefik.http.routers.ipfs-api.entrypoints=web"
- "traefik.http.routers.ipfs-api.service=ipfs-api"
- "traefik.http.routers.ipfs-api.middlewares=ipfs-api-ipallow"
# IPFS API — rspace.online alias
- "traefik.http.routers.ipfs-api-rspace.rule=Host(`ipfs-api.rspace.online`)"
- "traefik.http.routers.ipfs-api-rspace.entrypoints=web"
- "traefik.http.routers.ipfs-api-rspace.service=ipfs-api"
- "traefik.http.routers.ipfs-api-rspace.middlewares=ipfs-api-ipallow"
# Shared API service
- "traefik.http.services.ipfs-api.loadbalancer.server.port=5001"
# Restrict API to Headscale mesh + Cloudflare tunnel IPs
- "traefik.http.middlewares.ipfs-api-ipallow.ipallowlist.sourcerange=100.64.0.0/10,127.0.0.1/32,172.16.0.0/12"
- "traefik.http.routers.ipfs-api.middlewares=ipfs-api-ipallow"
healthcheck:
test: ["CMD", "ipfs", "id"]
interval: 30s

View File

@ -48,7 +48,7 @@ export class PinataBackend implements IPFSBackend {
constructor(jwt: string, gateway?: string) {
this.jwt = jwt
this.gateway = gateway ?? 'https://ipfs.jeffemmett.com/ipfs'
this.gateway = gateway ?? 'https://ipfs.rspace.online/ipfs'
}
async upload(data: Uint8Array, filename: string): Promise<string> {
@ -101,8 +101,8 @@ export class KuboBackend implements IPFSBackend {
/** Create KuboBackend from environment variables */
static fromEnv(): KuboBackend {
const apiUrl = process.env.IPFS_API_URL || 'https://ipfs-api.jeffemmett.com'
const gatewayUrl = process.env.IPFS_GATEWAY_URL || 'https://ipfs.jeffemmett.com'
const apiUrl = process.env.IPFS_API_URL || 'https://ipfs-api.rspace.online'
const gatewayUrl = process.env.IPFS_GATEWAY_URL || 'https://ipfs.rspace.online'
const authToken = process.env.IPFS_AUTH_TOKEN
return new KuboBackend(apiUrl, gatewayUrl, authToken)
}
@ -213,7 +213,7 @@ export class EncryptedIPFSClient {
* Generate an IPFS gateway URL (returns encrypted content client must decrypt)
*/
gatewayUrl(cid: string, gateway?: string): string {
return `${gateway ?? 'https://ipfs.jeffemmett.com/ipfs'}/${cid}`
return `${gateway ?? 'https://ipfs.rspace.online/ipfs'}/${cid}`
}
}
@ -242,7 +242,7 @@ export function createImageAttrs(
alt?: string
): IPFSImageAttrs {
return {
src: `${gateway ?? 'https://ipfs.jeffemmett.com/ipfs'}/${metadata.cid}`,
src: `${gateway ?? 'https://ipfs.rspace.online/ipfs'}/${metadata.cid}`,
cid: metadata.cid,
encKey: metadata.encryptionKey,
alt: alt ?? metadata.filename,
@ -253,8 +253,8 @@ export function createImageAttrs(
/** Create an EncryptedIPFSClient using the self-hosted kubo node */
export function createSelfHostedClient(
apiUrl = 'https://ipfs-api.jeffemmett.com',
gatewayUrl = 'https://ipfs.jeffemmett.com'
apiUrl = 'https://ipfs-api.rspace.online',
gatewayUrl = 'https://ipfs.rspace.online'
): EncryptedIPFSClient {
return new EncryptedIPFSClient(new KuboBackend(apiUrl, gatewayUrl))
}