mirror of
https://github.com/xpltdco/media-rip.git
synced 2026-04-02 18:43:59 -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
|
||||
node_modules/
|
||||
.venv/
|
||||
# Ignore Python / build artifacts
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Build artifacts
|
||||
frontend/dist/
|
||||
.venv/
|
||||
venv/
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Development files
|
||||
.git/
|
||||
# Ignore Node artifacts
|
||||
node_modules/
|
||||
frontend/dist/
|
||||
|
||||
# Ignore dev/test files
|
||||
.gsd/
|
||||
.planning/
|
||||
.github/
|
||||
.vscode/
|
||||
*.md
|
||||
!README.md
|
||||
|
||||
# Test files
|
||||
backend/tests/
|
||||
frontend/src/tests/
|
||||
frontend/vitest.config.ts
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Docker
|
||||
docker-compose*.yml
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
|
||||
# Misc
|
||||
.bg-shell/
|
||||
*.log
|
||||
coverage/
|
||||
.cache/
|
||||
tmp/
|
||||
.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
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main, master]
|
||||
push:
|
||||
branches: [main, master]
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
pull_request:
|
||||
branches: [main, master]
|
||||
|
||||
jobs:
|
||||
backend:
|
||||
name: Backend (Python)
|
||||
name: Backend (lint + test)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
|
@ -21,69 +18,55 @@ jobs:
|
|||
with:
|
||||
python-version: "3.12"
|
||||
cache: pip
|
||||
cache-dependency-path: backend/requirements.txt
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: backend
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
cd backend
|
||||
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)
|
||||
working-directory: backend
|
||||
run: ruff check app/
|
||||
- name: Lint
|
||||
run: cd backend && ruff check .
|
||||
|
||||
- name: Type check (optional)
|
||||
working-directory: backend
|
||||
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
|
||||
- name: Test
|
||||
run: cd backend && python -m pytest tests/ -q -m "not integration"
|
||||
|
||||
frontend:
|
||||
name: Frontend (Vue 3)
|
||||
name: Frontend (typecheck + test)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version: "22"
|
||||
cache: npm
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: frontend
|
||||
run: npm ci
|
||||
run: cd frontend && npm ci
|
||||
|
||||
- name: Type check (vue-tsc)
|
||||
working-directory: frontend
|
||||
run: npx vue-tsc --noEmit
|
||||
- name: Typecheck
|
||||
run: cd frontend && npx vue-tsc --noEmit
|
||||
|
||||
- name: Test (vitest)
|
||||
working-directory: frontend
|
||||
run: npx vitest run
|
||||
|
||||
- name: Build
|
||||
working-directory: frontend
|
||||
run: npm run build
|
||||
- name: Test
|
||||
run: cd frontend && npx vitest run
|
||||
|
||||
docker:
|
||||
name: Docker Build
|
||||
name: Docker build (smoke test)
|
||||
runs-on: ubuntu-latest
|
||||
needs: [backend, frontend]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Build image
|
||||
run: docker build -t media-rip:ci .
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Smoke test
|
||||
run: |
|
||||
docker run -d --name mediarip-test -p 8000:8000 media-rip:ci
|
||||
sleep 5
|
||||
curl -f http://localhost:8000/api/health
|
||||
docker stop mediarip-test
|
||||
- name: Build image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
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:
|
||||
# 1. frontend-build: Install npm deps + build Vue 3 SPA
|
||||
# 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
|
||||
# Image: ghcr.io/xpltd/media-rip
|
||||
# Platforms: linux/amd64, linux/arm64
|
||||
|
||||
# ══════════════════════════════════════════
|
||||
# Stage 1: Build frontend
|
||||
# ══════════════════════════════════════════
|
||||
FROM node:20-slim AS frontend-build
|
||||
|
||||
WORKDIR /build
|
||||
COPY frontend/package.json frontend/package-lock.json ./
|
||||
RUN npm ci --no-audit --no-fund
|
||||
# ── Stage 1: Frontend build ──────────────────────────────────────────
|
||||
FROM node:22-slim AS frontend-builder
|
||||
|
||||
WORKDIR /build/frontend
|
||||
COPY frontend/package.json frontend/package-lock.json* ./
|
||||
RUN npm ci --ignore-scripts
|
||||
COPY frontend/ ./
|
||||
RUN npm run build
|
||||
|
||||
# ══════════════════════════════════════════
|
||||
# Stage 2: Install Python dependencies
|
||||
# ══════════════════════════════════════════
|
||||
FROM python:3.12-slim AS backend-deps
|
||||
# ── Stage 2: Python dependencies ─────────────────────────────────────
|
||||
FROM python:3.12-slim AS python-deps
|
||||
|
||||
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 ./
|
||||
RUN python -m venv /opt/venv && \
|
||||
/opt/venv/bin/pip install --no-cache-dir -r requirements.txt
|
||||
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
|
||||
|
||||
# ══════════════════════════════════════════
|
||||
# Stage 3: Runtime image
|
||||
# ══════════════════════════════════════════
|
||||
# ── Stage 3: Runtime ─────────────────────────────────────────────────
|
||||
FROM python:3.12-slim AS runtime
|
||||
|
||||
LABEL org.opencontainers.image.title="media.rip()"
|
||||
LABEL org.opencontainers.image.description="Self-hostable yt-dlp web frontend"
|
||||
LABEL org.opencontainers.image.source="https://github.com/jlightner/media-rip"
|
||||
# Install ffmpeg (required by yt-dlp for muxing/transcoding)
|
||||
# Install deno (required by yt-dlp for YouTube JS interpretation)
|
||||
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
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ffmpeg \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
# Copy Python packages from deps stage
|
||||
COPY --from=python-deps /install /usr/local
|
||||
|
||||
# Install yt-dlp (latest stable)
|
||||
RUN pip install --no-cache-dir yt-dlp
|
||||
# Create non-root user
|
||||
RUN useradd --create-home --shell /bin/bash mediarip
|
||||
|
||||
# Copy virtual env from deps stage
|
||||
COPY --from=backend-deps /opt/venv /opt/venv
|
||||
ENV PATH="/opt/venv/bin:$PATH"
|
||||
|
||||
# Set up application directory
|
||||
# Application code
|
||||
WORKDIR /app
|
||||
COPY backend/ ./
|
||||
|
||||
# Copy backend source
|
||||
COPY backend/app ./app
|
||||
# Copy built frontend into backend static dir
|
||||
COPY --from=frontend-builder /build/frontend/dist ./static
|
||||
|
||||
# Copy built frontend into static serving directory
|
||||
COPY --from=frontend-build /build/dist ./static
|
||||
# Create default directories
|
||||
RUN mkdir -p /downloads /data && \
|
||||
chown -R mediarip:mediarip /app /downloads /data
|
||||
|
||||
# Create directories for runtime data
|
||||
RUN mkdir -p /downloads /themes /data
|
||||
USER mediarip
|
||||
|
||||
# Default environment
|
||||
ENV MEDIARIP__SERVER__HOST=0.0.0.0 \
|
||||
MEDIARIP__SERVER__PORT=8000 \
|
||||
MEDIARIP__SERVER__DB_PATH=/data/mediarip.db \
|
||||
MEDIARIP__DOWNLOADS__OUTPUT_DIR=/downloads \
|
||||
MEDIARIP__THEMES_DIR=/themes \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PYTHONDONTWRITEBYTECODE=1
|
||||
|
||||
# Volumes for persistent data
|
||||
VOLUME ["/downloads", "/themes", "/data"]
|
||||
# Environment defaults
|
||||
ENV MEDIARIP__DOWNLOADS__OUTPUT_DIR=/downloads \
|
||||
MEDIARIP__DATABASE__PATH=/data/mediarip.db \
|
||||
PYTHONUNBUFFERED=1
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
# Health check
|
||||
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 ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"]
|
||||
CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
# 1. Copy this file to docker-compose.yml
|
||||
# 2. Copy .env.example to .env and fill in your domain + admin password
|
||||
# 3. docker compose up -d
|
||||
# 1. Replace YOUR_DOMAIN with your actual domain
|
||||
# 2. Set a strong admin password hash (see below)
|
||||
# 3. Run: docker compose up -d
|
||||
#
|
||||
# Caddy automatically obtains and renews TLS certificates from Let's Encrypt.
|
||||
# The admin panel is protected behind HTTPS with Basic auth.
|
||||
# Generate a bcrypt password hash:
|
||||
# docker run --rm python:3.12-slim python -c \
|
||||
# "import bcrypt; print(bcrypt.hashpw(b'YOUR_PASSWORD', bcrypt.gensalt()).decode())"
|
||||
|
||||
services:
|
||||
mediarip:
|
||||
image: ghcr.io/jlightner/media-rip:latest
|
||||
volumes:
|
||||
- ./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
|
||||
media-rip:
|
||||
image: ghcr.io/xpltd/media-rip:latest
|
||||
container_name: media-rip
|
||||
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:
|
||||
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
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
# Not exposed directly — Caddy handles external traffic
|
||||
expose:
|
||||
- "8000"
|
||||
start_period: 10s
|
||||
|
||||
caddy:
|
||||
image: caddy:2-alpine
|
||||
container_name: media-rip-caddy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- caddy-data:/data
|
||||
- caddy-config:/config
|
||||
restart: unless-stopped
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
depends_on:
|
||||
- mediarip
|
||||
media-rip:
|
||||
condition: service_healthy
|
||||
|
||||
volumes:
|
||||
mediarip-data:
|
||||
caddy-data:
|
||||
caddy-config:
|
||||
downloads:
|
||||
data:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export default defineConfig({
|
|||
port: 5173,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8000',
|
||||
target: 'http://localhost:8001',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue