fix: single container without nginx — Express serves frontend

- Remove separate backend/ and frontend/ Dockerfiles
- Add root Dockerfile (3-stage: frontend build → backend build → runtime)
- Frontend static files served by Express from /public
- Simplify docker-compose to 3 services: mongodb, redis, app (port 3579)
- Fix vite proxy not to strip /api prefix from paths
- Default API URL to relative /api instead of hardcoded localhost

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Haiku 4.5 committed 2026-04-16 10:43:06 +02:00
1 parent a8787857b5
commit 1822ab1afe
9 files changed
+158 -481

No files matched your search

+3 -2
View File
@@ -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
+48
View File
@@ -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"]
+82 -407
View File
@@ -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
<?xml version="1.0" encoding="utf-8"?>
<Container version="2">
<Name>podcastic</Name>
<Repository>r0m1k3/podcastic:latest</Repository>
<Registry>https://hub.docker.com/</Registry>
<Network>bridge</Network>
<MyIP/>
<Shell>sh</Shell>
<Privileged>false</Privileged>
<Support/>
<Project/>
<Overview/>
<Category/>
<WebUI>http://[IP]:[PORT:3579]</WebUI>
<TemplateURL/>
<Icon/>
<ExtraParams/>
<PostArgs/>
<StaticNetwork>false</StaticNetwork>
<DonateText/>
<DonateLink/>
<Description>Podcastic - Multi-user podcast streaming application</Description>
<Networking>
<Mode>bridge</Mode>
<Publish>
<Port>
<HostPort>3579</HostPort>
<ContainerPort>3579</ContainerPort>
<Protocol>tcp</Protocol>
</Port>
</Publish>
</Networking>
<Data>
<Volume>
<HostDir>/mnt/user/appdata/podcastic/db</HostDir>
<ContainerDir>/data/db</ContainerDir>
<Mode>rw</Mode>
</Volume>
<Volume>
<HostDir>/mnt/user/appdata/podcastic/redis</HostDir>
<ContainerDir>/data/redis</ContainerDir>
<Mode>rw</Mode>
</Volume>
<Volume>
<HostDir>/mnt/user/appdata/podcastic/logs</HostDir>
<ContainerDir>/var/log/supervisor</ContainerDir>
<Mode>rw</Mode>
</Volume>
</Data>
<Environment>
<Variable>
<Name>JWT_SECRET</Name>
<Value>change-me-in-production</Value>
</Variable>
<Variable>
<Name>PODCAST_INDEX_API_KEY</Name>
<Value/>
</Variable>
<Variable>
<Name>PODCAST_INDEX_API_SECRET</Name>
<Value/>
</Variable>
</Environment>
</Container>
```
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
-15
View File
@@ -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"]
+19 -4
View File
@@ -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);
+5 -17
View File
@@ -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:
-34
View File
@@ -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;"]
+1 -1
View File
@@ -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,
-1
View File
@@ -10,7 +10,6 @@ export default defineConfig({
'/api': {
target: 'http://localhost:5000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},