diff --git a/.env.example b/.env.example index b630ce3..11757ab 100644 --- a/.env.example +++ b/.env.example @@ -22,5 +22,6 @@ JWT_REFRESH_EXPIRES_IN=30d PODCAST_INDEX_API_KEY= PODCAST_INDEX_API_SECRET= -# Frontend Configuration -VITE_API_URL=http://localhost:5000/api +# Frontend Configuration (only needed for local dev without Docker) +# In Docker, the frontend is served by the backend itself — no VITE_API_URL needed +VITE_API_URL=/api diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0a78a90 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,48 @@ +# Stage 1: Build frontend +FROM node:20-alpine AS frontend-builder + +WORKDIR /frontend + +COPY frontend/package*.json ./ +RUN npm ci + +COPY frontend/ ./ + +# Build with /api as the API base URL (served by Express on same host) +ENV VITE_API_URL=/api +RUN npm run build + +# Stage 2: Build backend +FROM node:20-alpine AS backend-builder + +WORKDIR /app + +COPY backend/package*.json ./ +RUN npm ci + +COPY backend/tsconfig.json ./ +COPY backend/src ./src + +RUN npm run build + +# Stage 3: Production image +FROM node:20-alpine + +WORKDIR /app + +# Copy backend production deps +COPY backend/package*.json ./ +RUN npm ci --omit=dev + +# Copy compiled backend +COPY --from=backend-builder /app/dist ./dist + +# Copy frontend build into public/ (backend will serve it as static files) +COPY --from=frontend-builder /frontend/dist ./public + +EXPOSE 3579 + +ENV NODE_ENV=production +ENV PORT=3579 + +CMD ["node", "dist/index.js"] diff --git a/UNRAID_INSTALL.md b/UNRAID_INSTALL.md index c9ddb49..8876bc3 100644 --- a/UNRAID_INSTALL.md +++ b/UNRAID_INSTALL.md @@ -1,458 +1,133 @@ -# 🐳 Podcastic - Unraid Installation Guide +# Podcastic - Unraid Installation Guide -Complete guide to install Podcastic on Unraid as a **single all-in-one container**. +Single container on port **3579** — includes backend, frontend, and serves everything via Express. -## 📋 What's Included +## What's Included This single Docker container includes: -- ✅ **Backend**: Node.js + Express.js API -- ✅ **Frontend**: React web interface -- ✅ **Database**: MongoDB (embedded) -- ✅ **Cache**: Redis (embedded) -- ✅ **Web Server**: Nginx reverse proxy -- ✅ **Process Manager**: Supervisor (manages all 5 services) - -All in **ONE container** on port **3579** +- **Backend**: Node.js + Express.js API +- **Frontend**: React web interface (served by Express as static files) +- **Database**: MongoDB (separate container, managed by Compose) +- **Cache**: Redis (separate container, managed by Compose) ## Prerequisites - Unraid server with Docker support -- ~2GB RAM available -- ~20GB storage for database/media -- Internet connection +- Docker Compose (or Unraid's Compose Manager plugin) -## Installation Steps +## Installation ### Step 1: Clone Repository ```bash -# Via terminal on Unraid -cd /mnt/user/appdata # or your preferred path +cd /mnt/user/appdata git clone https://github.com/R0m1k3/podcastic.git cd podcastic ``` -Or download as ZIP from GitHub and extract. - ### Step 2: Configure Environment ```bash -# Copy template cp .env.example .env - -# Edit with your settings nano .env ``` -**Key settings**: +**Change at minimum:** ```env -NODE_ENV=production - -# JWT Secret - CHANGE THIS! -JWT_SECRET=your-super-secret-key-change-me-in-production - -# Optional: PodcastIndex API for podcast search -PODCAST_INDEX_API_KEY= -PODCAST_INDEX_API_SECRET= +JWT_SECRET=your-random-secret-here ``` -### Step 3: Build Docker Image +### Step 3: Build and Start -Option A: Build locally (takes ~5-10 minutes) ```bash -docker-compose -f docker-compose.unraid.yml build --no-cache +docker-compose build --no-cache +docker-compose up -d ``` -Option B: Use pre-built image (faster) -```bash -# Will be available after first push to Docker Hub -# docker pull r0m1k3/podcastic:latest +### Step 4: Access + +Open: `http://your-unraid-ip:3579` + +--- + +## Container Architecture + +``` +docker-compose services: +├── podcastic (port 3579) +│ ├── Express.js API → /api/* +│ └── React frontend → /* (static files) +├── podcastic-mongodb → internal :27017 +└── podcastic-redis → internal :6379 ``` -### Step 4: Start Container +Only port **3579** is exposed externally. + +--- + +## Data Persistence + +Add volume mounts to `docker-compose.yml` for persistence: + +```yaml + mongodb: + volumes: + - /mnt/user/appdata/podcastic/db:/data/db + + redis: + volumes: + - /mnt/user/appdata/podcastic/redis:/data +``` + +--- + +## Management Commands ```bash -# Start -docker-compose -f docker-compose.unraid.yml up -d - # View logs -docker-compose -f docker-compose.unraid.yml logs -f podcastic +docker-compose logs -f app -# Check status -docker ps | grep podcastic -``` +# Restart +docker-compose restart app -### Step 5: Access Application +# Stop all +docker-compose down -Open browser: -``` -http://your-unraid-ip:3579 -``` - -**First Time Setup**: -1. Click "Create Account" -2. Register with email/password -3. Log in -4. Go to "Discover" to search podcasts -5. Subscribe and listen! - ---- - -## 📊 Container Architecture - -``` -Single Container (podcastic) -├── Nginx (port 3579) -│ ├── Serves static frontend -│ └── Proxies /api to backend -├── Node.js Backend (internal port 5000) -│ ├── API routes -│ ├── Authentication -│ └── Database management -├── MongoDB (internal port 27017) -│ └── Podcast data storage -└── Redis (internal port 6379) - └── Cache & sessions -``` - -All services communicate **internally** - only port 3579 exposed. - ---- - -## 🛠️ Unraid Integration - -### Add to Unraid via WebUI - -1. **Settings → Docker** → Enable Docker -2. **Docker** tab -3. Add custom template: - ```xml - - - podcastic - r0m1k3/podcastic:latest - https://hub.docker.com/ - bridge - - sh - false - - - - - http://[IP]:[PORT:3579] - - - - - false - - - Podcastic - Multi-user podcast streaming application - - bridge - - - 3579 - 3579 - tcp - - - - - - /mnt/user/appdata/podcastic/db - /data/db - rw - - - /mnt/user/appdata/podcastic/redis - /data/redis - rw - - - /mnt/user/appdata/podcastic/logs - /var/log/supervisor - rw - - - - - JWT_SECRET - change-me-in-production - - - PODCAST_INDEX_API_KEY - - - - PODCAST_INDEX_API_SECRET - - - - - ``` - -Or use Docker Compose tab with `docker-compose.unraid.yml` - ---- - -## 📂 Data Persistence - -Data is saved to Unraid shares: - -``` -/mnt/user/appdata/podcastic/ -├── db/ # MongoDB data (podcasts, users, episodes) -├── redis/ # Redis cache data -└── logs/ # Application logs -``` - -### Backup - -```bash -# Backup database -docker exec podcastic mongodump --out /backup/mongodb - -# Copy to safe location -cp -r /mnt/user/appdata/podcastic /mnt/backup/podcastic-backup -``` - -### Restore - -```bash -docker exec podcastic mongorestore /backup/mongodb -``` - ---- - -## 🔧 Management Commands - -### View Logs - -```bash -# All logs -docker logs -f podcastic - -# Specific service -docker exec podcastic tail -f /var/log/supervisor/backend.out.log -docker exec podcastic tail -f /var/log/supervisor/mongodb.out.log -docker exec podcastic tail -f /var/log/supervisor/redis.out.log -docker exec podcastic tail -f /var/log/supervisor/nginx.out.log -``` - -### Restart Container - -```bash -docker-compose -f docker-compose.unraid.yml restart podcastic -``` - -### Stop Container - -```bash -docker-compose -f docker-compose.unraid.yml down - -# Keep volumes (data persists) -# Data in /mnt/user/appdata/podcastic/ is preserved -``` - -### Update to Latest - -```bash -# Pull latest code -cd /path/to/podcastic +# Update git pull - -# Rebuild and restart -docker-compose -f docker-compose.unraid.yml build --no-cache -docker-compose -f docker-compose.unraid.yml up -d +docker-compose build --no-cache +docker-compose up -d ``` --- -## 🐛 Troubleshooting +## Unraid WebUI Template -### Container won't start +Add via Docker > Add Container: + +| Field | Value | +|-------|-------| +| Repository | (build from compose) | +| Port | 3579:3579 | +| Variable: JWT_SECRET | your-secret | +| Variable: MONGODB_URI | mongodb://podcastic-mongodb:27017/podcastic | +| Variable: REDIS_URL | redis://podcastic-redis:6379 | + +Or use **Compose Manager plugin** with the `docker-compose.yml` from this repo. + +--- + +## Troubleshooting ```bash -# Check logs -docker logs podcastic +# Check container status +docker-compose ps -# Common issues: -# - Port 3579 in use: Change in docker-compose.unraid.yml -# - Insufficient RAM: Free up memory -# - Permission issues: Check volume permissions -``` - -### MongoDB not starting - -```bash -# Check mongo logs -docker exec podcastic tail -f /var/log/supervisor/mongodb.out.log - -# Repair database -docker exec podcastic mongod --repair --dbpath /data/db -``` - -### Can't access on port 3579 - -```bash -# Check if container is running -docker ps | grep podcastic - -# Check if port is open -netstat -tulpn | grep 3579 - -# Test connection +# Check health endpoint curl http://localhost:3579/health + +# Check API +curl http://localhost:3579/api/health ``` -### Out of disk space - -MongoDB can grow large. Check: -```bash -docker exec podcastic du -sh /data/db - -# Clean old data (backup first!) -``` - ---- - -## 📈 Performance Optimization - -### For Limited Resources - -Edit `docker-compose.unraid.yml`: -```yaml -# Limit memory usage -mem_limit: 1g -cpus: '0.5' -``` - -### For High Performance - -Increase limits: -```yaml -mem_limit: 4g -cpus: '2' -``` - -### Disable Unneeded Features - -If only streaming (no search): -```env -PODCAST_INDEX_API_KEY= # Leave empty -``` - ---- - -## 🔒 Security Notes - -### Change Default Secrets - -```env -# MUST change in production! -JWT_SECRET=your-unique-secret-key-here - -# Use strong password for first admin account -``` - -### Firewall Rules - -Allow only trusted IPs: -```bash -# On Unraid, set firewall rules in UI -# Or use nginx config to restrict access -``` - -### SSL/HTTPS Setup - -If exposing to internet, use reverse proxy with SSL: -- Nginx Proxy Manager (separate container) -- Traefik (with Let's Encrypt) -- Unraid built-in reverse proxy - ---- - -## 📊 Monitoring - -### Check Container Health - -```bash -# Real-time stats -docker stats podcastic - -# Service status -docker exec podcastic supervisorctl status - -# Expected output: -# mongodb RUNNING pid 123, uptime 0:00:45 -# redis RUNNING pid 124, uptime 0:00:45 -# backend RUNNING pid 125, uptime 0:00:45 -# nginx RUNNING pid 126, uptime 0:00:45 -``` - -### Resource Usage - -```bash -# Disk usage -du -sh /mnt/user/appdata/podcastic/ - -# Memory usage -docker exec podcastic free -h - -# CPU usage -docker stats --no-stream podcastic -``` - ---- - -## 🆘 Support - -If issues occur: - -1. **Check logs**: - ```bash - docker logs podcastic - ``` - -2. **Check service status**: - ```bash - docker exec podcastic supervisorctl status - ``` - -3. **Rebuild container**: - ```bash - docker-compose -f docker-compose.unraid.yml build --no-cache - docker-compose -f docker-compose.unraid.yml up -d - ``` - -4. **Factory reset** (loses all data!): - ```bash - docker-compose -f docker-compose.unraid.yml down -v - rm -rf /mnt/user/appdata/podcastic - # Start fresh - ``` - ---- - -## 🎯 Next Steps - -1. **First Run**: Create account, set JWT_SECRET properly -2. **Configure PodcastIndex**: Optional, enables podcast search -3. **Add Podcasts**: Via Discover or RSS URLs -4. **Enjoy**: Start listening! - ---- - -## 📚 Additional Resources - -- **Main README**: See README.md -- **Getting Started**: See GETTING_STARTED.md -- **PodcastIndex Setup**: See PODCAST_INDEX_SETUP.md -- **GitHub**: https://github.com/R0m1k3/podcastic - ---- - -**Last Updated**: 2026-04-16 -**Tested on**: Unraid 6.12+ +**Last Updated**: 2026-04-16 diff --git a/backend/Dockerfile b/backend/Dockerfile deleted file mode 100644 index f23da40..0000000 --- a/backend/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -FROM node:20-alpine - -WORKDIR /app - -COPY package*.json ./ -RUN npm ci - -COPY tsconfig.json ./ -COPY src ./src - -RUN npm run build - -EXPOSE 5000 - -CMD ["node", "dist/index.js"] diff --git a/backend/src/index.ts b/backend/src/index.ts index e4300da..d01298a 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,14 +1,18 @@ import express from 'express'; import cors from 'cors'; import 'dotenv/config'; +import path from 'path'; +import { fileURLToPath } from 'url'; import { connectDB } from './config/database'; import { initRedis, closeRedis } from './config/redis'; -import { authenticate, optional } from './middleware/auth'; -import { errorHandler, notFoundHandler } from './middleware/errorHandler'; +import { errorHandler } from './middleware/errorHandler'; import authRoutes from './routes/authRoutes'; import podcastRoutes from './routes/podcastRoutes'; import episodeRoutes from './routes/episodeRoutes'; +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + const app = express(); const PORT = process.env.PORT || 5000; @@ -48,8 +52,19 @@ app.use('/api/auth', authRoutes); app.use('/api/podcasts', podcastRoutes); app.use('/api/episodes', episodeRoutes); -// 404 handler -app.use(notFoundHandler); +// Serve frontend static files in production +const frontendDist = path.join(__dirname, '..', 'public'); +app.use(express.static(frontendDist)); + +// SPA fallback — return index.html for any non-API route +app.get('*', (req, res) => { + const indexFile = path.join(frontendDist, 'index.html'); + res.sendFile(indexFile, (err) => { + if (err) { + res.status(404).json({ success: false, message: 'Not found' }); + } + }); +}); // Error handler (must be last) app.use(errorHandler); diff --git a/docker-compose.yml b/docker-compose.yml index cf3047d..ffce9d4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,16 +19,17 @@ services: networks: - podcastic - backend: + app: build: - context: ./backend + context: . dockerfile: Dockerfile - container_name: podcastic-backend + container_name: podcastic restart: unless-stopped ports: - - "5000:5000" + - "3579:3579" environment: NODE_ENV: production + PORT: 3579 MONGODB_URI: mongodb://mongodb:27017/podcastic REDIS_URL: redis://redis:6379 JWT_SECRET: ${JWT_SECRET:-change_me_in_production} @@ -42,19 +43,6 @@ services: networks: - podcastic - frontend: - build: - context: ./frontend - dockerfile: Dockerfile - container_name: podcastic-frontend - restart: unless-stopped - ports: - - "3579:3579" - depends_on: - - backend - networks: - - podcastic - volumes: mongodb_data: redis_data: diff --git a/frontend/Dockerfile b/frontend/Dockerfile deleted file mode 100644 index ad73eb7..0000000 --- a/frontend/Dockerfile +++ /dev/null @@ -1,34 +0,0 @@ -FROM node:20-alpine AS builder - -WORKDIR /app - -COPY package*.json ./ -RUN npm ci - -COPY . . - -RUN npm run build - -FROM nginx:alpine - -COPY --from=builder /app/dist /usr/share/nginx/html - -COPY <<'EOF' /etc/nginx/conf.d/default.conf -server { - listen 3579; - server_name _; - root /usr/share/nginx/html; - - location / { - try_files $uri $uri/ /index.html; - } - - location /api { - proxy_pass http://backend:5000; - } -} -EOF - -EXPOSE 3579 - -CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 3484b42..b69649a 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -1,6 +1,6 @@ import axios from 'axios'; -const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:5000/api'; +const API_URL = import.meta.env.VITE_API_URL || '/api'; const api = axios.create({ baseURL: API_URL, diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 1714950..8eff290 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -10,7 +10,6 @@ export default defineConfig({ '/api': { target: 'http://localhost:5000', changeOrigin: true, - rewrite: (path) => path.replace(/^\/api/, '') } } },