From fd20cd84e854f2abe2649517d15de5807490a29d Mon Sep 17 00:00:00 2001 From: Sam Chau Date: Mon, 19 Jan 2026 20:23:10 +1030 Subject: [PATCH] feat: add Docker deployment support --- .dockerignore | 85 ++++++++++++++++++++++ .gitignore | 1 + Dockerfile | 98 ++++++++++++++++++++++++++ Dockerfile.parser | 60 ++++++++++++++++ compose.dev.yml | 33 +++++++++ compose.yml | 29 ++++++++ deno.json | 6 +- docker/entrypoint.sh | 58 +++++++++++++++ src/hooks.server.ts | 5 +- src/lib/server/utils/logger/startup.ts | 37 ++++++++++ 10 files changed, 410 insertions(+), 2 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 Dockerfile.parser create mode 100644 compose.dev.yml create mode 100644 compose.yml create mode 100755 docker/entrypoint.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..67e948d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,85 @@ +# ============================================================================= +# Docker Build Exclusions +# ============================================================================= +# These files are NOT sent to Docker during build, making builds faster +# and images smaller. + +# ----------------------------------------------------------------------------- +# Dependencies (reinstalled during build) +# ----------------------------------------------------------------------------- +node_modules/ +.npm/ +.pnpm-store/ + +# ----------------------------------------------------------------------------- +# Build outputs (rebuilt during build) +# ----------------------------------------------------------------------------- +dist/ +.svelte-kit/ + +# ----------------------------------------------------------------------------- +# .NET build artifacts +# ----------------------------------------------------------------------------- +src/services/parser/bin/ +src/services/parser/obj/ + +# ----------------------------------------------------------------------------- +# Git (not needed in image) +# ----------------------------------------------------------------------------- +.git/ +.gitignore +.gitattributes + +# ----------------------------------------------------------------------------- +# IDE and editor files +# ----------------------------------------------------------------------------- +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# ----------------------------------------------------------------------------- +# Documentation (not needed in image) +# ----------------------------------------------------------------------------- +*.md +!README.md +docs/ +LICENSE + +# ----------------------------------------------------------------------------- +# Development and test files +# ----------------------------------------------------------------------------- +.env +.env.* +*.log +*.tmp +temp/ +coverage/ +.nyc_output/ + +# ----------------------------------------------------------------------------- +# Docker files themselves (prevent recursion) +# ----------------------------------------------------------------------------- +Dockerfile* +compose.yml +compose.yaml +docker-compose.yml +docker-compose.yaml +# Keep entrypoint script, ignore the rest +!docker/entrypoint.sh + +# ----------------------------------------------------------------------------- +# CI/CD +# ----------------------------------------------------------------------------- +.github/ +.gitlab-ci.yml +.travis.yml +Jenkinsfile + +# ----------------------------------------------------------------------------- +# Misc +# ----------------------------------------------------------------------------- +*.tgz +*.tar.gz +*.zip diff --git a/.gitignore b/.gitignore index bb64ede..35a4b0d 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ CLAUDE.md # Application /temp +/config # OS .DS_Store diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8527368 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,98 @@ +# ============================================================================= +# Profilarr Dockerfile +# ============================================================================= +# Multi-stage build for minimal final image size +# +# Build: docker build -t profilarr . +# Run: docker run -v ./config:/config -p 6868:6868 profilarr + +# ----------------------------------------------------------------------------- +# Stage 1: Build +# ----------------------------------------------------------------------------- +FROM denoland/deno:2.5.6 AS builder + +WORKDIR /build + +# Copy everything +COPY . . + +# Install dependencies (creates node_modules for npm packages) +RUN deno install --node-modules-dir + +# Build the application +# 1. Vite builds SvelteKit to dist/build/ +# 2. Deno compiles to standalone binary +ENV APP_BASE_PATH=/build/dist/build +RUN deno run -A npm:vite build +RUN deno compile \ + --no-check \ + --allow-net \ + --allow-read \ + --allow-write \ + --allow-env \ + --allow-ffi \ + --allow-run \ + --allow-sys \ + --target x86_64-unknown-linux-gnu \ + --output dist/build/profilarr \ + dist/build/mod.ts + +# ----------------------------------------------------------------------------- +# Stage 2: Runtime +# ----------------------------------------------------------------------------- +FROM debian:12-slim + +# Labels for container metadata +LABEL org.opencontainers.image.title="Profilarr" +LABEL org.opencontainers.image.description="Configuration management for Radarr and Sonarr" +LABEL org.opencontainers.image.source="https://github.com/Dictionarry-Hub/profilarr" +LABEL org.opencontainers.image.licenses="AGPL-3.0" + +# Install runtime dependencies +# - git: PCD repository operations (clone, pull, push) +# - tar: Backup creation and restoration +# - curl: Health checks +# - gosu: Drop privileges to non-root user +# - ca-certificates: HTTPS connections +RUN apt-get update && apt-get install -y --no-install-recommends \ + git \ + tar \ + curl \ + gosu \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get clean + +# Create application directory +WORKDIR /app + +# Copy built application from builder stage +COPY --from=builder /build/dist/build/profilarr /app/profilarr +COPY --from=builder /build/dist/build/server.js /app/server.js +COPY --from=builder /build/dist/build/static /app/static + +# Copy entrypoint script +COPY docker/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +# Create config directory +RUN mkdir -p /config + +# Environment variables +ENV PORT=6868 +ENV HOST=0.0.0.0 +ENV APP_BASE_PATH=/config +ENV TZ=UTC + +# Expose port +EXPOSE 6868 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \ + CMD curl -sf http://localhost:${PORT}/api/v1/health || exit 1 + +# Volume for persistent data +VOLUME /config + +# Entrypoint handles PUID/PGID/UMASK then runs the app +ENTRYPOINT ["/entrypoint.sh"] diff --git a/Dockerfile.parser b/Dockerfile.parser new file mode 100644 index 0000000..2df1d06 --- /dev/null +++ b/Dockerfile.parser @@ -0,0 +1,60 @@ +# ============================================================================= +# Profilarr Parser Dockerfile +# ============================================================================= +# .NET 8.0 microservice for parsing release titles +# This service is OPTIONAL - only needed for custom format/quality profile testing +# +# Build: docker build -f Dockerfile.parser -t profilarr-parser . +# Run: docker run -p 5000:5000 profilarr-parser + +# ----------------------------------------------------------------------------- +# Stage 1: Build +# ----------------------------------------------------------------------------- +FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS builder + +WORKDIR /build + +# Copy project file first for better layer caching +COPY src/services/parser/Parser.csproj ./ +RUN dotnet restore + +# Copy source and build +COPY src/services/parser/ ./ +RUN dotnet publish -c Release -o /app --no-restore + +# ----------------------------------------------------------------------------- +# Stage 2: Runtime +# ----------------------------------------------------------------------------- +FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine + +# Labels for container metadata +LABEL org.opencontainers.image.title="Profilarr Parser" +LABEL org.opencontainers.image.description="Release title parser for Profilarr (optional)" +LABEL org.opencontainers.image.source="https://github.com/Dictionarry-Hub/profilarr" +LABEL org.opencontainers.image.licenses="AGPL-3.0" + +WORKDIR /app + +# Copy built application +COPY --from=builder /app ./ + +# Create non-root user +RUN addgroup -g 1000 parser && \ + adduser -u 1000 -G parser -D -h /app parser + +# Switch to non-root user +USER parser + +# Environment variables +ENV ASPNETCORE_URLS=http://+:5000 +ENV ASPNETCORE_ENVIRONMENT=Production + +# Expose port +EXPOSE 5000 + +# Health check +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ + CMD wget -qO- http://localhost:5000/health || exit 1 + +# Run the application +ENTRYPOINT ["dotnet", "Parser.dll"] diff --git a/compose.dev.yml b/compose.dev.yml new file mode 100644 index 0000000..4d2700b --- /dev/null +++ b/compose.dev.yml @@ -0,0 +1,33 @@ +# Development compose - builds from source +# Usage: docker compose -f compose.dev.yml up --build + +services: + profilarr: + build: + context: . + dockerfile: Dockerfile + container_name: profilarr-dev + ports: + - "6868:6868" + volumes: + - ./config:/config + environment: + - PUID=1000 + - PGID=1000 + - UMASK=022 + - TZ=Etc/UTC + # - PORT=6868 + # - HOST=0.0.0.0 + - PARSER_HOST=parser + - PARSER_PORT=5000 + depends_on: + parser: + condition: service_healthy + + parser: + build: + context: . + dockerfile: Dockerfile.parser + container_name: profilarr-parser-dev + expose: + - "5000" diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..f3831e9 --- /dev/null +++ b/compose.yml @@ -0,0 +1,29 @@ +services: + profilarr: + image: santiagosayshey/profilarr:latest + container_name: profilarr + restart: unless-stopped + ports: + - "6868:6868" + volumes: + - ./config:/config + environment: + - PUID=1000 + - PGID=1000 + - UMASK=022 + - TZ=Etc/UTC + # - PORT=6868 + # - HOST=0.0.0.0 + - PARSER_HOST=parser + - PARSER_PORT=5000 + depends_on: + parser: + condition: service_healthy + + # Optional - only needed for CF/QP testing features + parser: + image: santiagosayshey/profilarr-parser:latest + container_name: profilarr-parser + restart: unless-stopped + expose: + - "5000" diff --git a/deno.json b/deno.json index 9cc9614..ee95cbb 100644 --- a/deno.json +++ b/deno.json @@ -40,7 +40,11 @@ "check:client": "npx svelte-check --tsconfig ./tsconfig.json", "test": "APP_BASE_PATH=./dist/test deno test src/tests --allow-read --allow-write --allow-env", "test:watch": "APP_BASE_PATH=./dist/test deno test src/tests --allow-read --allow-write --allow-env --watch", - "generate:api-types": "npx openapi-typescript docs/api/v1/openapi.yaml -o src/lib/api/v1.d.ts" + "generate:api-types": "npx openapi-typescript docs/api/v1/openapi.yaml -o src/lib/api/v1.d.ts", + "docker:build": "docker compose -f compose.dev.yml build --no-cache", + "docker:up": "docker compose -f compose.dev.yml up --build", + "docker:down": "docker compose -f compose.dev.yml down", + "docker:clean": "docker compose -f compose.dev.yml down -v --rmi local" }, "compilerOptions": { "lib": ["deno.window", "dom"], diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100755 index 0000000..a9f4a78 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,58 @@ +#!/bin/bash +# ============================================================================= +# Profilarr Container Entrypoint +# ============================================================================= +# Handles PUID/PGID/UMASK setup for proper file permissions +# All logging is handled by the application's startup module + +set -e + +# ----------------------------------------------------------------------------- +# Configuration with defaults +# ----------------------------------------------------------------------------- +PUID=${PUID:-1000} +PGID=${PGID:-1000} +UMASK=${UMASK:-022} + +# ----------------------------------------------------------------------------- +# Create group if it doesn't exist +# ----------------------------------------------------------------------------- +if ! getent group profilarr > /dev/null 2>&1; then + groupadd -g "${PGID}" profilarr +elif [ "$(getent group profilarr | cut -d: -f3)" != "${PGID}" ]; then + groupmod -g "${PGID}" profilarr 2>/dev/null || true +fi + +# ----------------------------------------------------------------------------- +# Create user if it doesn't exist +# ----------------------------------------------------------------------------- +if ! getent passwd profilarr > /dev/null 2>&1; then + useradd -u "${PUID}" -g "${PGID}" -d /config -s /bin/bash profilarr +elif [ "$(id -u profilarr)" != "${PUID}" ]; then + usermod -u "${PUID}" profilarr 2>/dev/null || true +fi + +# ----------------------------------------------------------------------------- +# Ensure user is in the correct group +# ----------------------------------------------------------------------------- +usermod -g "${PGID}" profilarr >/dev/null 2>&1 || true + +# ----------------------------------------------------------------------------- +# Set umask +# ----------------------------------------------------------------------------- +umask "${UMASK}" + +# ----------------------------------------------------------------------------- +# Create config directory structure if it doesn't exist +# ----------------------------------------------------------------------------- +mkdir -p /config/data /config/logs /config/backups /config/databases + +# ----------------------------------------------------------------------------- +# Fix ownership of config directory +# ----------------------------------------------------------------------------- +chown -R "${PUID}:${PGID}" /config + +# ----------------------------------------------------------------------------- +# Drop privileges and run the application +# ----------------------------------------------------------------------------- +exec gosu profilarr /app/profilarr diff --git a/src/hooks.server.ts b/src/hooks.server.ts index d7bddfa..b371919 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,5 +1,5 @@ import { config } from '$config'; -import { printBanner, getServerInfo } from '$logger/startup.ts'; +import { printBanner, getServerInfo, logContainerConfig } from '$logger/startup.ts'; import { logSettings } from '$logger/settings.ts'; import { logger } from '$logger/logger.ts'; import { db } from '$db/db.ts'; @@ -20,6 +20,9 @@ await runMigrations(); // Load log settings from database (must be after migrations) logSettings.load(); +// Log container config (if running in Docker) +await logContainerConfig(); + // Initialize PCD caches (must be after migrations and log settings) await pcdManager.initialize(); diff --git a/src/lib/server/utils/logger/startup.ts b/src/lib/server/utils/logger/startup.ts index 86befe4..7dc0e83 100644 --- a/src/lib/server/utils/logger/startup.ts +++ b/src/lib/server/utils/logger/startup.ts @@ -4,6 +4,7 @@ import { config } from '$config'; import { appInfoQueries } from '$db/queries/appInfo.ts'; +import { logger } from './logger.ts'; const BANNER = String.raw` _____.__.__ @@ -14,6 +15,42 @@ _____________ _____/ ____\__| | _____ ______________ |__| \/ `; +/** + * Check if running inside a Docker container + */ +function isDocker(): boolean { + try { + // Check for .dockerenv file (most reliable) + Deno.statSync('/.dockerenv'); + return true; + } catch { + // Check for docker in cgroup (fallback) + try { + const cgroup = Deno.readTextFileSync('/proc/1/cgroup'); + return cgroup.includes('docker'); + } catch { + return false; + } + } +} + +/** + * Log container configuration (only when running in Docker) + */ +export async function logContainerConfig(): Promise { + if (!isDocker()) return; + + await logger.info('Container initialized', { + source: 'Docker', + meta: { + puid: Deno.env.get('PUID') || '1000', + pgid: Deno.env.get('PGID') || '1000', + umask: Deno.env.get('UMASK') || '022', + tz: Deno.env.get('TZ') || 'UTC' + } + }); +} + export function printBanner(): void { const version = appInfoQueries.getVersion(); const url = config.serverUrl;