feat: add Docker deployment support

This commit is contained in:
Sam Chau
2026-01-19 20:23:10 +10:30
parent dd77d1af35
commit fd20cd84e8
10 changed files with 410 additions and 2 deletions

85
.dockerignore Normal file
View File

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

1
.gitignore vendored
View File

@@ -14,6 +14,7 @@ CLAUDE.md
# Application
/temp
/config
# OS
.DS_Store

98
Dockerfile Normal file
View File

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

60
Dockerfile.parser Normal file
View File

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

33
compose.dev.yml Normal file
View File

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

29
compose.yml Normal file
View File

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

View File

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

58
docker/entrypoint.sh Executable file
View File

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

View File

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

View File

@@ -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<void> {
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;