mirror of
https://github.com/xpltdco/media-rip.git
synced 2026-04-03 02:53:58 -06:00
R021/R022/R026: Docker, CI/CD, deployment example
Dockerfile (multi-stage): - Stage 1: Node 22 builds frontend (npm ci + npm run build) - Stage 2: Python 3.12 installs backend deps - Stage 3: Slim runtime with ffmpeg + deno (yt-dlp needs both) - Non-root user (mediarip), healthcheck, PYTHONUNBUFFERED - Volumes: /downloads (media), /data (SQLite DB) docker-compose.example.yml: - Caddy reverse proxy with automatic TLS via Let's Encrypt - Separate Caddyfile.example for domain configuration - Health-dependent startup ordering - Environment variables for admin setup CI/CD (.github/workflows/): - ci.yml: backend lint+test, frontend typecheck+test, Docker smoke build. Runs on PRs and pushes to main. - publish.yml: multi-platform build (amd64+arm64), pushes to ghcr.io/xpltd/media-rip on v*.*.* tags. Semantic version tags (v1.0.0 → latest + 1.0.0 + 1.0 + 1). Auto GitHub Release. .dockerignore: excludes dev artifacts, .gsd/, node_modules/, .venv/
This commit is contained in:
parent
cbaec9ad36
commit
c9ad4fc5d0
7 changed files with 202 additions and 174 deletions
|
|
@ -1,37 +1,33 @@
|
||||||
# Dependencies
|
# Ignore Python / build artifacts
|
||||||
node_modules/
|
|
||||||
.venv/
|
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
.venv/
|
||||||
# Build artifacts
|
venv/
|
||||||
frontend/dist/
|
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
# Development files
|
# Ignore Node artifacts
|
||||||
.git/
|
node_modules/
|
||||||
|
frontend/dist/
|
||||||
|
|
||||||
|
# Ignore dev/test files
|
||||||
.gsd/
|
.gsd/
|
||||||
.planning/
|
.bg-shell/
|
||||||
.github/
|
*.log
|
||||||
.vscode/
|
coverage/
|
||||||
*.md
|
.cache/
|
||||||
!README.md
|
tmp/
|
||||||
|
|
||||||
# Test files
|
|
||||||
backend/tests/
|
|
||||||
frontend/src/tests/
|
|
||||||
frontend/vitest.config.ts
|
|
||||||
|
|
||||||
# OS files
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
|
|
||||||
# Docker
|
|
||||||
docker-compose*.yml
|
|
||||||
Dockerfile
|
|
||||||
.dockerignore
|
|
||||||
|
|
||||||
# Misc
|
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
*.log
|
|
||||||
|
# Ignore git
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Ignore IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.code-workspace
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
|
||||||
73
.github/workflows/ci.yml
vendored
73
.github/workflows/ci.yml
vendored
|
|
@ -1,18 +1,15 @@
|
||||||
|
# CI — runs on all PRs and pushes to main
|
||||||
name: CI
|
name: CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
|
||||||
branches: [main, master]
|
|
||||||
push:
|
push:
|
||||||
branches: [main, master]
|
branches: [main, master]
|
||||||
|
pull_request:
|
||||||
concurrency:
|
branches: [main, master]
|
||||||
group: ci-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
backend:
|
backend:
|
||||||
name: Backend (Python)
|
name: Backend (lint + test)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
@ -21,69 +18,55 @@ jobs:
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
cache: pip
|
cache: pip
|
||||||
cache-dependency-path: backend/requirements.txt
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
working-directory: backend
|
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
cd backend
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
pip install pytest pytest-asyncio pytest-anyio httpx ruff
|
pip install ruff pytest pytest-asyncio httpx anyio[trio]
|
||||||
|
|
||||||
- name: Lint (ruff)
|
- name: Lint
|
||||||
working-directory: backend
|
run: cd backend && ruff check .
|
||||||
run: ruff check app/
|
|
||||||
|
|
||||||
- name: Type check (optional)
|
- name: Test
|
||||||
working-directory: backend
|
run: cd backend && python -m pytest tests/ -q -m "not integration"
|
||||||
continue-on-error: true
|
|
||||||
run: ruff check app/ --select=E,W,F
|
|
||||||
|
|
||||||
- name: Test (pytest)
|
|
||||||
working-directory: backend
|
|
||||||
run: python -m pytest tests/ -v --tb=short
|
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
name: Frontend (Vue 3)
|
name: Frontend (typecheck + test)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "20"
|
node-version: "22"
|
||||||
cache: npm
|
cache: npm
|
||||||
cache-dependency-path: frontend/package-lock.json
|
cache-dependency-path: frontend/package-lock.json
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
working-directory: frontend
|
run: cd frontend && npm ci
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Type check (vue-tsc)
|
- name: Typecheck
|
||||||
working-directory: frontend
|
run: cd frontend && npx vue-tsc --noEmit
|
||||||
run: npx vue-tsc --noEmit
|
|
||||||
|
|
||||||
- name: Test (vitest)
|
- name: Test
|
||||||
working-directory: frontend
|
run: cd frontend && npx vitest run
|
||||||
run: npx vitest run
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
working-directory: frontend
|
|
||||||
run: npm run build
|
|
||||||
|
|
||||||
docker:
|
docker:
|
||||||
name: Docker Build
|
name: Docker build (smoke test)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [backend, frontend]
|
needs: [backend, frontend]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Build image
|
- name: Set up Docker Buildx
|
||||||
run: docker build -t media-rip:ci .
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Smoke test
|
- name: Build image
|
||||||
run: |
|
uses: docker/build-push-action@v6
|
||||||
docker run -d --name mediarip-test -p 8000:8000 media-rip:ci
|
with:
|
||||||
sleep 5
|
context: .
|
||||||
curl -f http://localhost:8000/api/health
|
push: false
|
||||||
docker stop mediarip-test
|
tags: media-rip:ci-test
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
|
||||||
61
.github/workflows/publish.yml
vendored
Normal file
61
.github/workflows/publish.yml
vendored
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
# Publish — builds and pushes Docker image on version tags
|
||||||
|
name: Publish
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: ["v*.*.*"]
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: xpltd/media-rip
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
name: Build & push image
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up QEMU (for arm64)
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to GHCR
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=semver,pattern={{major}}
|
||||||
|
type=raw,value=latest,enable={{is_default_branch}}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
- name: Create GitHub Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
generate_release_notes: true
|
||||||
4
Caddyfile.example
Normal file
4
Caddyfile.example
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
# Replace YOUR_DOMAIN with your actual domain
|
||||||
|
YOUR_DOMAIN {
|
||||||
|
reverse_proxy media-rip:8000
|
||||||
|
}
|
||||||
109
Dockerfile
109
Dockerfile
|
|
@ -1,93 +1,66 @@
|
||||||
# media.rip() Docker Build
|
# media.rip() — multi-stage Docker build
|
||||||
|
# Stage 1: Build frontend (Node)
|
||||||
|
# Stage 2: Install backend deps (Python)
|
||||||
|
# Stage 3: Slim runtime with ffmpeg
|
||||||
#
|
#
|
||||||
# Multi-stage build:
|
# Image: ghcr.io/xpltd/media-rip
|
||||||
# 1. frontend-build: Install npm deps + build Vue 3 SPA
|
# Platforms: linux/amd64, linux/arm64
|
||||||
# 2. backend-deps: Install Python deps into a virtual env
|
|
||||||
# 3. runtime: Copy built assets + venv into minimal image
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# docker build -t media-rip .
|
|
||||||
# docker run -p 8080:8000 -v ./downloads:/downloads media-rip
|
|
||||||
|
|
||||||
# ══════════════════════════════════════════
|
# ── Stage 1: Frontend build ──────────────────────────────────────────
|
||||||
# Stage 1: Build frontend
|
FROM node:22-slim AS frontend-builder
|
||||||
# ══════════════════════════════════════════
|
|
||||||
FROM node:20-slim AS frontend-build
|
|
||||||
|
|
||||||
WORKDIR /build
|
|
||||||
COPY frontend/package.json frontend/package-lock.json ./
|
|
||||||
RUN npm ci --no-audit --no-fund
|
|
||||||
|
|
||||||
|
WORKDIR /build/frontend
|
||||||
|
COPY frontend/package.json frontend/package-lock.json* ./
|
||||||
|
RUN npm ci --ignore-scripts
|
||||||
COPY frontend/ ./
|
COPY frontend/ ./
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# ══════════════════════════════════════════
|
# ── Stage 2: Python dependencies ─────────────────────────────────────
|
||||||
# Stage 2: Install Python dependencies
|
FROM python:3.12-slim AS python-deps
|
||||||
# ══════════════════════════════════════════
|
|
||||||
FROM python:3.12-slim AS backend-deps
|
|
||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
# Install build tools needed for some pip packages (bcrypt, etc.)
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
gcc \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
COPY backend/requirements.txt ./
|
COPY backend/requirements.txt ./
|
||||||
RUN python -m venv /opt/venv && \
|
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
|
||||||
/opt/venv/bin/pip install --no-cache-dir -r requirements.txt
|
|
||||||
|
|
||||||
# ══════════════════════════════════════════
|
# ── Stage 3: Runtime ─────────────────────────────────────────────────
|
||||||
# Stage 3: Runtime image
|
|
||||||
# ══════════════════════════════════════════
|
|
||||||
FROM python:3.12-slim AS runtime
|
FROM python:3.12-slim AS runtime
|
||||||
|
|
||||||
LABEL org.opencontainers.image.title="media.rip()"
|
# Install ffmpeg (required by yt-dlp for muxing/transcoding)
|
||||||
LABEL org.opencontainers.image.description="Self-hostable yt-dlp web frontend"
|
# Install deno (required by yt-dlp for YouTube JS interpretation)
|
||||||
LABEL org.opencontainers.image.source="https://github.com/jlightner/media-rip"
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends ffmpeg curl unzip && \
|
||||||
|
curl -fsSL https://deno.land/install.sh | DENO_INSTALL=/usr/local sh && \
|
||||||
|
apt-get purge -y curl unzip && \
|
||||||
|
apt-get autoremove -y && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install runtime dependencies only
|
# Copy Python packages from deps stage
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
COPY --from=python-deps /install /usr/local
|
||||||
ffmpeg \
|
|
||||||
curl \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Install yt-dlp (latest stable)
|
# Create non-root user
|
||||||
RUN pip install --no-cache-dir yt-dlp
|
RUN useradd --create-home --shell /bin/bash mediarip
|
||||||
|
|
||||||
# Copy virtual env from deps stage
|
# Application code
|
||||||
COPY --from=backend-deps /opt/venv /opt/venv
|
|
||||||
ENV PATH="/opt/venv/bin:$PATH"
|
|
||||||
|
|
||||||
# Set up application directory
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
COPY backend/ ./
|
||||||
|
|
||||||
# Copy backend source
|
# Copy built frontend into backend static dir
|
||||||
COPY backend/app ./app
|
COPY --from=frontend-builder /build/frontend/dist ./static
|
||||||
|
|
||||||
# Copy built frontend into static serving directory
|
# Create default directories
|
||||||
COPY --from=frontend-build /build/dist ./static
|
RUN mkdir -p /downloads /data && \
|
||||||
|
chown -R mediarip:mediarip /app /downloads /data
|
||||||
|
|
||||||
# Create directories for runtime data
|
USER mediarip
|
||||||
RUN mkdir -p /downloads /themes /data
|
|
||||||
|
|
||||||
# Default environment
|
# Environment defaults
|
||||||
ENV MEDIARIP__SERVER__HOST=0.0.0.0 \
|
ENV MEDIARIP__DOWNLOADS__OUTPUT_DIR=/downloads \
|
||||||
MEDIARIP__SERVER__PORT=8000 \
|
MEDIARIP__DATABASE__PATH=/data/mediarip.db \
|
||||||
MEDIARIP__SERVER__DB_PATH=/data/mediarip.db \
|
PYTHONUNBUFFERED=1
|
||||||
MEDIARIP__DOWNLOADS__OUTPUT_DIR=/downloads \
|
|
||||||
MEDIARIP__THEMES_DIR=/themes \
|
|
||||||
PYTHONUNBUFFERED=1 \
|
|
||||||
PYTHONDONTWRITEBYTECODE=1
|
|
||||||
|
|
||||||
# Volumes for persistent data
|
|
||||||
VOLUME ["/downloads", "/themes", "/data"]
|
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
# Health check
|
|
||||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||||
CMD curl -f http://localhost:8000/api/health || exit 1
|
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/health')" || exit 1
|
||||||
|
|
||||||
# Run with uvicorn
|
CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"]
|
|
||||||
|
|
|
||||||
|
|
@ -1,51 +1,62 @@
|
||||||
# media.rip() — Secure Deployment with Caddy (Auto-TLS)
|
# media.rip() — Docker Compose example with Caddy reverse proxy
|
||||||
|
#
|
||||||
|
# This is the recommended deployment configuration.
|
||||||
|
# Caddy automatically provisions TLS certificates via Let's Encrypt.
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# 1. Copy this file to docker-compose.yml
|
# 1. Replace YOUR_DOMAIN with your actual domain
|
||||||
# 2. Copy .env.example to .env and fill in your domain + admin password
|
# 2. Set a strong admin password hash (see below)
|
||||||
# 3. docker compose up -d
|
# 3. Run: docker compose up -d
|
||||||
#
|
#
|
||||||
# Caddy automatically obtains and renews TLS certificates from Let's Encrypt.
|
# Generate a bcrypt password hash:
|
||||||
# The admin panel is protected behind HTTPS with Basic auth.
|
# docker run --rm python:3.12-slim python -c \
|
||||||
|
# "import bcrypt; print(bcrypt.hashpw(b'YOUR_PASSWORD', bcrypt.gensalt()).decode())"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
mediarip:
|
media-rip:
|
||||||
image: ghcr.io/jlightner/media-rip:latest
|
image: ghcr.io/xpltd/media-rip:latest
|
||||||
volumes:
|
container_name: media-rip
|
||||||
- ./downloads:/downloads
|
|
||||||
- ./themes:/themes
|
|
||||||
- mediarip-data:/data
|
|
||||||
environment:
|
|
||||||
- MEDIARIP__SESSION__MODE=isolated
|
|
||||||
- MEDIARIP__ADMIN__ENABLED=true
|
|
||||||
- MEDIARIP__ADMIN__USERNAME=${ADMIN_USERNAME:-admin}
|
|
||||||
- MEDIARIP__ADMIN__PASSWORD_HASH=${ADMIN_PASSWORD_HASH}
|
|
||||||
- MEDIARIP__PURGE__ENABLED=true
|
|
||||||
- MEDIARIP__PURGE__MAX_AGE_HOURS=168
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- downloads:/downloads
|
||||||
|
- data:/data
|
||||||
|
# Optional: custom themes
|
||||||
|
# - ./themes:/themes:ro
|
||||||
|
# Optional: config file
|
||||||
|
# - ./config.yaml:/app/config.yaml:ro
|
||||||
|
environment:
|
||||||
|
# Admin panel (optional — remove to disable)
|
||||||
|
MEDIARIP__ADMIN__ENABLED: "true"
|
||||||
|
MEDIARIP__ADMIN__USERNAME: "admin"
|
||||||
|
MEDIARIP__ADMIN__PASSWORD_HASH: "${ADMIN_PASSWORD_HASH}"
|
||||||
|
# Session mode: isolated (default), shared, or open
|
||||||
|
# MEDIARIP__SESSION__MODE: "isolated"
|
||||||
|
expose:
|
||||||
|
- "8000"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8000/api/health"]
|
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/health')"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
# Not exposed directly — Caddy handles external traffic
|
start_period: 10s
|
||||||
expose:
|
|
||||||
- "8000"
|
|
||||||
|
|
||||||
caddy:
|
caddy:
|
||||||
image: caddy:2-alpine
|
image: caddy:2-alpine
|
||||||
|
container_name: media-rip-caddy
|
||||||
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
- "443:443"
|
- "443:443"
|
||||||
volumes:
|
volumes:
|
||||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||||
- caddy-data:/data
|
- caddy_data:/data
|
||||||
- caddy-config:/config
|
- caddy_config:/config
|
||||||
restart: unless-stopped
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- mediarip
|
media-rip:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
mediarip-data:
|
downloads:
|
||||||
caddy-data:
|
data:
|
||||||
caddy-config:
|
caddy_data:
|
||||||
|
caddy_config:
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ export default defineConfig({
|
||||||
port: 5173,
|
port: 5173,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://localhost:8000',
|
target: 'http://localhost:8001',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue