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
+157 -480

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_KEY=
PODCAST_INDEX_API_SECRET= PODCAST_INDEX_API_SECRET=
# Frontend Configuration # Frontend Configuration (only needed for local dev without Docker)
VITE_API_URL=http://localhost:5000/api # 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"]
+81 -406
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: This single Docker container includes:
- ✅ **Backend**: Node.js + Express.js API - **Backend**: Node.js + Express.js API
- ✅ **Frontend**: React web interface - **Frontend**: React web interface (served by Express as static files)
- ✅ **Database**: MongoDB (embedded) - **Database**: MongoDB (separate container, managed by Compose)
- ✅ **Cache**: Redis (embedded) - **Cache**: Redis (separate container, managed by Compose)
- ✅ **Web Server**: Nginx reverse proxy
- ✅ **Process Manager**: Supervisor (manages all 5 services)
All in **ONE container** on port **3579**
## Prerequisites ## Prerequisites
- Unraid server with Docker support - Unraid server with Docker support
- ~2GB RAM available - Docker Compose (or Unraid's Compose Manager plugin)
- ~20GB storage for database/media
- Internet connection
## Installation Steps ## Installation
### Step 1: Clone Repository ### Step 1: Clone Repository
```bash ```bash
# Via terminal on Unraid cd /mnt/user/appdata
cd /mnt/user/appdata # or your preferred path
git clone https://github.com/R0m1k3/podcastic.git git clone https://github.com/R0m1k3/podcastic.git
cd podcastic cd podcastic
``` ```
Or download as ZIP from GitHub and extract.
### Step 2: Configure Environment ### Step 2: Configure Environment
```bash ```bash
# Copy template
cp .env.example .env cp .env.example .env
# Edit with your settings
nano .env nano .env
``` ```
**Key settings**: **Change at minimum:**
```env ```env
NODE_ENV=production JWT_SECRET=your-random-secret-here
# 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=
``` ```
### Step 3: Build Docker Image ### Step 3: Build and Start
Option A: Build locally (takes ~5-10 minutes)
```bash ```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) ### Step 4: Access
```bash
# Will be available after first push to Docker Hub Open: `http://your-unraid-ip:3579`
# docker pull r0m1k3/podcastic:latest
---
## 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 ```bash
# Start
docker-compose -f docker-compose.unraid.yml up -d
# View logs # View logs
docker-compose -f docker-compose.unraid.yml logs -f podcastic docker-compose logs -f app
# Check status # Restart
docker ps | grep podcastic docker-compose restart app
```
### Step 5: Access Application # Stop all
docker-compose down
Open browser: # Update
```
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
git pull git pull
docker-compose build --no-cache
# Rebuild and restart docker-compose up -d
docker-compose -f docker-compose.unraid.yml build --no-cache
docker-compose -f docker-compose.unraid.yml 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 ```bash
# Check logs # Check container status
docker logs podcastic docker-compose ps
# Common issues: # Check health endpoint
# - 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
curl http://localhost:3579/health 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 **Last Updated**: 2026-04-16
**Tested on**: Unraid 6.12+
-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 express from 'express';
import cors from 'cors'; import cors from 'cors';
import 'dotenv/config'; import 'dotenv/config';
import path from 'path';
import { fileURLToPath } from 'url';
import { connectDB } from './config/database'; import { connectDB } from './config/database';
import { initRedis, closeRedis } from './config/redis'; import { initRedis, closeRedis } from './config/redis';
import { authenticate, optional } from './middleware/auth'; import { errorHandler } from './middleware/errorHandler';
import { errorHandler, notFoundHandler } from './middleware/errorHandler';
import authRoutes from './routes/authRoutes'; import authRoutes from './routes/authRoutes';
import podcastRoutes from './routes/podcastRoutes'; import podcastRoutes from './routes/podcastRoutes';
import episodeRoutes from './routes/episodeRoutes'; import episodeRoutes from './routes/episodeRoutes';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express(); const app = express();
const PORT = process.env.PORT || 5000; const PORT = process.env.PORT || 5000;
@@ -48,8 +52,19 @@ app.use('/api/auth', authRoutes);
app.use('/api/podcasts', podcastRoutes); app.use('/api/podcasts', podcastRoutes);
app.use('/api/episodes', episodeRoutes); app.use('/api/episodes', episodeRoutes);
// 404 handler // Serve frontend static files in production
app.use(notFoundHandler); 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) // Error handler (must be last)
app.use(errorHandler); app.use(errorHandler);
+5 -17
View File
@@ -19,16 +19,17 @@ services:
networks: networks:
- podcastic - podcastic
backend: app:
build: build:
context: ./backend context: .
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: podcastic-backend container_name: podcastic
restart: unless-stopped restart: unless-stopped
ports: ports:
- "5000:5000" - "3579:3579"
environment: environment:
NODE_ENV: production NODE_ENV: production
PORT: 3579
MONGODB_URI: mongodb://mongodb:27017/podcastic MONGODB_URI: mongodb://mongodb:27017/podcastic
REDIS_URL: redis://redis:6379 REDIS_URL: redis://redis:6379
JWT_SECRET: ${JWT_SECRET:-change_me_in_production} JWT_SECRET: ${JWT_SECRET:-change_me_in_production}
@@ -42,19 +43,6 @@ services:
networks: networks:
- podcastic - podcastic
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
container_name: podcastic-frontend
restart: unless-stopped
ports:
- "3579:3579"
depends_on:
- backend
networks:
- podcastic
volumes: volumes:
mongodb_data: mongodb_data:
redis_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'; 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({ const api = axios.create({
baseURL: API_URL, baseURL: API_URL,
-1
View File
@@ -10,7 +10,6 @@ export default defineConfig({
'/api': { '/api': {
target: 'http://localhost:5000', target: 'http://localhost:5000',
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
} }
} }
}, },