diff --git a/Dockerfile b/Dockerfile index 421393d..ff4895e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,28 +1,16 @@ FROM node:24-alpine AS frontend-build WORKDIR /frontend -ARG VERSION -ARG BASE_PATH="" COPY web/package*.json ./ RUN npm ci && npm cache clean --force COPY web/ ./ -RUN env PUBLIC_VERSION=${VERSION} PUBLIC_API_URL=${BASE_PATH} BASE_PATH=${BASE_PATH}/web npm run build -FROM ghcr.io/astral-sh/uv:python3.13-trixie-slim ARG VERSION ARG BASE_PATH="" -LABEL author="github.com/maxdorninger" -LABEL version=${VERSION} -LABEL description="Docker image for MediaManager" +RUN env PUBLIC_VERSION=${VERSION} PUBLIC_API_URL=${BASE_PATH} BASE_PATH=${BASE_PATH}/web npm run build -ENV PUBLIC_VERSION=${VERSION} \ - CONFIG_DIR="/app/config"\ - BASE_PATH=${BASE_PATH}\ - FRONTEND_FILES_DIR="/app/web/build" - - -WORKDIR /app +FROM ghcr.io/astral-sh/uv:python3.13-trixie-slim AS base RUN apt-get update && \ apt-get install -y ca-certificates bash libtorrent21 gcc bc locales postgresql media-types mailcap curl gzip unzip tar 7zip bzip2 unar && \ @@ -34,8 +22,24 @@ RUN locale-gen ENV LANG=en_US.UTF-8 ENV LC_ALL=en_US.UTF-8 +FROM base AS dependencies +WORKDIR /app + COPY pyproject.toml uv.lock ./ -RUN uv sync --locked +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync --locked + +FROM dependencies AS app +ARG VERSION +ARG BASE_PATH="" +LABEL author="github.com/maxdorninger" +LABEL version=${VERSION} +LABEL description="Docker image for MediaManager" + +ENV PUBLIC_VERSION=${VERSION} \ + CONFIG_DIR="/app/config"\ + BASE_PATH=${BASE_PATH}\ + FRONTEND_FILES_DIR="/app/web/build" COPY --chmod=755 mediamanager-startup.sh . COPY config.example.toml . @@ -43,8 +47,9 @@ COPY media_manager ./media_manager COPY alembic ./alembic COPY alembic.ini . -COPY --from=frontend-build /frontend/build /app/web/build - HEALTHCHECK CMD curl -f http://localhost:8000${BASE_PATH}/api/v1/health || exit 1 EXPOSE 8000 CMD ["/app/mediamanager-startup.sh"] + +FROM app AS production +COPY --from=frontend-build /frontend/build /app/web/build \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..42bdf96 --- /dev/null +++ b/Makefile @@ -0,0 +1,49 @@ +SHELL := /bin/bash + +# Docker Compose command (override if needed) +COMPOSE ?= docker compose +DC := $(COMPOSE) -f docker-compose.dev.yaml + +# Log args passthrough, e.g.: +# make logs ARGS="--follow --tail=100" +ARGS ?= + +# Service names (override if your compose uses different names) +APP_SVC ?= mediamanager +FRONTEND_SVC ?= frontend + +.PHONY: help up down logs ps restartapp frontend + +help: + @echo "Usage:" + @echo " All commands run using the dev docker compose file ($(DEV_FILE))" + @echo "" + @echo " make up # Development environment up, runs with --build flag to rebuild if necessary" + @echo " make down # Development environment down" + @echo " make logs ARGS=\"...\" # (Optional) Set ARGS like \"--follow --tail=100\"" + @echo " make ps | restart # Check status or restart containers" + @echo " make app # Shell into $(APP_SVC) container" + @echo " make frontend # Shell into $(FRONTEND_SVC) container" + +# Core lifecycle +up: + $(DC) up -d --build + +down: + $(DC) down + +logs: + $(DC) logs $(ARGS) + +ps: + $(DC) ps + +restart: + $(DC) restart + +# Interactive shells (prefer bash, fallback to sh) +app: + @$(DC) exec -it $(APP_SVC) bash 2>/dev/null || $(DC) exec -it $(APP_SVC) sh + +frontend: + @$(DC) exec -it $(FRONTEND_SVC) bash 2>/dev/null || $(DC) exec -it $(FRONTEND_SVC) sh \ No newline at end of file diff --git a/Writerside/topics/developer-guide.md b/Writerside/topics/developer-guide.md index 8036fe8..d16ae74 100644 --- a/Writerside/topics/developer-guide.md +++ b/Writerside/topics/developer-guide.md @@ -11,13 +11,28 @@ This section is for those who want to contribute to Media Manager or understand ## Special Dev Configuration -#### Env Variables +### Environment Variables -- `BASE_PATH`: this sets the base path for the app (can be set for both backend and frontend) -- `PUBLIC_VERSION`: this sets the version variable, it is displayed in the frontend (requires rebuilding of the - frontend) and in the /api/v1/health endpoint (can be set for both backend and frontend) -- `FRONTEND_FILES_DIR`: directory for frontend files, e.g. in Docker container it is `/app/web/build` (only backend) -- `MEDIAMANAGER_MISC__DEVELOPMENT`: If set to `TRUE`, enables hot reloading of FastAPI (only when using the docker container) +MediaManager uses various environment variables for configuration. In the Docker development setup (`docker-compose.dev.yaml`), most of these are automatically configured for you. + +#### Backend Variables +- `BASE_PATH`: Sets the base path for the app (e.g., for subdirectory deployments) +- `PUBLIC_VERSION`: Version string displayed in `/api/v1/health` endpoint +- `FRONTEND_FILES_DIR`: Directory for built frontend files (e.g., `/app/web/build` in Docker) +- `MEDIAMANAGER_MISC__DEVELOPMENT`: When set to `TRUE`, enables FastAPI hot-reloading in Docker + +#### Frontend Variables +- `PUBLIC_API_URL`: API URL for backend communication (automatically configured via Vite proxy in Docker) +- `PUBLIC_VERSION`: Version string displayed in the frontend UI +- `BASE_PATH`: Base path for frontend routing (matches backend BASE_PATH) + +#### Docker Development Variables +- `DISABLE_FRONTEND_MOUNT`: When `TRUE`, disables mounting built frontend files (allows separate frontend container) + - **Note:** This is automatically set in `docker-compose.dev.yaml` to enable the separate frontend development container + +#### Configuration Files +- Backend: `res/config/config.toml` (created from `config.dev.toml`) +- Frontend: `web/.env` (created from `.env.example`) ## Contributing @@ -44,29 +59,68 @@ features. - VirtualKit - Writerside (for writing documentation) -### Other recommendations +### Recommended Development Workflow -I recommend developing using Docker, i.e. you can use the provided `docker-compose.dev.yaml` file. This dev -docker-compose file has the `./media_manager` directory mounted at `/app/media_manager` in the container, meaning you -can run the code using the container in exactly the environment it will be running in. +The **recommended way** to develop MediaManager is using the fully Dockerized setup with `docker-compose.dev.yaml`. +This ensures you're working in the same environment as production and makes it easy for new contributors to get started without installing Python, Node.js, or other dependencies locally. -Additionally, to develop the frontend I use a locally installed Node.js server. So basically a hybrid approach, where -the backend runs in a container and the frontend runs on Windows. To make this work, you need to make sure the -`cors_urls` and `frontend_url` are set correctly in the backend's config file. +The development environment includes: +- **Backend (FastAPI)** with automatic hot-reloading for Python code changes +- **Frontend (SvelteKit/Vite)** with Hot Module Replacement (HMR) for instant updates +- **Database (PostgreSQL)** pre-configured and ready to use -Unfortunately, a side effect of this setup is that you have to rebuild the Docker image every time when you change the -python dependencies in any way or at least restart the container if you change the code. For a fast-paced development it -may be more convenient to run the backend locally too, because then it supports hot reloading. +**What requires what:** +- **Python code changes (.py files)**: Automatically detected and hot-reloaded, no action needed ✨ +- **Frontend code changes (.svelte, .ts, .css)**: Instant HMR updates in browser, no action needed ✨ +- **Configuration changes (config.toml)**: Live updates via volume mount, no action needed ✨ +- **Backend dependencies (pyproject.toml)**: Require rebuilding: `docker compose -f docker-compose.dev.yaml build mediamanager` +- **Frontend dependencies (package.json)**: Restart frontend container: `docker compose -f docker-compose.dev.yaml restart frontend` +- **Database migrations**: Automatically run on backend container startup -### Setting up the basic development environment with Docker +This approach eliminates the need for container restarts during normal development and provides the best developer experience with instant feedback for code changes. -- Copy the `config.dev.toml` file to `config.toml` in the `./res` directory and edit it to your needs. -- Use the following command to start the development environment with Docker: - ```bash - docker compose -f docker-compose.dev.yaml up -d - ``` +#### How the Frontend Connects to the Backend -### Setting up the backend development environment +In the Docker development setup, the frontend and backend communicate through Vite's proxy configuration: + +- **Frontend runs on**: `http://localhost:5173` (exposed from Docker) +- **Backend runs on**: `http://mediamanager:8000` (Docker internal network) +- **Vite proxy**: Automatically forwards all `/api/*` requests from frontend to backend + +This means when your browser makes a request to `http://localhost:5173/api/v1/tv/shows`, Vite automatically proxies it to `http://mediamanager:8000/api/v1/tv/shows`. The `PUBLIC_API_URL` environment variable is set to use this proxy, so you don't need to configure anything manually. + +### Setting up the full development environment with Docker (Recommended) + +This is the easiest and recommended way to get started. Everything runs in Docker with hot-reloading enabled. + +1. **Copy the example config & .env:** + ```bash + cp config.dev.toml res/config/config.toml + cp web/.env.example web/.env + ``` + +2. **Start all services:** + ```bash + # Recommended: Use make commands for easy development + make up + + # Alternative: Use docker compose directly (if make is not available) + docker compose -f docker-compose.dev.yaml up + ``` + + **Tip:** Run `make help` to see all available development commands including `make down`, `make logs`, `make app` (shell into backend), and more. + +3. **Access the application:** + - Frontend (with HMR): http://localhost:5173 + - Backend API: http://localhost:8000 + - Database: localhost:5432 + +That's it! Now you can edit code and see changes instantly: +- Edit Python files → Backend auto-reloads +- Edit Svelte/TypeScript files → Frontend HMR updates in browser +- Edit config.toml → Changes apply immediately + +### Setting up the backend development environment (Local) 1. Clone the repository 2. cd into repo root @@ -93,18 +147,21 @@ may be more convenient to run the backend locally too, because then it supports 9. run the backend with ```bash - uv run ./media_manager/main.py --reload --port 8000 + uv run fastapi run media_manager/main.py --reload --port 8000 ``` -- format code with `uvx ruff format` -- lint code with `uvx ruff check` +- format code with `uv run ruff format .` +- lint code with `uv run ruff check .` -### Setting up the frontend development environment +### Setting up the frontend development environment (Local, Optional) + +**Note:** Using the Docker setup above is recommended. This section is for those who prefer to run the frontend locally +outside of Docker. 1. Clone the repository 2. cd into repo root 3. cd into `web` directory -4. install Node.js and npm if you haven't already, I +4. Install Node.js and npm if you haven't already, I used [nvm-windows](https://github.com/coreybutler/nvm-windows?tab=readme-ov-file): ```powershell nvm install 24.1.0 @@ -114,11 +171,73 @@ may be more convenient to run the backend locally too, because then it supports ```powershell Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser ``` -5. Install the dependencies with npm: `npm install` -6. Start the frontend development server: `npm run dev` +5. Create a `.env` file in the `web` directory: + ```bash + cp .env.example .env + ``` + Update `PUBLIC_API_URL` if your backend is not at `http://localhost:8000` -- format the code with `npm run format` -- lint the code with `npm run lint` +6. Install the dependencies with npm: `npm install` +7. Start the frontend development server: `npm run dev` + +**Important:** If running frontend locally, make sure to add `http://localhost:5173` to the `cors_urls` in your backend +config file. + +- Format the code with `npm run format` +- Lint the code with `npm run lint` + +## Troubleshooting + +### Common Docker Development Issues + +**Port already in use errors:** +- Check if ports 5173, 8000, or 5432 are already in use: `lsof -i :5173` (macOS/Linux) or `netstat -ano | findstr :5173` (Windows) +- Stop conflicting services or change ports in `docker-compose.dev.yaml` + +**Container not showing code changes:** +- Verify volume mounts are correct in `docker-compose.dev.yaml` +- For backend: Ensure `./media_manager:/app/media_manager` is mounted +- For frontend: Ensure `./web:/app` is mounted +- On Windows: Check that file watching is enabled in Docker Desktop settings + +**Frontend changes not updating:** +- Check that the frontend container is running: `make ps` or `docker compose -f docker-compose.dev.yaml ps` +- Verify Vite's file watching is working (should see HMR updates in browser console) +- Try restarting the frontend container: `docker compose -f docker-compose.dev.yaml restart frontend` + +**Backend changes not reloading:** +- Verify `MEDIAMANAGER_MISC__DEVELOPMENT=TRUE` is set in `docker-compose.dev.yaml` +- Check backend logs: `make logs ARGS="--follow mediamanager"` or `docker compose -f docker-compose.dev.yaml logs -f mediamanager` +- If dependencies changed, rebuild: `docker compose -f docker-compose.dev.yaml build mediamanager` + +**Database migration issues:** +- Migrations run automatically on container startup +- To run manually: `make app` then `uv run alembic upgrade head` +- To create new migration: `make app` then `uv run alembic revision --autogenerate -m "description"` + +**Viewing logs:** +- All services: `make logs` +- Follow logs in real-time: `make logs ARGS="--follow"` +- Specific service: `make logs ARGS="mediamanager --follow"` + +**Interactive debugging:** +- Shell into backend: `make app` (or `docker compose -f docker-compose.dev.yaml exec -it mediamanager bash`) +- Shell into frontend: `make frontend` (or `docker compose -f docker-compose.dev.yaml exec -it frontend sh`) +- Once inside, you can run commands like `uv run alembic upgrade head`, `npm install`, etc. + +**Volume permission issues (Linux):** +- Docker containers may create files as root, causing permission issues +- Solution: Run containers with your user ID or use Docker's `user:` directive +- Alternatively: `sudo chown -R $USER:$USER .` to reclaim ownership + +**Complete reset:** +If all else fails, you can completely reset your development environment: +```bash +make down +docker compose -f docker-compose.dev.yaml down -v # Remove volumes +docker compose -f docker-compose.dev.yaml build --no-cache # Rebuild without cache +make up +``` ## Sequence Diagrams diff --git a/config.dev.toml b/config.dev.toml index 50af4de..eebda7b 100644 --- a/config.dev.toml +++ b/config.dev.toml @@ -8,7 +8,7 @@ [misc] # it's very likely that you need to change this for MediaManager to work frontend_url = "http://localhost:5173/" # note the trailing slash -cors_urls = ["http://localhost:8000", "http://localhost:5173"] # note the lack of a trailing slash +cors_urls = ["http://localhost:8000", "http://localhost:5173", "http://mediamanager:8000"] # note the lack of a trailing slash image_directory = "/data/images" tv_directory = "/data/tv" diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml index e8ea84c..5f49b2e 100644 --- a/docker-compose.dev.yaml +++ b/docker-compose.dev.yaml @@ -15,6 +15,7 @@ services: build: context: . dockerfile: Dockerfile + target: app args: - VERSION=locally-built - BASE_PATH= @@ -24,12 +25,26 @@ services: - "8000:8000" environment: - CONFIG_DIR=/app/config + - MEDIAMANAGER_MISC__DEVELOPMENT=TRUE + - DISABLE_FRONTEND_MOUNT=TRUE volumes: #- ./web/build:/app/web/build # this is only needed to test built frontend when developing frontend - ./res/images/:/data/images/ - ./res/:/data/ - ./res/config/:/app/config/ - ./media_manager:/app/media_manager + frontend: + image: node:24-alpine + container_name: mediamanager-frontend-dev + working_dir: /app + command: sh -c "npm install && npm run dev -- --host 0.0.0.0" + ports: + - "5173:5173" + - "24678:24678" + volumes: + - ./web:/app + depends_on: + - mediamanager # ---------------------------- # Additional services can be uncommented and configured as needed diff --git a/media_manager/main.py b/media_manager/main.py index 4e4d941..8033aef 100644 --- a/media_manager/main.py +++ b/media_manager/main.py @@ -190,6 +190,7 @@ async def lifespan(app: FastAPI): BASE_PATH = os.getenv("BASE_PATH", "") FRONTEND_FILES_DIR = os.getenv("FRONTEND_FILES_DIR") +DISABLE_FRONTEND_MOUNT = os.getenv("DISABLE_FRONTEND_MOUNT", "").lower() in ["true", "1", "yes"] app = FastAPI(lifespan=lifespan, root_path=BASE_PATH) @@ -290,7 +291,16 @@ app.mount( ) app.include_router(api_app) -app.mount("/web", StaticFiles(directory=FRONTEND_FILES_DIR, html=True), name="frontend") + +# ---------------------------- +# Frontend mounting (disabled in development) +# ---------------------------- + +if not DISABLE_FRONTEND_MOUNT: + app.mount("/web", StaticFiles(directory=FRONTEND_FILES_DIR, html=True), name="frontend") + log.info(f"Mounted frontend at /web from {FRONTEND_FILES_DIR}") +else: + log.info("Frontend mounting disabled (DISABLE_FRONTEND_MOUNT is set)") # ---------------------------- # Redirects to frontend @@ -325,7 +335,7 @@ app.add_exception_handler(UniqueViolation, sqlalchemy_integrity_error_handler) @app.exception_handler(404) async def not_found_handler(request, exc): - if any( + if not DISABLE_FRONTEND_MOUNT and any( base_path in ["/web", "/dashboard", "/login"] for base_path in request.url.path ): return FileResponse(f"{FRONTEND_FILES_DIR}/404.html") diff --git a/web/.env.example b/web/.env.example new file mode 100644 index 0000000..11aad28 --- /dev/null +++ b/web/.env.example @@ -0,0 +1,13 @@ +# Frontend Environment Variables +# These are set automatically in docker-compose.dev.yaml for development + +# API URL for backend communication +# Development: http://localhost:5173 +# Production: Built into the static files during Docker build +PUBLIC_API_URL=http://localhost:5173 + +# Base path for routing (leave empty unless using a subdirectory deployment) +BASE_PATH= + +# Version string (automatically set during build) +PUBLIC_VERSION=dev diff --git a/web/vite.config.ts b/web/vite.config.ts index 0359a4a..5522549 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -4,5 +4,21 @@ import { enhancedImages } from '@sveltejs/enhanced-img'; import tailwindcss from '@tailwindcss/vite'; export default defineConfig({ - plugins: [tailwindcss(), enhancedImages(), sveltekit()] + plugins: [tailwindcss(), enhancedImages(), sveltekit()], + server: { + host: '0.0.0.0', // Allow external connections (required for Docker) + port: 5173, + strictPort: true, // Fail if port is already in use + watch: { + usePolling: true, // Required for file watching in Docker on some systems + interval: 100 // Check for changes every 100ms + }, + proxy: { + // Proxy API requests to backend container + '/api': { + target: 'http://mediamanager:8000', + changeOrigin: true + } + } + } });