BREAKING CHANGE: ditch gosu, make rootless+distroless container. Reduced size from 1GB to to 500MB.
Ditch UID and GID variables. These are now set in docker-compose.yaml
This commit is contained in:
@@ -1,62 +1,36 @@
|
|||||||
# Git
|
# Allowlist minimal build context
|
||||||
.git
|
*
|
||||||
.gitignore
|
|
||||||
.gitattributes
|
|
||||||
|
|
||||||
# Docker
|
# Backend
|
||||||
docker-compose.yaml
|
!requirements.txt
|
||||||
docker-compose.yml
|
!app.py
|
||||||
Dockerfile
|
!routes/**
|
||||||
.dockerignore
|
# Re-ignore caches and compiled files inside routes
|
||||||
|
routes/**/__pycache__/
|
||||||
|
routes/**/.pytest_cache/
|
||||||
|
routes/**/*.pyc
|
||||||
|
routes/**/*.pyo
|
||||||
|
|
||||||
# Node
|
# Frontend: only what's needed to build
|
||||||
node_modules
|
!spotizerr-ui/package.json
|
||||||
spotizerr-ui/node_modules
|
!spotizerr-ui/pnpm-lock.yaml
|
||||||
npm-debug.log
|
!spotizerr-ui/pnpm-workspace.yaml
|
||||||
pnpm-lock.yaml
|
!spotizerr-ui/index.html
|
||||||
|
!spotizerr-ui/vite.config.ts
|
||||||
|
!spotizerr-ui/postcss.config.mjs
|
||||||
|
!spotizerr-ui/tsconfig.json
|
||||||
|
!spotizerr-ui/tsconfig.app.json
|
||||||
|
!spotizerr-ui/tsconfig.node.json
|
||||||
|
!spotizerr-ui/src/**
|
||||||
|
!spotizerr-ui/public/**
|
||||||
|
!spotizerr-ui/scripts/**
|
||||||
|
# Exclude heavy/unnecessary frontend folders
|
||||||
|
spotizerr-ui/node_modules/**
|
||||||
|
spotizerr-ui/dist/**
|
||||||
|
spotizerr-ui/dev-dist/**
|
||||||
|
|
||||||
# Python
|
# Always exclude local data/logs/tests/etc.
|
||||||
__pycache__
|
.data/
|
||||||
*.pyc
|
|
||||||
*.pyo
|
|
||||||
*.pyd
|
|
||||||
.Python
|
|
||||||
.env
|
|
||||||
.venv
|
|
||||||
venv/
|
|
||||||
env/
|
|
||||||
.env.example
|
|
||||||
|
|
||||||
# Editor/OS
|
|
||||||
.vscode
|
|
||||||
.idea
|
|
||||||
.DS_Store
|
|
||||||
*.swp
|
|
||||||
|
|
||||||
# Application data
|
|
||||||
credentials.json
|
|
||||||
test.py
|
|
||||||
downloads/
|
|
||||||
creds/
|
|
||||||
Test.py
|
|
||||||
prgs/
|
|
||||||
flask_server.log
|
|
||||||
test.sh
|
|
||||||
routes/__pycache__/*
|
|
||||||
routes/utils/__pycache__/*
|
|
||||||
search_test.py
|
|
||||||
config/main.json
|
|
||||||
.cache
|
|
||||||
config/state/queue_state.json
|
|
||||||
output.log
|
|
||||||
queue_state.json
|
|
||||||
search_demo.py
|
|
||||||
celery_worker.log
|
|
||||||
static/js/*
|
|
||||||
logs/
|
logs/
|
||||||
data
|
Downloads/
|
||||||
tests/
|
tests/
|
||||||
|
|
||||||
# Non-essential files
|
|
||||||
docs/
|
|
||||||
README.md
|
|
||||||
|
|||||||
@@ -19,12 +19,6 @@ REDIS_PASSWORD=CHANGE_ME
|
|||||||
# Set to true to filter out explicit content.
|
# Set to true to filter out explicit content.
|
||||||
EXPLICIT_FILTER=false
|
EXPLICIT_FILTER=false
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# User and group ID for the container. Sets the owner of the downloaded files.
|
|
||||||
PUID=1000
|
|
||||||
PGID=1000
|
|
||||||
|
|
||||||
# Optional: Sets the default file permissions for newly created files within the container.
|
# Optional: Sets the default file permissions for newly created files within the container.
|
||||||
UMASK=0022
|
UMASK=0022
|
||||||
|
|
||||||
|
|||||||
69
Dockerfile
69
Dockerfile
@@ -7,40 +7,59 @@ RUN pnpm install --frozen-lockfile
|
|||||||
COPY spotizerr-ui/. .
|
COPY spotizerr-ui/. .
|
||||||
RUN pnpm build
|
RUN pnpm build
|
||||||
|
|
||||||
# Stage 2: Final application image
|
# Stage 2: Python dependencies builder (create relocatable deps dir)
|
||||||
FROM python:3.12-slim
|
FROM python:3.11-slim AS py-deps
|
||||||
|
WORKDIR /app
|
||||||
|
COPY requirements.txt .
|
||||||
|
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/
|
||||||
|
RUN uv pip install --target /python -r requirements.txt
|
||||||
|
|
||||||
# Set an environment variable for non-interactive frontend installation
|
# Stage 3: Fetch static ffmpeg/ffprobe binaries
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
FROM debian:stable-slim AS ffmpeg
|
||||||
|
ARG TARGETARCH
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
ca-certificates curl xz-utils \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
RUN case "$TARGETARCH" in \
|
||||||
|
amd64) FFMPEG_PKG=ffmpeg-master-latest-linux64-gpl.tar.xz ;; \
|
||||||
|
arm64) FFMPEG_PKG=ffmpeg-master-latest-linuxarm64-gpl.tar.xz ;; \
|
||||||
|
*) echo "Unsupported arch: $TARGETARCH" && exit 1 ;; \
|
||||||
|
esac && \
|
||||||
|
curl -fsSL -o /tmp/ffmpeg.tar.xz https://github.com/BtbN/FFmpeg-Builds/releases/latest/download/${FFMPEG_PKG} && \
|
||||||
|
tar -xJf /tmp/ffmpeg.tar.xz -C /tmp && \
|
||||||
|
mv /tmp/ffmpeg-* /ffmpeg
|
||||||
|
|
||||||
|
# Stage 4: Prepare world-writable runtime directories
|
||||||
|
FROM busybox:1.36.1-musl AS runtime-dirs
|
||||||
|
RUN mkdir -p /artifact/downloads /artifact/data/config /artifact/data/creds /artifact/data/watch /artifact/data/history /artifact/logs/tasks \
|
||||||
|
&& chmod -R 0777 /artifact
|
||||||
|
|
||||||
|
# Stage 5: Final application image (distroless)
|
||||||
|
FROM gcr.io/distroless/python3-debian12
|
||||||
|
|
||||||
LABEL org.opencontainers.image.source="https://github.com/Xoconoch/spotizerr"
|
LABEL org.opencontainers.image.source="https://github.com/Xoconoch/spotizerr"
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install system dependencies
|
# Ensure Python finds vendored site-packages and unbuffered output
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
ENV PYTHONPATH=/python
|
||||||
ffmpeg gosu\
|
ENV PYTHONUNBUFFERED=1
|
||||||
&& apt-get clean \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Install Python dependencies
|
# Copy application code
|
||||||
COPY requirements.txt .
|
COPY --chown=65532:65532 . .
|
||||||
|
|
||||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/
|
# Copy compiled assets from the frontend build
|
||||||
RUN uv pip install --system -r requirements.txt
|
COPY --from=frontend-builder --chown=65532:65532 /app/spotizerr-ui/dist ./spotizerr-ui/dist
|
||||||
|
|
||||||
# Copy application code (excluding UI source and TS source)
|
# Copy vendored Python dependencies
|
||||||
COPY . .
|
COPY --from=py-deps --chown=65532:65532 /python /python
|
||||||
|
|
||||||
# Copy compiled assets from previous stages
|
# Copy static ffmpeg binaries
|
||||||
COPY --from=frontend-builder /app/spotizerr-ui/dist ./spotizerr-ui/dist
|
COPY --from=ffmpeg --chown=65532:65532 /ffmpeg/bin/ffmpeg /usr/local/bin/ffmpeg
|
||||||
|
COPY --from=ffmpeg --chown=65532:65532 /ffmpeg/bin/ffprobe /usr/local/bin/ffprobe
|
||||||
|
|
||||||
# Create necessary directories with proper permissions
|
# Copy pre-created world-writable runtime directories
|
||||||
RUN mkdir -p downloads data/config data/creds data/watch data/history logs/tasks && \
|
COPY --from=runtime-dirs --chown=65532:65532 /artifact/ ./
|
||||||
chmod -R 777 downloads data logs
|
|
||||||
|
|
||||||
# Make entrypoint script executable
|
# No shell or package manager available in distroless
|
||||||
RUN chmod +x entrypoint.sh
|
ENTRYPOINT ["python3", "app.py"]
|
||||||
|
|
||||||
# Set entrypoint to our script
|
|
||||||
ENTRYPOINT ["/app/entrypoint.sh"]
|
|
||||||
|
|||||||
97
app.py
97
app.py
@@ -13,43 +13,14 @@ import redis
|
|||||||
import socket
|
import socket
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
# Run DB migrations as early as possible, before importing any routers that may touch DBs
|
# Apply process umask from environment as early as possible
|
||||||
try:
|
_umask_value = os.getenv("UMASK")
|
||||||
from routes.migrations import run_migrations_if_needed
|
if _umask_value:
|
||||||
|
try:
|
||||||
run_migrations_if_needed()
|
os.umask(int(_umask_value, 8))
|
||||||
logging.getLogger(__name__).info(
|
except Exception:
|
||||||
"Database migrations executed (if needed) early in startup."
|
# Defer logging setup; avoid failing on invalid UMASK
|
||||||
)
|
pass
|
||||||
except Exception as e:
|
|
||||||
logging.getLogger(__name__).error(
|
|
||||||
f"Database migration step failed early in startup: {e}", exc_info=True
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Import route routers (to be created)
|
|
||||||
from routes.auth.credentials import router as credentials_router
|
|
||||||
from routes.auth.auth import router as auth_router
|
|
||||||
from routes.content.artist import router as artist_router
|
|
||||||
from routes.content.album import router as album_router
|
|
||||||
from routes.content.track import router as track_router
|
|
||||||
from routes.content.playlist import router as playlist_router
|
|
||||||
from routes.core.search import router as search_router
|
|
||||||
from routes.core.history import router as history_router
|
|
||||||
from routes.system.progress import router as prgs_router
|
|
||||||
from routes.system.config import router as config_router
|
|
||||||
|
|
||||||
|
|
||||||
# Import Celery configuration and manager
|
|
||||||
from routes.utils.celery_manager import celery_manager
|
|
||||||
from routes.utils.celery_config import REDIS_URL
|
|
||||||
|
|
||||||
# Import authentication system
|
|
||||||
from routes.auth import AUTH_ENABLED
|
|
||||||
from routes.auth.middleware import AuthMiddleware
|
|
||||||
|
|
||||||
# Import watch manager controls (start/stop) without triggering side effects
|
|
||||||
from routes.utils.watch.manager import start_watch_manager, stop_watch_manager
|
|
||||||
|
|
||||||
# Import and initialize routes (this will start the watch manager)
|
# Import and initialize routes (this will start the watch manager)
|
||||||
|
|
||||||
@@ -61,6 +32,17 @@ def setup_logging():
|
|||||||
logs_dir = Path("logs")
|
logs_dir = Path("logs")
|
||||||
logs_dir.mkdir(exist_ok=True)
|
logs_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
# Ensure required runtime directories exist
|
||||||
|
for p in [
|
||||||
|
Path("downloads"),
|
||||||
|
Path("data/config"),
|
||||||
|
Path("data/creds"),
|
||||||
|
Path("data/watch"),
|
||||||
|
Path("data/history"),
|
||||||
|
Path("logs/tasks"),
|
||||||
|
]:
|
||||||
|
p.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# Set up log file paths
|
# Set up log file paths
|
||||||
main_log = logs_dir / "spotizerr.log"
|
main_log = logs_dir / "spotizerr.log"
|
||||||
|
|
||||||
@@ -111,6 +93,8 @@ def setup_logging():
|
|||||||
|
|
||||||
def check_redis_connection():
|
def check_redis_connection():
|
||||||
"""Check if Redis is available and accessible"""
|
"""Check if Redis is available and accessible"""
|
||||||
|
from routes.utils.celery_config import REDIS_URL
|
||||||
|
|
||||||
if not REDIS_URL:
|
if not REDIS_URL:
|
||||||
logging.error("REDIS_URL is not configured. Please check your environment.")
|
logging.error("REDIS_URL is not configured. Please check your environment.")
|
||||||
return False
|
return False
|
||||||
@@ -156,6 +140,20 @@ async def lifespan(app: FastAPI):
|
|||||||
# Startup
|
# Startup
|
||||||
setup_logging()
|
setup_logging()
|
||||||
|
|
||||||
|
# Run migrations before initializing services
|
||||||
|
try:
|
||||||
|
from routes.migrations import run_migrations_if_needed
|
||||||
|
|
||||||
|
run_migrations_if_needed()
|
||||||
|
logging.getLogger(__name__).info(
|
||||||
|
"Database migrations executed (if needed) early in startup."
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logging.getLogger(__name__).error(
|
||||||
|
f"Database migration step failed early in startup: {e}", exc_info=True
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
# Check Redis connection
|
# Check Redis connection
|
||||||
if not check_redis_connection():
|
if not check_redis_connection():
|
||||||
logging.error(
|
logging.error(
|
||||||
@@ -165,6 +163,8 @@ async def lifespan(app: FastAPI):
|
|||||||
|
|
||||||
# Start Celery workers
|
# Start Celery workers
|
||||||
try:
|
try:
|
||||||
|
from routes.utils.celery_manager import celery_manager
|
||||||
|
|
||||||
celery_manager.start()
|
celery_manager.start()
|
||||||
logging.info("Celery workers started successfully")
|
logging.info("Celery workers started successfully")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -172,6 +172,8 @@ async def lifespan(app: FastAPI):
|
|||||||
|
|
||||||
# Start Watch Manager after Celery is up
|
# Start Watch Manager after Celery is up
|
||||||
try:
|
try:
|
||||||
|
from routes.utils.watch.manager import start_watch_manager, stop_watch_manager
|
||||||
|
|
||||||
start_watch_manager()
|
start_watch_manager()
|
||||||
logging.info("Watch Manager initialized and registered for shutdown.")
|
logging.info("Watch Manager initialized and registered for shutdown.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -184,12 +186,16 @@ async def lifespan(app: FastAPI):
|
|||||||
|
|
||||||
# Shutdown
|
# Shutdown
|
||||||
try:
|
try:
|
||||||
|
from routes.utils.watch.manager import start_watch_manager, stop_watch_manager
|
||||||
|
|
||||||
stop_watch_manager()
|
stop_watch_manager()
|
||||||
logging.info("Watch Manager stopped")
|
logging.info("Watch Manager stopped")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error stopping Watch Manager: {e}")
|
logging.error(f"Error stopping Watch Manager: {e}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
from routes.utils.celery_manager import celery_manager
|
||||||
|
|
||||||
celery_manager.stop()
|
celery_manager.stop()
|
||||||
logging.info("Celery workers stopped")
|
logging.info("Celery workers stopped")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -215,13 +221,30 @@ def create_app():
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Add authentication middleware (only if auth is enabled)
|
# Add authentication middleware (only if auth is enabled)
|
||||||
|
try:
|
||||||
|
from routes.auth import AUTH_ENABLED
|
||||||
|
from routes.auth.middleware import AuthMiddleware
|
||||||
|
|
||||||
if AUTH_ENABLED:
|
if AUTH_ENABLED:
|
||||||
app.add_middleware(AuthMiddleware)
|
app.add_middleware(AuthMiddleware)
|
||||||
logging.info("Authentication system enabled")
|
logging.info("Authentication system enabled")
|
||||||
else:
|
else:
|
||||||
logging.info("Authentication system disabled")
|
logging.info("Authentication system disabled")
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning(f"Auth system initialization failed or unavailable: {e}")
|
||||||
|
|
||||||
# Register routers with URL prefixes
|
# Register routers with URL prefixes
|
||||||
|
from routes.auth.auth import router as auth_router
|
||||||
|
from routes.system.config import router as config_router
|
||||||
|
from routes.core.search import router as search_router
|
||||||
|
from routes.auth.credentials import router as credentials_router
|
||||||
|
from routes.content.album import router as album_router
|
||||||
|
from routes.content.track import router as track_router
|
||||||
|
from routes.content.playlist import router as playlist_router
|
||||||
|
from routes.content.artist import router as artist_router
|
||||||
|
from routes.system.progress import router as prgs_router
|
||||||
|
from routes.core.history import router as history_router
|
||||||
|
|
||||||
app.include_router(auth_router, prefix="/api/auth", tags=["auth"])
|
app.include_router(auth_router, prefix="/api/auth", tags=["auth"])
|
||||||
|
|
||||||
# Include SSO router if available
|
# Include SSO router if available
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
name: spotizerr
|
name: spotizerr
|
||||||
services:
|
services:
|
||||||
spotizerr:
|
spotizerr:
|
||||||
image: cooldockerizer93/spotizerr
|
image: cooldockerizer93/spotizerr:beta
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
- ./downloads:/app/downloads
|
- ./downloads:/app/downloads
|
||||||
@@ -9,6 +9,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 7171:7171
|
- 7171:7171
|
||||||
container_name: spotizerr-app
|
container_name: spotizerr-app
|
||||||
|
user: "1000:1000" # spotizerr user:group ids
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
|
|||||||
@@ -1,91 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Set umask if UMASK variable is provided
|
|
||||||
if [ -n "${UMASK}" ]; then
|
|
||||||
umask "${UMASK}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Compose Redis URLs from base variables if not explicitly provided
|
|
||||||
if [ -z "${REDIS_URL}" ]; then
|
|
||||||
REDIS_HOST=${REDIS_HOST:-redis}
|
|
||||||
REDIS_PORT=${REDIS_PORT:-6379}
|
|
||||||
REDIS_DB=${REDIS_DB:-0}
|
|
||||||
|
|
||||||
if [ -n "${REDIS_PASSWORD}" ]; then
|
|
||||||
if [ -n "${REDIS_USERNAME}" ]; then
|
|
||||||
AUTH_PART="${REDIS_USERNAME}:${REDIS_PASSWORD}@"
|
|
||||||
else
|
|
||||||
AUTH_PART=":${REDIS_PASSWORD}@"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
AUTH_PART=""
|
|
||||||
fi
|
|
||||||
export REDIS_URL="redis://${AUTH_PART}${REDIS_HOST}:${REDIS_PORT}/${REDIS_DB}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "${REDIS_BACKEND}" ]; then
|
|
||||||
export REDIS_BACKEND="${REDIS_URL}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Redis is now in a separate container so we don't need to start it locally
|
|
||||||
echo "Using Redis at ${REDIS_URL}"
|
|
||||||
|
|
||||||
# Check if both PUID and PGID are not set
|
|
||||||
if [ -z "${PUID}" ] && [ -z "${PGID}" ]; then
|
|
||||||
# Run as root directly
|
|
||||||
echo "Running as root user (no PUID/PGID specified)"
|
|
||||||
exec python app.py
|
|
||||||
else
|
|
||||||
# Verify both PUID and PGID are set
|
|
||||||
if [ -z "${PUID}" ] || [ -z "${PGID}" ]; then
|
|
||||||
echo "ERROR: Must supply both PUID and PGID or neither"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check for root user request
|
|
||||||
if [ "${PUID}" -eq 0 ] && [ "${PGID}" -eq 0 ]; then
|
|
||||||
echo "Running as root user (PUID/PGID=0)"
|
|
||||||
exec python app.py
|
|
||||||
else
|
|
||||||
# Check if the group with the specified GID already exists
|
|
||||||
if getent group "${PGID}" >/dev/null; then
|
|
||||||
# If the group exists, use its name instead of creating a new one
|
|
||||||
GROUP_NAME=$(getent group "${PGID}" | cut -d: -f1)
|
|
||||||
echo "Using existing group: ${GROUP_NAME} (GID: ${PGID})"
|
|
||||||
else
|
|
||||||
# If the group doesn't exist, create it
|
|
||||||
GROUP_NAME="appgroup"
|
|
||||||
groupadd -g "${PGID}" "${GROUP_NAME}"
|
|
||||||
echo "Created group: ${GROUP_NAME} (GID: ${PGID})"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if the user with the specified UID already exists
|
|
||||||
if getent passwd "${PUID}" >/dev/null; then
|
|
||||||
# If the user exists, use its name instead of creating a new one
|
|
||||||
USER_NAME=$(getent passwd "${PUID}" | cut -d: -f1)
|
|
||||||
echo "Using existing user: ${USER_NAME} (UID: ${PUID})"
|
|
||||||
else
|
|
||||||
# If the user doesn't exist, create it
|
|
||||||
USER_NAME="appuser"
|
|
||||||
useradd -u "${PUID}" -g "${GROUP_NAME}" -d /app "${USER_NAME}"
|
|
||||||
echo "Created user: ${USER_NAME} (UID: ${PUID})"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Ensure proper permissions for all app directories unless skipped via env var
|
|
||||||
if [ "${SKIP_SET_PERMISSIONS}" = "true" ] || [ "${SKIP_SET_PERMISSIONS}" = "1" ]; then
|
|
||||||
echo "SKIP_SET_PERMISSIONS is set; skipping permissions for /app/downloads /app/data /app/logs"
|
|
||||||
else
|
|
||||||
echo "Setting permissions for /app directories..."
|
|
||||||
chown -R "${USER_NAME}:${GROUP_NAME}" /app/downloads /app/data /app/logs || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Ensure Spotipy cache file exists and is writable (fast, local to container)
|
|
||||||
touch /app/.cache || true
|
|
||||||
chown "${USER_NAME}:${GROUP_NAME}" /app/.cache || true
|
|
||||||
|
|
||||||
# Run as specified user
|
|
||||||
echo "Starting application as ${USER_NAME}..."
|
|
||||||
exec gosu "${USER_NAME}" python app.py
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
@@ -7,3 +7,5 @@ bcrypt==4.2.1
|
|||||||
PyJWT==2.10.1
|
PyJWT==2.10.1
|
||||||
python-multipart==0.0.17
|
python-multipart==0.0.17
|
||||||
fastapi-sso==0.18.0
|
fastapi-sso==0.18.0
|
||||||
|
redis==5.0.7
|
||||||
|
async-timeout==4.0.3
|
||||||
|
|||||||
Reference in New Issue
Block a user