mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-05-24 01:12:54 -04:00
feat: consolidate deployment targets and publish to ghcr.io (#2539)
* WIP: proof of concept * basic meta tag injection * add support for scraping public/private links * make tests go brrrrr * cleanup initialization * rewrite build config * remove recipe meta on frontend * make type checker happy * remove other deployment methods * fix issue with JSON response on un-authenticated request * docs updates * update tivy scanner * fix linter stuff * change registry tag * build fixes * fix same mistake I always make
This commit is contained in:
parent
aec4cb4f31
commit
2ad6af2cce
22
.github/workflows/nightly.yml
vendored
22
.github/workflows/nightly.yml
vendored
@ -41,24 +41,4 @@ jobs:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_NIGHTLY_WEBHOOK }}
|
||||
uses: Ilshidur/action-discord@0.3.2
|
||||
with:
|
||||
args: "🚀 New builds of mealie:api-nightly, mealie:frontend-nightly, and mealie:omni-nightly are available"
|
||||
|
||||
deploy-demo:
|
||||
runs-on: ubuntu-latest
|
||||
name: Deploy Demo
|
||||
needs:
|
||||
- build-release
|
||||
steps:
|
||||
- name: Clean and Deploy Demo
|
||||
uses: appleboy/ssh-action@master
|
||||
with:
|
||||
host: ${{ secrets.DEMO_SERVER_IP }}
|
||||
username: ${{ secrets.DEMO_SERVER_USER }}
|
||||
key: ${{ secrets.DEMO_SERVER_SSH_KEY }}
|
||||
port: ${{ secrets.DEMO_SERVER_PORT }}
|
||||
script_stop: true
|
||||
script: |
|
||||
cd ~/docker/mealie
|
||||
docker-compose pull
|
||||
docker-compose down -v
|
||||
docker-compose up -d
|
||||
args: "🚀 New builds of ghcr.io/mealie-recipes/mealie:nightly"
|
||||
|
144
.github/workflows/partial-builder.yml
vendored
144
.github/workflows/partial-builder.yml
vendored
@ -13,163 +13,37 @@ on:
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
build-frontend:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
name: Build Frontend
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up QEMU
|
||||
id: qemu
|
||||
uses: docker/setup-qemu-action@v2
|
||||
with:
|
||||
image: tonistiigi/binfmt:latest
|
||||
platforms: all
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
install: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
|
||||
with:
|
||||
images: |
|
||||
hkotel/mealie
|
||||
ghcr.io/${{ github.repository }}
|
||||
|
||||
- name: Build and push Frontend images
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
file: docker/frontend.Dockerfile
|
||||
context: .
|
||||
push: true
|
||||
tags: frontend-${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
build-backend:
|
||||
runs-on: ubuntu-latest
|
||||
name: Build Backend
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up QEMU
|
||||
id: qemu
|
||||
uses: docker/setup-qemu-action@v2
|
||||
with:
|
||||
image: tonistiigi/binfmt:latest
|
||||
platforms: all
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
install: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Override __init__.py
|
||||
run: |
|
||||
echo "__version__ = \"${{ inputs.tag }}\"" > ./mealie/__init__.py
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
|
||||
with:
|
||||
images: |
|
||||
hkotel/mealie
|
||||
ghcr.io/${{ github.repository }}
|
||||
|
||||
- name: Build and push API images
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
file: docker/api.Dockerfile
|
||||
file: ./docker/Dockerfile
|
||||
context: .
|
||||
push: true
|
||||
tags: api-${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
build-omni:
|
||||
runs-on: ubuntu-latest
|
||||
name: Build Omni
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up QEMU
|
||||
id: qemu
|
||||
uses: docker/setup-qemu-action@v2
|
||||
with:
|
||||
image: tonistiigi/binfmt:latest
|
||||
platforms: all
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
install: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Override __init__.py
|
||||
run: |
|
||||
echo "__version__ = \"${{ inputs.tag }}\"" > ./mealie/__init__.py
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
|
||||
with:
|
||||
images: |
|
||||
hkotel/mealie
|
||||
ghcr.io/${{ github.repository }}
|
||||
|
||||
- name: Build and push API images
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
file: docker/omni.Dockerfile
|
||||
context: .
|
||||
push: true
|
||||
tags: omni-${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ghcr.io/${{ github.repository }}:${{ inputs.tag }}
|
||||
|
@ -1,11 +1,11 @@
|
||||
name: Trivy Backend Container Scanning
|
||||
name: Trivy Container Scanning
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and Scan Backend Container
|
||||
name: Build and Scan Container
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: true
|
||||
@ -15,7 +15,7 @@ jobs:
|
||||
|
||||
- name: Build Dockerfile
|
||||
run: |
|
||||
docker build -t mealie --file=./docker/api.Dockerfile .
|
||||
docker build -t mealie --file=./docker/Dockerfile .
|
||||
|
||||
- name: Run Trivy vulnerability scanner
|
||||
uses: aquasecurity/trivy-action@master
|
@ -1,31 +0,0 @@
|
||||
name: Trivy Frontend Container Scanning
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and Scan Frontend Container
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: true
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build Dockerfile
|
||||
run: |
|
||||
docker build -t mealie --file=./docker/frontend.Dockerfile .
|
||||
|
||||
- name: Run Trivy vulnerability scanner
|
||||
uses: aquasecurity/trivy-action@master
|
||||
with:
|
||||
ignore-unfixed: true
|
||||
image-ref: "mealie"
|
||||
format: "sarif"
|
||||
output: "trivy-results.sarif"
|
||||
|
||||
- name: Upload Trivy scan results to GitHub Security tab
|
||||
uses: github/codeql-action/upload-sarif@v2
|
||||
with:
|
||||
sarif_file: "trivy-results.sarif"
|
10
.github/workflows/pull-requests.yml
vendored
10
.github/workflows/pull-requests.yml
vendored
@ -14,10 +14,6 @@ jobs:
|
||||
name: "Frontend and End-to-End Tests"
|
||||
uses: ./.github/workflows/partial-frontend.yml
|
||||
|
||||
backend-container-scanning:
|
||||
name: "Trivy Backend Container Scanning"
|
||||
uses: ./.github/workflows/partial-trivy-backend-container-scanning.yml
|
||||
|
||||
frontend-container-scanning:
|
||||
name: "Trivy Frontend Container Scanning"
|
||||
uses: ./.github/workflows/partial-trivy-frontend-container-scanning.yml
|
||||
container-scanning:
|
||||
name: "Trivy Container Scanning"
|
||||
uses: ./.github/workflows/partial-trivy-container-scanning.yml
|
||||
|
@ -12,14 +12,7 @@ RUN yarn install \
|
||||
# https://github.com/docker/build-push-action/issues/471
|
||||
--network-timeout 1000000
|
||||
|
||||
RUN yarn build
|
||||
|
||||
RUN rm -rf node_modules && \
|
||||
NODE_ENV=production yarn install \
|
||||
--prefer-offline \
|
||||
--pure-lockfile \
|
||||
--non-interactive \
|
||||
--production=true
|
||||
RUN yarn generate
|
||||
|
||||
###############################################
|
||||
# Base Image - Python
|
||||
@ -150,12 +143,13 @@ HEALTHCHECK CMD python $MEALIE_HOME/mealie/scripts/healthcheck.py || exit 1
|
||||
# Copy Frontend
|
||||
|
||||
# copying caddy into image
|
||||
COPY --from=builder /app $MEALIE_HOME/frontend/
|
||||
ENV STATIC_FILES=/spa/static
|
||||
COPY --from=builder /app/dist ${STATIC_FILES}
|
||||
|
||||
ENV HOST 0.0.0.0
|
||||
|
||||
EXPOSE ${APP_PORT}
|
||||
COPY ./docker/omni.entry.sh $MEALIE_HOME/run.sh
|
||||
COPY ./docker/entry.sh $MEALIE_HOME/run.sh
|
||||
|
||||
RUN chmod +x $MEALIE_HOME/run.sh
|
||||
ENTRYPOINT $MEALIE_HOME/run.sh
|
@ -1,115 +0,0 @@
|
||||
###############################################
|
||||
# Base Image
|
||||
###############################################
|
||||
FROM python:3.10-slim as python-base
|
||||
|
||||
ENV MEALIE_HOME="/app"
|
||||
|
||||
ENV PYTHONUNBUFFERED=1 \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
PIP_NO_CACHE_DIR=off \
|
||||
PIP_DISABLE_PIP_VERSION_CHECK=on \
|
||||
PIP_DEFAULT_TIMEOUT=100 \
|
||||
POETRY_HOME="/opt/poetry" \
|
||||
POETRY_VIRTUALENVS_IN_PROJECT=true \
|
||||
POETRY_NO_INTERACTION=1 \
|
||||
PYSETUP_PATH="/opt/pysetup" \
|
||||
VENV_PATH="/opt/pysetup/.venv"
|
||||
|
||||
# prepend poetry and venv to path
|
||||
ENV PATH="$POETRY_HOME/bin:$VENV_PATH/bin:$PATH"
|
||||
|
||||
# create user account
|
||||
RUN useradd -u 911 -U -d $MEALIE_HOME -s /bin/bash abc \
|
||||
&& usermod -G users abc \
|
||||
&& mkdir $MEALIE_HOME
|
||||
|
||||
###############################################
|
||||
# Builder Image
|
||||
###############################################
|
||||
FROM python-base as builder-base
|
||||
RUN apt-get update \
|
||||
&& apt-get install --no-install-recommends -y \
|
||||
curl \
|
||||
build-essential \
|
||||
libpq-dev \
|
||||
libwebp-dev \
|
||||
tesseract-ocr-all \
|
||||
# LDAP Dependencies
|
||||
libsasl2-dev libldap2-dev libssl-dev \
|
||||
gnupg gnupg2 gnupg1 \
|
||||
&& pip install -U --no-cache-dir pip
|
||||
|
||||
# install poetry - respects $POETRY_VERSION & $POETRY_HOME
|
||||
ENV POETRY_VERSION=1.3.1
|
||||
RUN curl -sSL https://install.python-poetry.org | python3 -
|
||||
|
||||
# copy project requirement files here to ensure they will be cached.
|
||||
WORKDIR $PYSETUP_PATH
|
||||
COPY ./poetry.lock ./pyproject.toml ./
|
||||
|
||||
# install runtime deps - uses $POETRY_VIRTUALENVS_IN_PROJECT internally
|
||||
RUN poetry install -E pgsql --only main
|
||||
|
||||
###############################################
|
||||
# CRFPP Image
|
||||
###############################################
|
||||
FROM hkotel/crfpp as crfpp
|
||||
|
||||
RUN echo "crfpp-container"
|
||||
|
||||
###############################################
|
||||
# Production Image
|
||||
###############################################
|
||||
FROM python-base as production
|
||||
ENV PRODUCTION=true
|
||||
ENV TESTING=false
|
||||
|
||||
ARG COMMIT
|
||||
ENV GIT_COMMIT_HASH=$COMMIT
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install --no-install-recommends -y \
|
||||
gosu \
|
||||
iproute2 \
|
||||
tesseract-ocr-all \
|
||||
libldap-common \
|
||||
&& apt-get autoremove \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# copying poetry and venv into image
|
||||
COPY --from=builder-base $POETRY_HOME $POETRY_HOME
|
||||
COPY --from=builder-base $PYSETUP_PATH $PYSETUP_PATH
|
||||
|
||||
ENV LD_LIBRARY_PATH=/usr/local/lib
|
||||
COPY --from=crfpp /usr/local/lib/ /usr/local/lib
|
||||
COPY --from=crfpp /usr/local/bin/crf_learn /usr/local/bin/crf_learn
|
||||
COPY --from=crfpp /usr/local/bin/crf_test /usr/local/bin/crf_test
|
||||
|
||||
# copy backend
|
||||
COPY ./mealie $MEALIE_HOME/mealie
|
||||
COPY ./poetry.lock ./pyproject.toml $MEALIE_HOME/
|
||||
COPY ./gunicorn_conf.py $MEALIE_HOME
|
||||
|
||||
# Alembic
|
||||
COPY ./alembic $MEALIE_HOME/alembic
|
||||
COPY ./alembic.ini $MEALIE_HOME/
|
||||
|
||||
# venv already has runtime deps installed we get a quicker install
|
||||
WORKDIR $MEALIE_HOME
|
||||
RUN . $VENV_PATH/bin/activate && poetry install -E pgsql --only main
|
||||
WORKDIR /
|
||||
|
||||
# Grab CRF++ Model Release
|
||||
RUN python $MEALIE_HOME/mealie/scripts/install_model.py
|
||||
|
||||
VOLUME [ "$MEALIE_HOME/data/" ]
|
||||
ENV APP_PORT=9000
|
||||
|
||||
EXPOSE ${APP_PORT}
|
||||
|
||||
HEALTHCHECK CMD python $MEALIE_HOME/mealie/scripts/healthcheck.py || exit 1
|
||||
|
||||
COPY ./docker/api.entry.sh $MEALIE_HOME/mealie/run.sh
|
||||
RUN chmod +x $MEALIE_HOME/mealie/run.sh
|
||||
ENTRYPOINT $MEALIE_HOME/mealie/run.sh
|
@ -1,58 +1,20 @@
|
||||
version: "3.4"
|
||||
services:
|
||||
mealie-frontend:
|
||||
container_name: mealie-frontend
|
||||
image: mealie-frontend:dev
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 500M
|
||||
build:
|
||||
context: ../
|
||||
dockerfile: ./docker/frontend.Dockerfile
|
||||
restart: always
|
||||
volumes:
|
||||
- mealie-data:/app/data/
|
||||
ports:
|
||||
- 9091:3000
|
||||
environment:
|
||||
- API_URL=http://mealie-api:9000
|
||||
|
||||
# =====================================
|
||||
# Light Mode Config
|
||||
- THEME_LIGHT_PRIMARY=#E58325
|
||||
- THEME_LIGHT_ACCENT=#007A99
|
||||
- THEME_LIGHT_SECONDARY=#973542
|
||||
- THEME_LIGHT_SUCCESS=#43A047
|
||||
- THEME_LIGHT_INFO=#1976D2
|
||||
- THEME_LIGHT_WARNING=#FF6D00
|
||||
- THEME_LIGHT_ERROR=#EF5350
|
||||
# =====================================
|
||||
# Dark Mode Config
|
||||
- THEME_DARK_PRIMARY=#E58325
|
||||
- THEME_DARK_ACCENT=#007A99
|
||||
- THEME_DARK_SECONDARY=#973542
|
||||
- THEME_DARK_SUCCESS=#43A047
|
||||
- THEME_DARK_INFO=#1976D2
|
||||
- THEME_DARK_WARNING=#FF6D00
|
||||
- THEME_DARK_ERROR=#EF5350
|
||||
mealie:
|
||||
container_name: mealie-api
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 1000M
|
||||
container_name: mealie
|
||||
image: mealie:dev
|
||||
build:
|
||||
context: ../
|
||||
target: production
|
||||
dockerfile: ./docker/api.Dockerfile
|
||||
dockerfile: ./docker/Dockerfile
|
||||
restart: always
|
||||
volumes:
|
||||
- mealie-data:/app/data/
|
||||
ports:
|
||||
- 9092:9000
|
||||
- 9091:9000
|
||||
environment:
|
||||
ALLOW_SIGNUP: "false"
|
||||
|
||||
DB_ENGINE: sqlite # Optional: 'sqlite', 'postgres'
|
||||
# =====================================
|
||||
# Postgres Config
|
||||
@ -78,14 +40,6 @@ services:
|
||||
# SMTP_USER=
|
||||
# SMTP_PASSWORD=
|
||||
|
||||
# postgres:
|
||||
# container_name: postgres
|
||||
# image: postgres
|
||||
# restart: always
|
||||
# environment:
|
||||
# POSTGRES_PASSWORD: mealie
|
||||
# POSTGRES_USER: mealie
|
||||
|
||||
volumes:
|
||||
mealie-data:
|
||||
driver: local
|
||||
|
@ -1,9 +1,9 @@
|
||||
# Start Backend API
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# Get Reload Arg `run.sh reload` for dev server
|
||||
ARG1=${1:-production}
|
||||
# Strict Mode
|
||||
# set -e
|
||||
# IFS=$'\n\t'
|
||||
|
||||
# Get PUID/PGID
|
||||
PUID=${PUID:-911}
|
||||
@ -41,12 +41,8 @@ init() {
|
||||
poetry run python /app/mealie/db/init_db.py
|
||||
}
|
||||
|
||||
echo "Production"
|
||||
|
||||
change_user
|
||||
|
||||
# change_user
|
||||
init
|
||||
|
||||
GUNICORN_PORT=${API_PORT:-9000}
|
||||
|
||||
# Start API
|
@ -1,46 +0,0 @@
|
||||
{
|
||||
auto_https off
|
||||
admin off
|
||||
}
|
||||
|
||||
:3000 {
|
||||
@apidocs path /docs /openapi.json
|
||||
|
||||
@static {
|
||||
file
|
||||
path *.ico *.css *.js *.gif *.jpg *.jpeg *.png *.svg *.woff *.woff2 *.webp
|
||||
}
|
||||
|
||||
encode gzip zstd
|
||||
|
||||
# Handles Recipe Images / Assets
|
||||
handle_path /api/media/recipes/* {
|
||||
header @static Cache-Control max-age=31536000
|
||||
root * /app/data/recipes/
|
||||
file_server
|
||||
}
|
||||
|
||||
# Handles User Images
|
||||
handle_path /api/media/users/* {
|
||||
header @static Cache-Control max-age=31536000
|
||||
root * /app/data/users/
|
||||
file_server
|
||||
}
|
||||
|
||||
# Handle Docker Volume Validation File
|
||||
handle_path /api/media/docker/* {
|
||||
root * /app/data/docker-validation/
|
||||
file_server
|
||||
}
|
||||
|
||||
|
||||
handle @apidocs {
|
||||
uri strip_suffix /
|
||||
reverse_proxy {$API_URL}
|
||||
}
|
||||
|
||||
handle {
|
||||
uri strip_suffix /
|
||||
reverse_proxy http://127.0.0.1:3001
|
||||
}
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
FROM node:16 as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./frontend .
|
||||
|
||||
RUN yarn install \
|
||||
--prefer-offline \
|
||||
--frozen-lockfile \
|
||||
--non-interactive \
|
||||
--production=false \
|
||||
# https://github.com/docker/build-push-action/issues/471
|
||||
--network-timeout 1000000
|
||||
|
||||
RUN yarn build
|
||||
|
||||
RUN rm -rf node_modules && \
|
||||
NODE_ENV=production yarn install \
|
||||
--prefer-offline \
|
||||
--pure-lockfile \
|
||||
--non-interactive \
|
||||
--production=true
|
||||
|
||||
FROM node:16-alpine
|
||||
|
||||
RUN apk add caddy
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# copying caddy into image
|
||||
COPY --from=builder /app .
|
||||
COPY ./docker/frontend.Caddyfile /app/Caddyfile
|
||||
COPY ./docker/frontend.entry.sh /app/run.sh
|
||||
|
||||
ENV HOST 0.0.0.0
|
||||
EXPOSE 3000
|
||||
|
||||
RUN chmod +x /app/run.sh
|
||||
ENTRYPOINT /app/run.sh
|
@ -1,7 +0,0 @@
|
||||
# Production entry point for the frontend docker container
|
||||
|
||||
# Web Server
|
||||
caddy start --config /app/Caddyfile
|
||||
|
||||
# Start Node Application
|
||||
yarn start -p 3001
|
@ -1,45 +0,0 @@
|
||||
version: "3.4"
|
||||
services:
|
||||
omni-mealie:
|
||||
container_name: mealie
|
||||
image: mealie-omni:dev
|
||||
build:
|
||||
context: ../
|
||||
target: production
|
||||
dockerfile: ./docker/omni.Dockerfile
|
||||
restart: always
|
||||
volumes:
|
||||
- mealie-data:/app/data/
|
||||
ports:
|
||||
- 9091:3000
|
||||
environment:
|
||||
ALLOW_SIGNUP: "false"
|
||||
|
||||
DB_ENGINE: sqlite # Optional: 'sqlite', 'postgres'
|
||||
# =====================================
|
||||
# Postgres Config
|
||||
POSTGRES_USER: mealie
|
||||
POSTGRES_PASSWORD: mealie
|
||||
POSTGRES_SERVER: postgres
|
||||
POSTGRES_PORT: 5432
|
||||
POSTGRES_DB: mealie
|
||||
|
||||
# =====================================
|
||||
# Web Concurrency
|
||||
WEB_GUNICORN: "false"
|
||||
WORKERS_PER_CORE: 0.5
|
||||
MAX_WORKERS: 1
|
||||
WEB_CONCURRENCY: 1
|
||||
# =====================================
|
||||
# Email Configuration
|
||||
# SMTP_HOST=
|
||||
# SMTP_PORT=587
|
||||
# SMTP_FROM_NAME=Mealie
|
||||
# SMTP_AUTH_STRATEGY=TLS # Options: 'TLS', 'SSL', 'NONE'
|
||||
# SMTP_FROM_EMAIL=
|
||||
# SMTP_USER=
|
||||
# SMTP_PASSWORD=
|
||||
|
||||
volumes:
|
||||
mealie-data:
|
||||
driver: local
|
@ -1,59 +0,0 @@
|
||||
# Start Backend API
|
||||
#!/bin/bash
|
||||
|
||||
# Strict Mode
|
||||
# set -e
|
||||
# IFS=$'\n\t'
|
||||
|
||||
# Get PUID/PGID
|
||||
PUID=${PUID:-911}
|
||||
PGID=${PGID:-911}
|
||||
|
||||
add_user() {
|
||||
groupmod -o -g "$PGID" abc
|
||||
usermod -o -u "$PUID" abc
|
||||
}
|
||||
|
||||
change_user() {
|
||||
# If container is started as root then create a new user and switch to it
|
||||
if [ "$(id -u)" = "0" ]; then
|
||||
add_user
|
||||
chown -R $PUID:$PGID /app
|
||||
|
||||
echo "Switching to dedicated user"
|
||||
exec gosu $PUID "$BASH_SOURCE" "$@"
|
||||
elif [ "$(id -u)" = $PUID ]; then
|
||||
echo "
|
||||
User uid: $PUID
|
||||
User gid: $PGID
|
||||
"
|
||||
fi
|
||||
}
|
||||
|
||||
init() {
|
||||
# $MEALIE_HOME directory
|
||||
cd /app
|
||||
|
||||
# Activate our virtual environment here
|
||||
. /opt/pysetup/.venv/bin/activate
|
||||
|
||||
# Initialize Database Prerun
|
||||
poetry run python /app/mealie/db/init_db.py
|
||||
}
|
||||
|
||||
# change_user
|
||||
init
|
||||
GUNICORN_PORT=${API_PORT:-9000}
|
||||
|
||||
# Start API
|
||||
hostip=`/sbin/ip route|awk '/default/ { print $3 }'`
|
||||
if [ "$WEB_GUNICORN" = 'true' ]; then
|
||||
echo "Starting Gunicorn"
|
||||
gunicorn mealie.app:app -b 0.0.0.0:$GUNICORN_PORT --forwarded-allow-ips=$hostip -k uvicorn.workers.UvicornWorker -c /app/gunicorn_conf.py --preload &
|
||||
else
|
||||
uvicorn mealie.app:app --host 0.0.0.0 --forwarded-allow-ips=$hostip --port $GUNICORN_PORT &
|
||||
fi
|
||||
|
||||
# ------------------------------
|
||||
# Start Frontend Nuxt Server
|
||||
cd /app/frontend && yarn start -p 3000
|
@ -1,29 +0,0 @@
|
||||
# Frontend Configuration
|
||||
|
||||
## Environment Variables
|
||||
|
||||
### General
|
||||
|
||||
| Variables | Default | Description |
|
||||
| --------- | :--------------------: | ------------------------- |
|
||||
| API_URL | http://mealie-api:9000 | URL to proxy API requests |
|
||||
|
||||
### Themeing
|
||||
Setting the following environmental variables will change the theme of the frontend. Note that the themes are the same for all users. This is a break-change when migration from v0.x.x -> 1.x.x.
|
||||
|
||||
| Variables | Default | Description |
|
||||
| --------------------- | :-----: | --------------------------- |
|
||||
| THEME_LIGHT_PRIMARY | #E58325 | Light Theme Config Variable |
|
||||
| THEME_LIGHT_ACCENT | #007A99 | Light Theme Config Variable |
|
||||
| THEME_LIGHT_SECONDARY | #973542 | Light Theme Config Variable |
|
||||
| THEME_LIGHT_SUCCESS | #43A047 | Light Theme Config Variable |
|
||||
| THEME_LIGHT_INFO | #1976D2 | Light Theme Config Variable |
|
||||
| THEME_LIGHT_WARNING | #FF6D00 | Light Theme Config Variable |
|
||||
| THEME_LIGHT_ERROR | #EF5350 | Light Theme Config Variable |
|
||||
| THEME_DARK_PRIMARY | #E58325 | Dark Theme Config Variable |
|
||||
| THEME_DARK_ACCENT | #007A99 | Dark Theme Config Variable |
|
||||
| THEME_DARK_SECONDARY | #973542 | Dark Theme Config Variable |
|
||||
| THEME_DARK_SUCCESS | #43A047 | Dark Theme Config Variable |
|
||||
| THEME_DARK_INFO | #1976D2 | Dark Theme Config Variable |
|
||||
| THEME_DARK_WARNING | #FF6D00 | Dark Theme Config Variable |
|
||||
| THEME_DARK_ERROR | #EF5350 | Dark Theme Config Variable |
|
@ -55,10 +55,8 @@ After you've decided setup the files it's important to set a few ENV variables t
|
||||
|
||||
- [x] You've configured the relevant ENV variables for your database selection in the `docker-compose.yaml` files.
|
||||
- [x] You've configured the [SMTP server settings](./backend-config.md#email) (used for invitations, password resets, etc). You can setup a [google app password](https://support.google.com/accounts/answer/185833?hl=en) if you want to send email via gmail.
|
||||
- [x] Verified the port mapped on the `mealie-frontend` container is an open port on your server (Default: 9925)
|
||||
- [x] You've set the [`BASE_URL`](./backend-config.md#general) variable.
|
||||
- [x] You've set the `DEFAULT_EMAIL` and `DEFAULT_GROUP` variable.
|
||||
- [x] Make any theme changes on the frontend container. [See Frontend Config](./frontend-config.md#themeing)
|
||||
|
||||
## Step 3: Startup
|
||||
After you've configured your database, and updated the `docker-compose.yaml` files, you can start Mealie by running the following command in the directory where you've added your `docker-compose.yaml`.
|
||||
@ -79,9 +77,6 @@ You should see the containers start up without error. You should now be able to
|
||||
|
||||
After the startup is complete you should see a login screen. Use the default credentials above to login and navigate to `/admin/site-settings`. Here you'll find a summary of your configuration details and their respective status. Before proceeding you should validate that the configuration is correct. For any warnings or errors the page will display an error and notify you of what you need to verify.
|
||||
|
||||
!!! tip "Docker Volume"
|
||||
Mealie uses a shared data-volume between the Backend and Frontend containers for images and assets. Ensure that this is configured correctly by using the "Docker Volume Test" section in the settings page. Running this validation will ensure that you have configured your volumes correctly. Mealie will not work correctly without this configured correctly.
|
||||
|
||||
## Step 5: Backup
|
||||
While v1.0.0 is a great step to data-stability and security, it's not a backup. Mealie provides a full site data backup mechanism through the UI.
|
||||
|
||||
@ -91,6 +86,14 @@ These backups are just plain .zip files that you can download from the UI or acc
|
||||
|
||||
### Docker Tags
|
||||
|
||||
`ghcr.io/mealie-recipes/mealie:nightly`
|
||||
|
||||
The nightly build are the latest and greatest builds that are built directly off of every commit to the `mealie-next` branch and as such may contain bugs. These are great to help the community catch bugs before they hit the stable release or if you like living on the edge.
|
||||
|
||||
---
|
||||
|
||||
**These tags no are long updated**
|
||||
|
||||
`mealie:frontend-v1.0.0beta-x` **and** `mealie:api-v1.0.0beta-x`
|
||||
|
||||
These are the tags for the latest beta release of the frontend docker-container. These are currently considered the latest and most stable releases and the recommended way of using Mealie.
|
||||
@ -98,16 +101,3 @@ These are the tags for the latest beta release of the frontend docker-container.
|
||||
`mealie:frontend-nightly`**and** `mealie:api-nightly`
|
||||
|
||||
The nightly build are the latest and greatest builds that are built directly off of every commit to the `mealie-next` branch and as such may contain bugs. These are great to help the community catch bugs before they hit the stable release or if you like living on the edge.
|
||||
|
||||
|
||||
### Docker Diagram
|
||||
|
||||
While the docker-compose file should work without modification, some users want to tailor it to their installation. This diagram shows network and volume architecture for the default setup. You can use this to help you customize your configuration.
|
||||
|
||||

|
||||
|
||||
In the diagram above there's a few crucial things to note.
|
||||
|
||||
1. Port 9925 is the host port, this can be anything you want. The important part is that it's mapped to the mealie-frontend container at port 3000.
|
||||
2. The mealie-frontend container communicated with the mealie-api container through the INTERNAL docker network. This requires that the two containers are on the same network and that the network supports name resolution (anything but the default bridge network). The resolution URL can be specified in the docker-compose as the `API_URL` environment variable. The API_URL must match the network name of the mealie-backend container, which should be the same as the container name (e.g. a container renamed to `my-api` should have an `API_URL` set to `http://my-api:<backend port, default 9000`)
|
||||
3. The mealie-data volume is mounted to BOTH the mealie-frontend and mealie-api containers. This is REQUIRED to ensure that images and assets are served up correctly. While the default configuration is a docker-volume, that same can be accomplished by using a local directory mounted to the containers.
|
||||
|
@ -4,33 +4,21 @@ PostgreSQL might be considered if you need to support many concurrent users. In
|
||||
|
||||
**For Environmental Variable Configuration See:**
|
||||
|
||||
- [Frontend Configuration](./frontend-config.md)
|
||||
- [Backend Configuration](./backend-config.md)
|
||||
- [Configuration](./backend-config.md)
|
||||
|
||||
```yaml
|
||||
---
|
||||
version: "3.7"
|
||||
services:
|
||||
mealie-frontend:
|
||||
image: hkotel/mealie:frontend-v1.0.0beta-5
|
||||
container_name: mealie-frontend
|
||||
depends_on:
|
||||
- mealie-api
|
||||
environment:
|
||||
# Set Frontend ENV Variables Here
|
||||
- API_URL=http://mealie-api:9000 # (1)
|
||||
restart: always
|
||||
mealie:
|
||||
image: ghcr.io/mealie-recipes/mealie:nightly
|
||||
container_name: mealie
|
||||
ports:
|
||||
- "9925:3000" # (2)
|
||||
volumes:
|
||||
- mealie-data:/app/data/ # (3)
|
||||
mealie-api:
|
||||
image: hkotel/mealie:api-v1.0.0beta-5
|
||||
container_name: mealie-api
|
||||
- "9925:9000"
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 1000M # (4)
|
||||
memory: 1000M # (1)
|
||||
depends_on:
|
||||
- postgres
|
||||
volumes:
|
||||
@ -72,8 +60,5 @@ volumes:
|
||||
|
||||
<!-- Updating This? Be Sure to also update the SQLite Annotations -->
|
||||
|
||||
1. Whoa whoa whoa, what is this nonsense? The API_URL is the URL the frontend container uses to proxy api requests to the backend server. In this example, the name `mealie-api` resolves to the `mealie-api` container which runs the API server on port 9000. This allows you to access the API without exposing an additional port on the host.
|
||||
<br/> <br/> **Note** that both containers must be on the same docker-network for this to work.
|
||||
2. To access the mealie interface you only need to expose port 3000 on the mealie-frontend container. Here we expose port 9925 on the host, feel free to change this to any port you like.
|
||||
3. Mounting the data directory to the frontend is now required to access the images/assets directory. This can be mounted read-only. Internally the frontend containers runs a Caddy proxy server that serves the assets requested to reduce load on the backend API.
|
||||
4. Setting an explicit memory limit is recommended. Python can pre-allocate larger amounts of memory than is necessary if you have a machine with a lot of RAM. This can cause the container to idle at a high memory usage. Setting a memory limit will improve idle performance.
|
||||
1. To access the mealie interface you only need to expose port 9000 on the mealie-frontend container. Here we expose port 9925 on the host, feel free to change this to any port you like.
|
||||
2. Setting an explicit memory limit is recommended. Python can pre-allocate larger amounts of memory than is necessary if you have a machine with a lot of RAM. This can cause the container to idle at a high memory usage. Setting a memory limit will improve idle performance.
|
||||
|
@ -1,37 +0,0 @@
|
||||
# Using the Omni Image
|
||||
|
||||
Since [#1948](https://github.com/hay-kot/mealie/pull/1948) we've started publishing an experimental image that merges both the frontend and backend services into a single container image. This image is currently in an experimental state, and should be used with caution. Continued support for this image will be based on user feedback and demand, if you're using this image please let us know how it's working for you. The single container comes with SQLite, and can be used with PostgreSQL by adding a postgres container to the docker-compose file (see the [PostgreSQL install](./postgres.md) for a snippet).
|
||||
|
||||
- [Feedback Discussion](https://github.com/hay-kot/mealie/discussions/1949)
|
||||
|
||||
**For Environmental Variable Configuration See:**
|
||||
|
||||
Note that frontend and backend configurations are both applied to the same container. The container exposes the port 9000 for the API and port 3000 for the frontend.
|
||||
|
||||
- [Frontend Configuration](./frontend-config.md)
|
||||
- [Backend Configuration](./backend-config.md)
|
||||
|
||||
```yaml
|
||||
---
|
||||
version: "3.7"
|
||||
services:
|
||||
mealie-omni:
|
||||
image: hkotel/mealie:omni-nightly
|
||||
container_name: mealie
|
||||
ports:
|
||||
- "3000:3000"
|
||||
- "9000:9000"
|
||||
volumes:
|
||||
- mealie-data:/app/data/
|
||||
environment:
|
||||
- ALLOW_SIGNUP=true
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=America/Anchorage
|
||||
- BASE_URL=https://mealie.yourdomain.com
|
||||
restart: always
|
||||
|
||||
volumes:
|
||||
mealie-data:
|
||||
driver: local
|
||||
```
|
@ -4,31 +4,21 @@ SQLite is a popular, open source, self-contained, zero-configuration database th
|
||||
|
||||
**For Environmental Variable Configuration See:**
|
||||
|
||||
- [Frontend Configuration](./frontend-config.md)
|
||||
- [Backend Configuration](./backend-config.md)
|
||||
- [Configuration](./backend-config.md)
|
||||
|
||||
```yaml
|
||||
---
|
||||
version: "3.7"
|
||||
services:
|
||||
mealie-frontend:
|
||||
image: hkotel/mealie:frontend-v1.0.0beta-5
|
||||
container_name: mealie-frontend
|
||||
environment:
|
||||
# Set Frontend ENV Variables Here
|
||||
- API_URL=http://mealie-api:9000 # (1)
|
||||
restart: always
|
||||
ports:
|
||||
- "9925:3000" # (2)
|
||||
volumes:
|
||||
- mealie-data:/app/data/ # (3)
|
||||
mealie-api:
|
||||
image: hkotel/mealie:api-v1.0.0beta-5
|
||||
image: ghcr.io/mealie-recipes/mealie:nightly
|
||||
container_name: mealie-api
|
||||
ports:
|
||||
"9925:9000" # (1)
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 1000M # (4)
|
||||
memory: 1000M # (2)
|
||||
volumes:
|
||||
- mealie-data:/app/data/
|
||||
environment:
|
||||
@ -49,8 +39,5 @@ volumes:
|
||||
|
||||
<!-- Updating This? Be Sure to also update the Postgres Annotations -->
|
||||
|
||||
1. Whoa whoa whoa, what is this nonsense? The API_URL is the URL the frontend container uses to proxy api requests to the backend server. In this example, the name `mealie-api` resolves to the `mealie-api` container which runs the API server on port 9000. This allows you to access the API without exposing an additional port on the host.
|
||||
<br/> <br/> **Note** that both containers must be on the same docker-network for this to work.
|
||||
2. To access the mealie interface you only need to expose port 3000 on the mealie-frontend container. Here we expose port 9925 on the host, feel free to change this to any port you like.
|
||||
3. Mounting the data directory to the frontend is now required to access the images/assets directory. This can be mounted read-only. Internally the frontend containers runs a Caddy proxy server that serves the assets requested to reduce load on the backend API.
|
||||
4. Setting an explicit memory limit is recommended. Python can pre-allocate larger amounts of memory than is necessary if you have a machine with a lot of RAM. This can cause the container to idle at a high memory usage. Setting a memory limit will improve idle performance.
|
||||
1. To access the mealie interface you only need to expose port 9000 on the mealie-frontend container. Here we expose port 9925 on the host, feel free to change this to any port you like.
|
||||
2. Setting an explicit memory limit is recommended. Python can pre-allocate larger amounts of memory than is necessary if you have a machine with a lot of RAM. This can cause the container to idle at a high memory usage. Setting a memory limit will improve idle performance.
|
||||
|
File diff suppressed because one or more lines are too long
@ -315,8 +315,8 @@
|
||||
Groups
|
||||
</h2>
|
||||
<p>
|
||||
Sort users into groups to share recipes and mealplans with the whole family while keeping
|
||||
your cooking club separate.
|
||||
Full multi-user support with groups for sharing recipes and meal plans
|
||||
with family.
|
||||
</p>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
|
@ -71,8 +71,6 @@ nav:
|
||||
- Installation Checklist: "documentation/getting-started/installation/installation-checklist.md"
|
||||
- SQLite (Recommended): "documentation/getting-started/installation/sqlite.md"
|
||||
- PostgreSQL: "documentation/getting-started/installation/postgres.md"
|
||||
- Single Container (Experimental): "documentation/getting-started/installation/single-container.md"
|
||||
- Frontend Configuration: "documentation/getting-started/installation/frontend-config.md"
|
||||
- Backend Configuration: "documentation/getting-started/installation/backend-config.md"
|
||||
- Usage:
|
||||
- Backup and Restoring: "documentation/getting-started/usage/backups-and-restoring.md"
|
||||
|
@ -1,7 +1,13 @@
|
||||
<template>
|
||||
<v-container :class="{ 'pa-0': $vuetify.breakpoint.smAndDown }">
|
||||
<v-card :flat="$vuetify.breakpoint.smAndDown" class="d-print-none">
|
||||
<RecipePageHeader :recipe="recipe" :recipe-scale="scale" :landscape="landscape" @save="saveRecipe" @delete="deleteRecipe" />
|
||||
<RecipePageHeader
|
||||
:recipe="recipe"
|
||||
:recipe-scale="scale"
|
||||
:landscape="landscape"
|
||||
@save="saveRecipe"
|
||||
@delete="deleteRecipe"
|
||||
/>
|
||||
<LazyRecipeJsonEditor v-if="isEditJSON" v-model="recipe" class="mt-10" :options="EDITOR_OPTIONS" />
|
||||
<v-card-text v-else>
|
||||
<!--
|
||||
@ -100,7 +106,6 @@ import RecipePrintContainer from "~/components/Domain/Recipe/RecipePrintContaine
|
||||
import { EditorMode, PageMode, usePageState, usePageUser } from "~/composables/recipe-page/shared-state";
|
||||
import { NoUndefinedField } from "~/lib/api/types/non-generated";
|
||||
import { Recipe } from "~/lib/api/types/recipe";
|
||||
import { useRecipeMeta } from "~/composables/recipes";
|
||||
import { useRouteQuery } from "~/composables/use-router";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { uuid4, deepCopy } from "~/composables/use-utils";
|
||||
@ -275,9 +280,6 @@ export default defineComponent({
|
||||
/** =============================================================
|
||||
* Meta Tags
|
||||
*/
|
||||
const { recipeMeta } = useRecipeMeta();
|
||||
useMeta(recipeMeta(ref(props.recipe)));
|
||||
|
||||
const { user } = usePageUser();
|
||||
|
||||
return {
|
||||
|
@ -3,4 +3,3 @@ export { useRecipe } from "./use-recipe";
|
||||
export { useRecipes, recentRecipes, allRecipes, useLazyRecipes } from "./use-recipes";
|
||||
export { parseIngredientText, useParsedIngredientText } from "./use-recipe-ingredients";
|
||||
export { useTools } from "./use-recipe-tools";
|
||||
export { useRecipeMeta } from "./use-recipe-meta";
|
||||
|
@ -1,58 +0,0 @@
|
||||
import { Ref } from "@nuxtjs/composition-api";
|
||||
import { useStaticRoutes } from "~/composables/api";
|
||||
import { Recipe } from "~/lib/api/types/recipe";
|
||||
|
||||
export interface RecipeMeta {
|
||||
title?: string;
|
||||
mainImage?: string;
|
||||
meta: Array<any>;
|
||||
__dangerouslyDisableSanitizers: Array<string>;
|
||||
script: Array<any>;
|
||||
}
|
||||
|
||||
export const useRecipeMeta = () => {
|
||||
const { recipeImage } = useStaticRoutes();
|
||||
function recipeMeta(recipe: Ref<Recipe | null>): RecipeMeta {
|
||||
const imageURL = recipeImage(recipe?.value?.id ?? "");
|
||||
return {
|
||||
title: recipe?.value?.name,
|
||||
mainImage: imageURL,
|
||||
meta: [
|
||||
{ hid: "og:title", property: "og:title", content: recipe?.value?.name || "Recipe" },
|
||||
{
|
||||
hid: "og:description",
|
||||
property: "og:description",
|
||||
content: recipe?.value?.description ?? "",
|
||||
},
|
||||
{
|
||||
hid: "og:image",
|
||||
property: "og:image",
|
||||
content: imageURL,
|
||||
},
|
||||
{
|
||||
hid: "twitter:title",
|
||||
property: "twitter:title",
|
||||
content: recipe?.value?.name ?? "",
|
||||
},
|
||||
{
|
||||
hid: "twitter:desc",
|
||||
property: "twitter:description",
|
||||
content: recipe?.value?.description ?? "",
|
||||
},
|
||||
{ hid: "t-type", name: "twitter:card", content: "summary_large_image" },
|
||||
],
|
||||
__dangerouslyDisableSanitizers: ["script"],
|
||||
script: [
|
||||
{
|
||||
innerHTML: JSON.stringify({
|
||||
"@context": "http://schema.org",
|
||||
"@type": "Recipe",
|
||||
...recipe.value,
|
||||
}),
|
||||
type: "application/ld+json",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return { recipeMeta };
|
||||
};
|
@ -1,5 +1,6 @@
|
||||
export default {
|
||||
// Global page headers: https://go.nuxtjs.dev/config-head
|
||||
target: "static",
|
||||
head: {
|
||||
title: "Mealie",
|
||||
meta: [
|
||||
|
@ -7,10 +7,9 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, useAsync, useMeta, useRoute, useRouter } from "@nuxtjs/composition-api";
|
||||
import { defineComponent, useAsync, useMeta, useRoute, useRouter } from "@nuxtjs/composition-api";
|
||||
import RecipePage from "~/components/Domain/Recipe/RecipePage/RecipePage.vue";
|
||||
import { usePublicExploreApi } from "~/composables/api/api-client";
|
||||
import { useRecipeMeta } from "~/composables/recipes";
|
||||
|
||||
export default defineComponent({
|
||||
components: { RecipePage },
|
||||
@ -22,8 +21,7 @@ export default defineComponent({
|
||||
const recipeSlug = route.value.params.recipeSlug;
|
||||
const api = usePublicExploreApi(groupSlug);
|
||||
|
||||
const { meta, title } = useMeta();
|
||||
const { recipeMeta } = useRecipeMeta();
|
||||
const { title } = useMeta();
|
||||
|
||||
const recipe = useAsync(async () => {
|
||||
const { data, error } = await api.explore.recipes.getOne(recipeSlug);
|
||||
@ -35,8 +33,6 @@ export default defineComponent({
|
||||
|
||||
if (data) {
|
||||
title.value = data?.name || "";
|
||||
const metaObj = recipeMeta(ref(data));
|
||||
meta.value = metaObj.meta;
|
||||
}
|
||||
|
||||
return data;
|
||||
|
@ -10,7 +10,6 @@
|
||||
import { defineComponent, ref, useAsync, useMeta, useRoute, useRouter } from "@nuxtjs/composition-api";
|
||||
import RecipePage from "~/components/Domain/Recipe/RecipePage/RecipePage.vue";
|
||||
import { usePublicApi } from "~/composables/api/api-client";
|
||||
import { useRecipeMeta } from "~/composables/recipes";
|
||||
|
||||
export default defineComponent({
|
||||
components: { RecipePage },
|
||||
@ -21,8 +20,7 @@ export default defineComponent({
|
||||
const recipeId = route.value.params.id;
|
||||
const api = usePublicApi();
|
||||
|
||||
const { meta, title } = useMeta();
|
||||
const { recipeMeta } = useRecipeMeta();
|
||||
const { title } = useMeta();
|
||||
|
||||
const recipe = useAsync(async () => {
|
||||
const { data, error } = await api.shared.getShared(recipeId);
|
||||
@ -34,8 +32,6 @@ export default defineComponent({
|
||||
|
||||
if (data) {
|
||||
title.value = data?.name || "";
|
||||
const metaObj = recipeMeta(ref(data));
|
||||
meta.value = metaObj.meta;
|
||||
}
|
||||
|
||||
return data;
|
||||
|
3
makefile
3
makefile
@ -122,9 +122,6 @@ frontend-lint: ## 🧺 Run yarn lint
|
||||
# -----------------------------------------------------------------------------
|
||||
# Docker makefile
|
||||
|
||||
docker/omni: ## 🐳 Build and start the omni style container
|
||||
cd docker && docker-compose -f omni.docker-compose.yml -p mealie-omni up --build
|
||||
|
||||
docker/prod: ## 🐳 Build and Start Docker Production Stack
|
||||
cd docker && docker-compose -f docker-compose.yml -p mealie up --build
|
||||
|
||||
|
@ -6,7 +6,7 @@ from fastapi.routing import APIRoute
|
||||
from mealie.core.config import get_app_settings
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.core.settings.static import APP_VERSION
|
||||
from mealie.routes import router, utility_routes
|
||||
from mealie.routes import router, spa, utility_routes
|
||||
from mealie.routes.handlers import register_debug_handler
|
||||
from mealie.routes.media import media_router
|
||||
from mealie.services.scheduler import SchedulerRegistry, SchedulerService, tasks
|
||||
@ -77,6 +77,9 @@ def api_routers():
|
||||
app.include_router(media_router)
|
||||
app.include_router(utility_routes.router)
|
||||
|
||||
if settings.PRODUCTION and not settings.TESTING:
|
||||
spa.mount_spa(app)
|
||||
|
||||
|
||||
api_routers()
|
||||
|
||||
|
@ -5,7 +5,7 @@ from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
import fastapi
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi import Depends, HTTPException, Request, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from jose import JWTError, jwt
|
||||
from sqlalchemy.orm.session import Session
|
||||
@ -64,12 +64,29 @@ async def get_public_group(group_slug: str = fastapi.Path(...), session=Depends(
|
||||
return group
|
||||
|
||||
|
||||
async def get_current_user(token: str = Depends(oauth2_scheme), session=Depends(generate_session)) -> PrivateUser:
|
||||
async def try_get_current_user(
|
||||
request: Request,
|
||||
token: str = Depends(oauth2_scheme_soft_fail),
|
||||
session=Depends(generate_session),
|
||||
) -> PrivateUser | None:
|
||||
try:
|
||||
return await get_current_user(request, token, session)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
async def get_current_user(
|
||||
request: Request, token: str = Depends(oauth2_scheme_soft_fail), session=Depends(generate_session)
|
||||
) -> PrivateUser:
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
if token is None and "mealie.access_token" in request.cookies:
|
||||
# Try extract from cookie
|
||||
token = request.cookies.get("mealie.access_token", "")
|
||||
|
||||
try:
|
||||
payload = jwt.decode(token, settings.SECRET, algorithms=[ALGORITHM])
|
||||
user_id: str = payload.get("sub")
|
||||
|
@ -27,6 +27,9 @@ class AppSettings(BaseSettings):
|
||||
BASE_URL: str = "http://localhost:8080"
|
||||
"""trailing slashes are trimmed (ex. `http://localhost:8080/` becomes ``http://localhost:8080`)"""
|
||||
|
||||
STATIC_FILES: str = ""
|
||||
"""path to static files directory (ex. `mealie/dist`)"""
|
||||
|
||||
IS_DEMO: bool = False
|
||||
API_PORT: int = 9000
|
||||
API_DOCS: bool = True
|
||||
|
@ -1,6 +1,6 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from fastapi import APIRouter, Depends, Form, Request, status
|
||||
from fastapi import APIRouter, Depends, Form, Request, Response, status
|
||||
from fastapi.exceptions import HTTPException
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from pydantic import BaseModel
|
||||
@ -49,7 +49,12 @@ class MealieAuthToken(BaseModel):
|
||||
|
||||
|
||||
@public_router.post("/token")
|
||||
def get_token(request: Request, data: CustomOAuth2Form = Depends(), session: Session = Depends(generate_session)):
|
||||
def get_token(
|
||||
request: Request,
|
||||
response: Response,
|
||||
data: CustomOAuth2Form = Depends(),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
email = data.username
|
||||
password = data.password
|
||||
if "x-forwarded-for" in request.headers:
|
||||
@ -73,6 +78,14 @@ def get_token(request: Request, data: CustomOAuth2Form = Depends(), session: Ses
|
||||
|
||||
duration = timedelta(days=14) if data.remember_me else None
|
||||
access_token = security.create_access_token(dict(sub=str(user.id)), duration) # type: ignore
|
||||
|
||||
response.set_cookie(
|
||||
key="mealie.access_token",
|
||||
value=access_token,
|
||||
httponly=True,
|
||||
max_age=duration.seconds if duration else None,
|
||||
)
|
||||
|
||||
return MealieAuthToken.respond(access_token)
|
||||
|
||||
|
||||
|
160
mealie/routes/spa/__init__.py
Normal file
160
mealie/routes/spa/__init__.py
Normal file
@ -0,0 +1,160 @@
|
||||
import json
|
||||
import pathlib
|
||||
|
||||
from fastapi import Depends, FastAPI, Response
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from sqlalchemy.orm.session import Session
|
||||
from starlette.exceptions import HTTPException
|
||||
from text_unidecode import os
|
||||
|
||||
from mealie.core.config import get_app_settings
|
||||
from mealie.core.dependencies.dependencies import try_get_current_user
|
||||
from mealie.db.db_setup import generate_session
|
||||
from mealie.repos.repository_factory import AllRepositories
|
||||
from mealie.schema.recipe.recipe import Recipe
|
||||
from mealie.schema.user.user import PrivateUser
|
||||
|
||||
|
||||
class SPAStaticFiles(StaticFiles):
|
||||
async def get_response(self, path: str, scope):
|
||||
try:
|
||||
return await super().get_response(path, scope)
|
||||
except HTTPException as ex:
|
||||
if ex.status_code == 404:
|
||||
return await super().get_response("index.html", scope)
|
||||
else:
|
||||
raise ex
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
|
||||
__app_settings = get_app_settings()
|
||||
__contents = ""
|
||||
|
||||
|
||||
def content_with_meta(recipe: Recipe) -> str:
|
||||
# Inject meta tags
|
||||
recipe_url = f"{__app_settings.BASE_URL}/recipe/{recipe.slug}"
|
||||
image_url = f"{__app_settings.BASE_URL}/api/media/recipes/{recipe.id}/images/original.webp?version={recipe.image}"
|
||||
|
||||
ingredients: list[str] = []
|
||||
if recipe.settings.disable_amount: # type: ignore
|
||||
ingredients = [i.note for i in recipe.recipe_ingredient if i.note]
|
||||
|
||||
else:
|
||||
for ing in recipe.recipe_ingredient:
|
||||
s = ""
|
||||
if ing.quantity:
|
||||
s += f"{ing.quantity} "
|
||||
if ing.unit:
|
||||
s += f"{ing.unit.name} "
|
||||
if ing.food:
|
||||
s += f"{ing.food.name} "
|
||||
if ing.note:
|
||||
s += f"{ing.note}"
|
||||
|
||||
ingredients.append(s)
|
||||
|
||||
nutrition: dict[str, str | None] = {}
|
||||
if recipe.nutrition:
|
||||
nutrition["calories"] = recipe.nutrition.calories
|
||||
nutrition["fatContent"] = recipe.nutrition.fat_content
|
||||
nutrition["fiberContent"] = recipe.nutrition.fiber_content
|
||||
nutrition["proteinContent"] = recipe.nutrition.protein_content
|
||||
nutrition["carbohydrateContent"] = recipe.nutrition.carbohydrate_content
|
||||
nutrition["sodiumContent"] = recipe.nutrition.sodium_content
|
||||
nutrition["sugarContent"] = recipe.nutrition.sugar_content
|
||||
|
||||
as_schema_org = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Recipe",
|
||||
"name": recipe.name,
|
||||
"description": recipe.description,
|
||||
"image": [image_url],
|
||||
"datePublished": recipe.created_at,
|
||||
"prepTime": recipe.prep_time,
|
||||
"cookTime": recipe.cook_time,
|
||||
"totalTime": recipe.total_time,
|
||||
"recipeYield": recipe.recipe_yield,
|
||||
"recipeIngredient": ingredients,
|
||||
"recipeInstructions": [i.text for i in recipe.recipe_instructions] if recipe.recipe_instructions else [],
|
||||
"recipeCategory": [c.name for c in recipe.recipe_category] if recipe.recipe_category else [],
|
||||
"keywords": [t.name for t in recipe.tags] if recipe.tags else [],
|
||||
"nutrition": nutrition,
|
||||
}
|
||||
|
||||
tags = [
|
||||
f'<meta property="og:title" content="{recipe.name}" />',
|
||||
f'<meta property="og:description" content="{recipe.description}" />',
|
||||
f'<meta property="og:image" content="{image_url}" />',
|
||||
f'<meta property="og:url" content="{recipe_url}" />',
|
||||
'<meta name="twitter:card" content="summary_large_image" />',
|
||||
f'<meta name="twitter:title" content="{recipe.name}" />',
|
||||
f'<meta name="twitter:description" content="{recipe.description}" />',
|
||||
f'<meta name="twitter:image" content="{image_url}" />',
|
||||
f'<meta name="twitter:url" content="{recipe_url}" />',
|
||||
f"""<script type="application/ld+json">{json.dumps(jsonable_encoder(as_schema_org))}</script>""",
|
||||
]
|
||||
|
||||
return __contents.replace("</head>", "\n".join(tags) + "\n</head>", 1)
|
||||
|
||||
|
||||
def response_404():
|
||||
return Response(__contents, media_type="text/html", status_code=404)
|
||||
|
||||
|
||||
def serve_recipe_with_meta_public(
|
||||
group_slug: str,
|
||||
recipe_slug: str,
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
try:
|
||||
repos = AllRepositories(session)
|
||||
group = repos.groups.get_by_slug_or_id(group_slug)
|
||||
|
||||
if not group or group.preferences.private_group: # type: ignore
|
||||
return response_404()
|
||||
|
||||
recipe = repos.recipes.by_group(group.id).get_one(recipe_slug)
|
||||
|
||||
if not recipe or not recipe.settings.public: # type: ignore
|
||||
return response_404()
|
||||
|
||||
# Inject meta tags
|
||||
return Response(content_with_meta(recipe), media_type="text/html")
|
||||
except Exception:
|
||||
return response_404()
|
||||
|
||||
|
||||
async def serve_recipe_with_meta(
|
||||
slug: str,
|
||||
user: PrivateUser = Depends(try_get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
if not user:
|
||||
return Response(__contents, media_type="text/html", status_code=401)
|
||||
|
||||
try:
|
||||
repos = AllRepositories(session)
|
||||
|
||||
recipe = repos.recipes.by_group(user.group_id).get_one(slug, "slug")
|
||||
if recipe is None:
|
||||
return response_404()
|
||||
|
||||
# Serve contents as HTML
|
||||
return Response(content_with_meta(recipe), media_type="text/html")
|
||||
except Exception:
|
||||
return response_404()
|
||||
|
||||
|
||||
def mount_spa(app: FastAPI):
|
||||
if not os.path.exists(__app_settings.STATIC_FILES):
|
||||
return
|
||||
|
||||
global __contents
|
||||
__contents = pathlib.Path(__app_settings.STATIC_FILES).joinpath("index.html").read_text()
|
||||
|
||||
app.get("/recipe/{slug}")(serve_recipe_with_meta)
|
||||
app.get("/explore/recipes/{group_slug}/{recipe_slug}")(serve_recipe_with_meta_public)
|
||||
app.mount("/", SPAStaticFiles(directory=__app_settings.STATIC_FILES, html=True), name="spa")
|
Loading…
x
Reference in New Issue
Block a user