mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-31 02:03:35 -04:00 
			
		
		
		
	refactor/docker-updates (#369)
* convert all images to webp * consolidate docker files * serve images wiith caddy * consolidate docker files * new slim-buster image * set image url * add image path * remove print * set image path correctly * cleanup * caddy proxy path * docs Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
		| @@ -10,6 +10,11 @@ | |||||||
|   encode gzip |   encode gzip | ||||||
|   uri strip_suffix / |   uri strip_suffix / | ||||||
|    |    | ||||||
|  |   handle_path /api/recipes/image/* { | ||||||
|  |     root * /app/data/img/ | ||||||
|  |     file_server | ||||||
|  |   } | ||||||
|  |  | ||||||
|   handle @proxied { |   handle @proxied { | ||||||
|     reverse_proxy http://127.0.0.1:9000  |     reverse_proxy http://127.0.0.1:9000  | ||||||
|   } |   } | ||||||
|   | |||||||
							
								
								
									
										66
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										66
									
								
								Dockerfile
									
									
									
									
									
								
							| @@ -1,3 +1,4 @@ | |||||||
|  | # build | ||||||
| FROM node:lts-alpine as build-stage | FROM node:lts-alpine as build-stage | ||||||
| WORKDIR /app | WORKDIR /app | ||||||
| COPY ./frontend/package*.json ./ | COPY ./frontend/package*.json ./ | ||||||
| @@ -5,50 +6,53 @@ RUN npm install | |||||||
| COPY ./frontend/ . | COPY ./frontend/ . | ||||||
| RUN npm run build | RUN npm run build | ||||||
|  |  | ||||||
| FROM python:3.9-alpine |  | ||||||
|  |  | ||||||
|  |  | ||||||
| RUN apk add --no-cache libxml2-dev \ |  | ||||||
|     libxslt-dev \ |  | ||||||
|     libxml2 caddy \ |  | ||||||
|     libffi-dev \ |  | ||||||
|     python3 \ |  | ||||||
|     python3-dev \ |  | ||||||
|     jpeg-dev \ |  | ||||||
|     lcms2-dev \ |  | ||||||
|     openjpeg-dev \ |  | ||||||
|     zlib-dev |  | ||||||
|  |  | ||||||
|  | FROM python:3.9-slim-buster | ||||||
|  |  | ||||||
| ENV PRODUCTION true | ENV PRODUCTION true | ||||||
| EXPOSE 80 | ENV POETRY_VERSION 1.1.6 | ||||||
| WORKDIR /app/ |  | ||||||
|  |  | ||||||
| COPY ./pyproject.toml /app/ |  | ||||||
|  |  | ||||||
| RUN apk add --update --no-cache --virtual .build-deps \ | RUN apt-get update && apt-get install -y --no-install-recommends \ | ||||||
|  |     gcc g++ \ | ||||||
|     curl \ |     curl \ | ||||||
|     g++ \ |     gnupg gnupg2 gnupg1  \ | ||||||
|     python3-dev \ |     apt-transport-https \ | ||||||
|     musl-dev \ |     debian-archive-keyring \ | ||||||
|     gcc \ |     debian-keyring \ | ||||||
|     build-base && \ |     libwebp-dev \ | ||||||
|     curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | POETRY_HOME=/opt/poetry python && \ |     && curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | apt-key add - \ | ||||||
|     cd /usr/local/bin && \ |     && curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee -a /etc/apt/sources.list.d/caddy-stable.list \ | ||||||
|     ln -s /opt/poetry/bin/poetry && \ |     && apt-get update && apt-get install -y --no-install-recommends \ | ||||||
|     poetry config virtualenvs.create false && \ |     caddy \ | ||||||
|     cd /app/ && poetry install --no-root --no-dev && \ |     && apt autoremove \ | ||||||
|     apk --purge del .build-deps |     && rm -rf /var/lib/apt/lists/* \ | ||||||
|  |     && apt-get remove -y curl apt-transport-https debian-keyring g++ gnupg gnupg2 gnupg1  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | RUN pip install --no-cache-dir "poetry==$POETRY_VERSION"  | ||||||
|  |  | ||||||
|  | #! Future | ||||||
|  | # pip install --no-cache-dir "psycopg2-binary==2.8.6" | ||||||
|  |  | ||||||
|  | WORKDIR /app | ||||||
|  | COPY pyproject.toml /app/ | ||||||
| COPY ./mealie /app/mealie | COPY ./mealie /app/mealie | ||||||
| RUN poetry install --no-dev  | RUN poetry config virtualenvs.create false \ | ||||||
|  |     && poetry install --no-dev | ||||||
|  |  | ||||||
|  | #! Future | ||||||
|  | # COPY ./alembic /app | ||||||
|  | # COPY alembic.ini /app | ||||||
|  |  | ||||||
| COPY ./Caddyfile /app | COPY ./Caddyfile /app | ||||||
| COPY ./dev/data/templates /app/data/templates | COPY ./dev/data/templates /app/data/templates | ||||||
|  |  | ||||||
|  | # frontend build | ||||||
| COPY --from=build-stage /app/dist /app/dist | COPY --from=build-stage /app/dist /app/dist | ||||||
|  |  | ||||||
| VOLUME [ "/app/data/" ] | VOLUME [ "/app/data/" ] | ||||||
|  |  | ||||||
| RUN chmod +x /app/mealie/run.sh | EXPOSE 80 | ||||||
| CMD /app/mealie/run.sh |  | ||||||
|  |  | ||||||
|  | CMD /app/mealie/run.sh | ||||||
| @@ -1,24 +1,33 @@ | |||||||
| FROM python:3 | FROM python:3.9-slim-buster | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ENV PRODUCTION false | ||||||
|  | ENV POETRY_VERSION 1.1.6 | ||||||
|  |  | ||||||
|  | RUN apt-get update && apt-get install -y --no-install-recommends \ | ||||||
|  |     gcc g++ \ | ||||||
|  |     curl \ | ||||||
|  |     gnupg gnupg2 gnupg1  \ | ||||||
|  |     apt-transport-https \ | ||||||
|  |     debian-archive-keyring \ | ||||||
|  |     debian-keyring \ | ||||||
|  |     libwebp-dev \ | ||||||
|  |     && curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | apt-key add - \ | ||||||
|  |     && curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee -a /etc/apt/sources.list.d/caddy-stable.list \ | ||||||
|  |     && apt-get update && apt-get install -y --no-install-recommends \ | ||||||
|  |     && apt autoremove \ | ||||||
|  |     && rm -rf /var/lib/apt/lists/* \ | ||||||
|  |     && apt-get remove -y curl apt-transport-https debian-keyring g++ gnupg gnupg2 gnupg1  | ||||||
|  |  | ||||||
|  | RUN pip install --no-cache-dir "poetry==$POETRY_VERSION"  | ||||||
|  |  | ||||||
| WORKDIR /app/ | WORKDIR /app/ | ||||||
|  |  | ||||||
| ENV PRODUCTION false |  | ||||||
|  |  | ||||||
| RUN apt-get update -y && \ |  | ||||||
|     apt-get install -y python-pip python-dev |  | ||||||
|  |  | ||||||
| # Install Poetry |  | ||||||
| RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | POETRY_HOME=/opt/poetry python && \ |  | ||||||
|     cd /usr/local/bin && \ |  | ||||||
|     ln -s /opt/poetry/bin/poetry && \ |  | ||||||
|     poetry config virtualenvs.create false |  | ||||||
|  |  | ||||||
| # Copy poetry.lock* in case it doesn't exist in the repo | # Copy poetry.lock* in case it doesn't exist in the repo | ||||||
| COPY ./pyproject.toml /app/ | COPY ./pyproject.toml /app/ | ||||||
|  |  | ||||||
| COPY ./mealie /app/mealie | COPY ./mealie /app/mealie | ||||||
|  | RUN poetry config virtualenvs.create false \ | ||||||
| RUN poetry install  |     && poetry install | ||||||
|  |  | ||||||
| RUN chmod +x /app/mealie/run.sh | RUN chmod +x /app/mealie/run.sh | ||||||
| CMD ["/app/mealie/run.sh", "reload"] | CMD ["/app/mealie/run.sh", "reload"] | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ version: "3.1" | |||||||
| services: | services: | ||||||
|   # Vue Frontend |   # Vue Frontend | ||||||
|   mealie-frontend: |   mealie-frontend: | ||||||
|  |     container_name: mealie-frontend | ||||||
|     image: mealie-frontend:dev |     image: mealie-frontend:dev | ||||||
|     build: |     build: | ||||||
|       context: ./frontend |       context: ./frontend | ||||||
| @@ -18,6 +19,7 @@ services: | |||||||
|  |  | ||||||
|   # Fast API |   # Fast API | ||||||
|   mealie-api: |   mealie-api: | ||||||
|  |     container_name: mealie-api | ||||||
|     image: mealie-api:dev |     image: mealie-api:dev | ||||||
|     build: |     build: | ||||||
|       context: ./ |       context: ./ | ||||||
| @@ -34,6 +36,7 @@ services: | |||||||
|  |  | ||||||
|   # Mkdocs |   # Mkdocs | ||||||
|   mealie-docs: |   mealie-docs: | ||||||
|  |     container_name: mealie-docs | ||||||
|     image: squidfunk/mkdocs-material |     image: squidfunk/mkdocs-material | ||||||
|     restart: always |     restart: always | ||||||
|     ports: |     ports: | ||||||
|   | |||||||
| @@ -20,6 +20,11 @@ | |||||||
| - 'Dinner this week' shows a warning when no meal is planned yet | - 'Dinner this week' shows a warning when no meal is planned yet | ||||||
| - 'Dinner today' shows a warning when no meal is planned yet | - 'Dinner today' shows a warning when no meal is planned yet | ||||||
|  |  | ||||||
|  | ### Performance | ||||||
|  | - Images are now served up by the Caddy increase performance and offloading some loads from the API server | ||||||
|  | - Requesting all recipes from the server has been rewritten to refresh less often and manage client side data better. | ||||||
|  | - All images are now converted to .webp for better compression | ||||||
|  |  | ||||||
| ### General | ### General | ||||||
| - New Toolbox Page! | - New Toolbox Page! | ||||||
|   - Bulk assign categories and tags by keyword search |   - Bulk assign categories and tags by keyword search | ||||||
| @@ -38,6 +43,7 @@ | |||||||
|  |  | ||||||
|  |  | ||||||
| ### Behind the Scenes | ### Behind the Scenes | ||||||
|  | - New debian based docker image | ||||||
| - Unified Sidebar Components | - Unified Sidebar Components | ||||||
| - Refactor UI components to fit Vue best practices (WIP) | - Refactor UI components to fit Vue best practices (WIP) | ||||||
| - The API returns more consistent status codes | - The API returns more consistent status codes | ||||||
|   | |||||||
| @@ -134,14 +134,14 @@ export const recipeAPI = { | |||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   recipeImage(recipeSlug) { |   recipeImage(recipeSlug) { | ||||||
|     return `/api/recipes/${recipeSlug}/image?image_type=original`; |     return `/api/recipes/image/${recipeSlug}/original.webp`; | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   recipeSmallImage(recipeSlug) { |   recipeSmallImage(recipeSlug) { | ||||||
|     return `/api/recipes/${recipeSlug}/image?image_type=small`; |     return `/api/recipes/image/${recipeSlug}/min-original.webp`; | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   recipeTinyImage(recipeSlug) { |   recipeTinyImage(recipeSlug) { | ||||||
|     return `/api/recipes/${recipeSlug}/image?image_type=tiny`; |     return `/api/recipes/image/${recipeSlug}/tiny-original.webp`; | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -165,7 +165,7 @@ export default { | |||||||
|     }, |     }, | ||||||
|     getImage(image) { |     getImage(image) { | ||||||
|       if (image) { |       if (image) { | ||||||
|         return api.recipes.recipeImage(image) + "&rnd=" + this.imageKey; |         return api.recipes.recipeImage(image) + "?&rnd=" + this.imageKey; | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     async deleteRecipe() { |     async deleteRecipe() { | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								makefile
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								makefile
									
									
									
									
									
								
							| @@ -74,7 +74,7 @@ docker-dev: ## Build and Start Docker Development Stack | |||||||
| 	docker-compose -f docker-compose.dev.yml -p dev-mealie up --build | 	docker-compose -f docker-compose.dev.yml -p dev-mealie up --build | ||||||
|  |  | ||||||
| docker-prod: ## Build and Start Docker Production Stack | docker-prod: ## Build and Start Docker Production Stack | ||||||
| 	docker-compose -p mealie up --build -d | 	docker-compose -f docker-compose.yml -p mealie up --build -d | ||||||
|  |  | ||||||
| code-gen: ## Run Code-Gen Scripts | code-gen: ## Run Code-Gen Scripts | ||||||
| 	poetry run python dev/scripts/app_routes_gen.py | 	poetry run python dev/scripts/app_routes_gen.py | ||||||
|   | |||||||
| @@ -118,7 +118,6 @@ class RecipeModel(SqlAlchemyBase, BaseMixins): | |||||||
|  |  | ||||||
|         # Mealie Specific |         # Mealie Specific | ||||||
|         self.settings = RecipeSettings(**settings) if settings else RecipeSettings() |         self.settings = RecipeSettings(**settings) if settings else RecipeSettings() | ||||||
|         print(self.settings) |  | ||||||
|         self.tags = [Tag.create_if_not_exist(session=session, name=tag) for tag in tags] |         self.tags = [Tag.create_if_not_exist(session=session, name=tag) for tag in tags] | ||||||
|         self.slug = slug |         self.slug = slug | ||||||
|         self.date_added = date_added |         self.date_added = date_added | ||||||
|   | |||||||
| @@ -2,6 +2,8 @@ from enum import Enum | |||||||
|  |  | ||||||
| from fastapi import APIRouter, Depends, File, Form, HTTPException, status | from fastapi import APIRouter, Depends, File, Form, HTTPException, status | ||||||
| from fastapi.responses import FileResponse | from fastapi.responses import FileResponse | ||||||
|  | from mealie.core.config import app_dirs | ||||||
|  | from mealie.core.root_logger import get_logger | ||||||
| from mealie.db.database import db | from mealie.db.database import db | ||||||
| from mealie.db.db_setup import generate_session | from mealie.db.db_setup import generate_session | ||||||
| from mealie.routes.deps import get_current_user | from mealie.routes.deps import get_current_user | ||||||
| @@ -11,6 +13,7 @@ from mealie.services.scraper.scraper import create_from_url | |||||||
| from sqlalchemy.orm.session import Session | from sqlalchemy.orm.session import Session | ||||||
|  |  | ||||||
| router = APIRouter(prefix="/api/recipes", tags=["Recipe CRUD"]) | router = APIRouter(prefix="/api/recipes", tags=["Recipe CRUD"]) | ||||||
|  | logger = get_logger() | ||||||
|  |  | ||||||
|  |  | ||||||
| @router.post("/create", status_code=201, response_model=str) | @router.post("/create", status_code=201, response_model=str) | ||||||
| @@ -104,22 +107,15 @@ def delete_recipe( | |||||||
|  |  | ||||||
|  |  | ||||||
| class ImageType(str, Enum): | class ImageType(str, Enum): | ||||||
|     original = "original" |     original = "original.webp" | ||||||
|     small = "small" |     small = "min-original.webp" | ||||||
|     tiny = "tiny" |     tiny = "tiny-original.webp" | ||||||
|  |  | ||||||
|  |  | ||||||
| @router.get("/{recipe_slug}/image") | @router.get("/image/{recipe_slug}/{file_name}") | ||||||
| async def get_recipe_img(recipe_slug: str, image_type: ImageType = ImageType.original): | async def get_recipe_img(recipe_slug: str, file_name: ImageType = ImageType.original): | ||||||
|     """ Takes in a recipe slug, returns the static image """ |     """ Takes in a recipe slug, returns the static image """ | ||||||
|     if image_type == ImageType.original: |     recipe_image = app_dirs.IMG_DIR.joinpath(recipe_slug, file_name.value) | ||||||
|         which_image = IMG_OPTIONS.ORIGINAL_IMAGE |  | ||||||
|     elif image_type == ImageType.small: |  | ||||||
|         which_image = IMG_OPTIONS.MINIFIED_IMAGE |  | ||||||
|     elif image_type == ImageType.tiny: |  | ||||||
|         which_image = IMG_OPTIONS.TINY_IMAGE |  | ||||||
|  |  | ||||||
|     recipe_image = read_image(recipe_slug, image_type=which_image) |  | ||||||
|     if recipe_image: |     if recipe_image: | ||||||
|         return FileResponse(recipe_image) |         return FileResponse(recipe_image) | ||||||
|     else: |     else: | ||||||
|   | |||||||
| @@ -1,11 +1,8 @@ | |||||||
| #!/bin/sh | #!/bin/bash | ||||||
|  |  | ||||||
| # Get Reload Arg `run.sh reload` for dev server | # Get Reload Arg `run.sh reload` for dev server | ||||||
| ARG1=${1:-production} | ARG1=${1:-production} | ||||||
|  |  | ||||||
| # Set Script Directory - Used for running the script from a different directory. |  | ||||||
| # DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"  |  | ||||||
|  |  | ||||||
| # # Initialize Database Prerun | # # Initialize Database Prerun | ||||||
| poetry run python /app/mealie/db/init_db.py | poetry run python /app/mealie/db/init_db.py | ||||||
| poetry run python /app/mealie/services/image/minify.py | poetry run python /app/mealie/services/image/minify.py | ||||||
| @@ -15,12 +12,12 @@ poetry run python /app/mealie/services/image/minify.py | |||||||
|     # Migrations |     # Migrations | ||||||
|     # Set Port from ENV Variable |     # Set Port from ENV Variable | ||||||
|  |  | ||||||
| if [[ "$ARG1" = "reload" ]] | if [ "$ARG1" == "reload" ] | ||||||
| then | then | ||||||
|     echo "Hot Reload!" |     echo "Hot Reload!" | ||||||
|  |  | ||||||
|     # Start API |     # Start API | ||||||
|     uvicorn mealie.app:app --host 0.0.0.0 --port 9000 --reload |     python /app/mealie/app.py | ||||||
| else | else | ||||||
|     echo "Production" |     echo "Production" | ||||||
|     # Web Server |     # Web Server | ||||||
|   | |||||||
| @@ -35,21 +35,31 @@ def minify_image(image_file: Path) -> ImageSizes: | |||||||
|         min_dest (Path): FULL Destination File Path |         min_dest (Path): FULL Destination File Path | ||||||
|         tiny_dest (Path): FULL Destination File Path |         tiny_dest (Path): FULL Destination File Path | ||||||
|     """ |     """ | ||||||
|     min_dest = image_file.parent.joinpath(f"min-original{image_file.suffix}") |     def cleanup(dir: Path) -> None: | ||||||
|     tiny_dest = image_file.parent.joinpath(f"tiny-original{image_file.suffix}") |         for file in dir.glob("*.*"): | ||||||
|  |             if file.suffix != ".webp": | ||||||
|  |                 file.unlink() | ||||||
|  |  | ||||||
|     if min_dest.exists() and tiny_dest.exists(): |     org_dest = image_file.parent.joinpath(f"original.webp") | ||||||
|  |     min_dest = image_file.parent.joinpath(f"min-original.webp") | ||||||
|  |     tiny_dest = image_file.parent.joinpath(f"tiny-original.webp") | ||||||
|  |  | ||||||
|  |     if min_dest.exists() and tiny_dest.exists() and org_dest.exists(): | ||||||
|         return |         return | ||||||
|     try: |     try: | ||||||
|         img = Image.open(image_file) |         img = Image.open(image_file) | ||||||
|  |  | ||||||
|  |         img.save(org_dest, "WEBP") | ||||||
|         basewidth = 720 |         basewidth = 720 | ||||||
|         wpercent = basewidth / float(img.size[0]) |         wpercent = basewidth / float(img.size[0]) | ||||||
|         hsize = int((float(img.size[1]) * float(wpercent))) |         hsize = int((float(img.size[1]) * float(wpercent))) | ||||||
|         img = img.resize((basewidth, hsize), Image.ANTIALIAS) |         img = img.resize((basewidth, hsize), Image.ANTIALIAS) | ||||||
|         img.save(min_dest, quality=70) |         img.save(min_dest, "WEBP", quality=70) | ||||||
|  |  | ||||||
|         tiny_image = crop_center(img) |         tiny_image = crop_center(img) | ||||||
|         tiny_image.save(tiny_dest, quality=70) |         tiny_image.save(tiny_dest, "WEBP", quality=70) | ||||||
|  |  | ||||||
|  |         cleanup_images = True | ||||||
|  |  | ||||||
|     except Exception: |     except Exception: | ||||||
|         shutil.copy(image_file, min_dest) |         shutil.copy(image_file, min_dest) | ||||||
| @@ -59,6 +69,9 @@ def minify_image(image_file: Path) -> ImageSizes: | |||||||
|  |  | ||||||
|     logger.info(f"{image_file.name} Minified: {image_sizes.org} -> {image_sizes.min} -> {image_sizes.tiny}") |     logger.info(f"{image_file.name} Minified: {image_sizes.org} -> {image_sizes.min} -> {image_sizes.tiny}") | ||||||
|  |  | ||||||
|  |     if cleanup_images: | ||||||
|  |         cleanup(image_file.parent) | ||||||
|  |  | ||||||
|     return image_sizes |     return image_sizes | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user