Improve local dev experience

This commit is contained in:
Tyler Satre
2025-10-31 12:19:23 -04:00
parent 7c7c1b90ea
commit 106190d7ac
8 changed files with 280 additions and 53 deletions

View File

@@ -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

49
Makefile Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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")

13
web/.env.example Normal file
View File

@@ -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

View File

@@ -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
}
}
}
});