Dockerfile
Rama main · réplica de lectura del repositorio original.
# Stage 0: Download EmulatorJS assets
FROM alpine:3.19 AS emulatorjs
WORKDIR /emulatorjs
RUN apk add --no-cache curl unzip
# Pre-warm the EmulatorJS asset cache from the CDN. This is an optimization only:
# the runtime (EmulatorController) fetches any missing file from the same CDN on
# first use and caches it under /app/config/emulatorjs. So a CDN outage at build
# time must NOT fail the whole image build -- retry hard, then warn and continue
# ( -f keeps a 404 from being saved as a bogus asset ).
RUN mkdir -p /emulatorjs/data && \
for f in loader.js emulator.min.js emulator.min.css version.json GameManager.js \
gamepad.js nipplejs.js shaders.js storage.js socket.io.min.js; do \
curl -fsSL --retry 5 --retry-delay 3 --retry-connrefused --retry-all-errors \
-o "/emulatorjs/data/$f" "https://cdn.emulatorjs.org/latest/data/$f" \
|| echo "[emulatorjs] WARN: could not fetch $f at build time; runtime will cache it on first use"; \
done && \
echo "stable" > /emulatorjs/data/version.txt
# Pre-download ALL supported EmulatorJS cores (matches PlatformIdToCore in EmulatorController)
# Each core needs a .js loader and a -wasm.data binary; use || true per-line so missing CDN files don't break the build.
RUN mkdir -p /emulatorjs/data/cores && \
for CORE in nes snes n64 gb gbc gba nds vb \
segaMS segaMD segaGG segaSaturn sega32x segaCD \
psx psp \
atari2600 atari5200 atari7800 lynx jaguar \
arcade mame2003 3do pce; do \
curl -fsSL --retry 3 --retry-delay 2 --retry-connrefused --retry-all-errors -o "/emulatorjs/data/cores/${CORE}-wasm.data" \
"https://cdn.emulatorjs.org/latest/data/cores/${CORE}-wasm.data" 2>/dev/null || true; \
curl -fsSL --retry 3 --retry-delay 2 --retry-connrefused --retry-all-errors -o "/emulatorjs/data/cores/${CORE}.js" \
"https://cdn.emulatorjs.org/latest/data/cores/${CORE}.js" 2>/dev/null || true; \
done && \
echo "Core pre-download complete: $(ls /emulatorjs/data/cores/ | wc -l) files"
# Stage: Download SteamPrefill (LanCache prefill tool, orchestrated at runtime).
# Non-fatal: a GitHub-release outage must not break the image build; the LanCache
# prefill feature simply reports "unavailable" until the binary is present.
FROM alpine:3.19 AS steamprefill
ARG TARGETARCH
ARG STEAMPREFILL_VERSION=3.7.1
WORKDIR /steamprefill
RUN apk add --no-cache curl unzip
RUN case "$TARGETARCH" in \
amd64) SP_ARCH=linux-x64 ;; \
arm64) SP_ARCH=linux-arm64 ;; \
*) SP_ARCH=linux-x64 ;; \
esac && \
( curl -fsSL --retry 5 --retry-delay 3 --retry-connrefused --retry-all-errors \
-o /tmp/sp.zip "https://github.com/tpill90/steam-lancache-prefill/releases/download/v${STEAMPREFILL_VERSION}/SteamPrefill-${STEAMPREFILL_VERSION}-${SP_ARCH}.zip" \
&& unzip -oq /tmp/sp.zip -d /steamprefill \
&& rm -f /tmp/sp.zip \
&& ( [ -f /steamprefill/SteamPrefill ] || find /steamprefill -maxdepth 2 -type f -name 'SteamPrefill' -exec cp {} /steamprefill/SteamPrefill \; ) \
&& chmod +x /steamprefill/SteamPrefill \
) || echo "[steamprefill] WARN: download failed at build time; LanCache prefill will be unavailable until the image is rebuilt"
# Stage: Download BattleNetPrefill (same author/shape as SteamPrefill, non-fatal).
FROM alpine:3.19 AS battlenetprefill
ARG TARGETARCH
ARG BATTLENETPREFILL_VERSION=2.3.0
WORKDIR /battlenetprefill
RUN apk add --no-cache curl unzip
RUN case "$TARGETARCH" in \
amd64) A=linux-x64 ;; arm64) A=linux-arm64 ;; *) A=linux-x64 ;; \
esac && \
( curl -fsSL --retry 5 --retry-delay 3 --retry-connrefused --retry-all-errors \
-o /tmp/bn.zip "https://github.com/tpill90/battlenet-lancache-prefill/releases/download/v${BATTLENETPREFILL_VERSION}/BattleNetPrefill-${BATTLENETPREFILL_VERSION}-${A}.zip" \
&& unzip -oq /tmp/bn.zip -d /battlenetprefill && rm -f /tmp/bn.zip \
&& ( [ -f /battlenetprefill/BattleNetPrefill ] || find /battlenetprefill -maxdepth 2 -type f -name 'BattleNetPrefill' -exec cp {} /battlenetprefill/BattleNetPrefill \; ) \
&& chmod +x /battlenetprefill/BattleNetPrefill \
) || echo "[battlenetprefill] WARN: download failed at build time; Battle.net prefill will be unavailable"
# Stage: Download EpicPrefill (same author/shape, non-fatal).
FROM alpine:3.19 AS epicprefill
ARG TARGETARCH
ARG EPICPREFILL_VERSION=2.1.0
WORKDIR /epicprefill
RUN apk add --no-cache curl unzip
RUN case "$TARGETARCH" in \
amd64) A=linux-x64 ;; arm64) A=linux-arm64 ;; *) A=linux-x64 ;; \
esac && \
( curl -fsSL --retry 5 --retry-delay 3 --retry-connrefused --retry-all-errors \
-o /tmp/ep.zip "https://github.com/tpill90/epic-lancache-prefill/releases/download/v${EPICPREFILL_VERSION}/EpicPrefill-${EPICPREFILL_VERSION}-${A}.zip" \
&& unzip -oq /tmp/ep.zip -d /epicprefill && rm -f /tmp/ep.zip \
&& ( [ -f /epicprefill/EpicPrefill ] || find /epicprefill -maxdepth 2 -type f -name 'EpicPrefill' -exec cp {} /epicprefill/EpicPrefill \; ) \
&& chmod +x /epicprefill/EpicPrefill \
) || echo "[epicprefill] WARN: download failed at build time; Epic prefill will be unavailable"
# Stage 1: Build the Frontend (React)
FROM node:22 AS frontend
WORKDIR /src
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci --prefer-offline
COPY frontend/ ./frontend/
COPY tsconfig.json ./
COPY frontend/build/webpack.config.js ./frontend/build/
RUN npm run build
# Stage 2: Build the Backend (.NET)
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS backend
WORKDIR /source
ENV DOTNET_RUNNING_IN_CONTAINER=true
# Copy solution and build configuration files to root of source
COPY src/RetroArr.sln ./
COPY src/Directory.Build.props ./
COPY src/Directory.Build.targets ./
COPY src/NuGet.config ./
# Copy each project file explicitly into its own folder (relative to WORKDIR)
COPY src/RetroArr.Api.V3/*.csproj RetroArr.Api.V3/
COPY src/RetroArr.Common/*.csproj RetroArr.Common/
COPY src/RetroArr.Console/*.csproj RetroArr.Console/
COPY src/RetroArr.Core/*.csproj RetroArr.Core/
COPY src/RetroArr.Host/*.csproj RetroArr.Host/
COPY src/RetroArr.Http/*.csproj RetroArr.Http/
COPY src/RetroArr.SignalR/*.csproj RetroArr.SignalR/
COPY src/RetroArr.UsbHelper/*.csproj RetroArr.UsbHelper/
# Restore dependencies (Host project pulls in all runtime deps)
RUN --mount=type=cache,target=/root/.nuget/packages \
dotnet restore RetroArr.Host/RetroArr.Host.csproj
# Copy everything else
COPY src/ ./
# Inject ScreenScraper dev credentials at build time (replaced via GitHub Secrets)
ARG SCREENSCRAPER_DEVID=""
ARG SCREENSCRAPER_DEVPASSWORD=""
RUN if [ -n "$SCREENSCRAPER_DEVID" ]; then \
sed -i "s/%%SCREENSCRAPER_DEVID%%/${SCREENSCRAPER_DEVID}/g" RetroArr.Core/MetadataSource/ScreenScraper/ScreenScraperClient.cs; \
fi && \
if [ -n "$SCREENSCRAPER_DEVPASSWORD" ]; then \
sed -i "s/%%SCREENSCRAPER_DEVPASSWORD%%/${SCREENSCRAPER_DEVPASSWORD}/g" RetroArr.Core/MetadataSource/ScreenScraper/ScreenScraperClient.cs; \
fi
RUN --mount=type=cache,target=/root/.nuget/packages \
dotnet publish RetroArr.Host/RetroArr.Host.csproj -c Release -o /app/publish
# Stage 3: Final Runtime Image
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
# Install runtime dependencies for Switch USB support (Python + libusb),
# healthcheck (curl), and self-signed cert generation (openssl)
RUN apt-get update && apt-get install -y \
python3 \
python3-pip \
libusb-1.0-0 \
curl \
openssl \
&& rm -rf /var/lib/apt/lists/*
# Install pyusb
RUN pip3 install --break-system-packages pyusb
COPY --from=backend /app/publish .
# Ensure no personal configs are included in the image
RUN rm -f /app/config/*.json && rm -f /app/settings/*.json && rm -f /app/appsettings.Development.json
# Copy frontend artifacts to where the backend expects them
COPY --from=frontend /src/_output/UI ./_output/UI
# Copy CHANGELOG.md for the system/changelog API endpoint
COPY CHANGELOG.md /app/CHANGELOG.md
# Copy EmulatorJS assets (pre-downloaded during build)
COPY --from=emulatorjs /emulatorjs/data /app/config/emulatorjs
# Bundle the LanCache prefill tools (Steam / Battle.net / Epic). Each keeps its
# session/state in ./Config next to the binary; the entrypoint symlinks those to
# the persistent config volume.
COPY --from=steamprefill /steamprefill /opt/steamprefill
COPY --from=battlenetprefill /battlenetprefill /opt/battlenetprefill
COPY --from=epicprefill /epicprefill /opt/epicprefill
# Entrypoint script handles self-signed cert generation + dual-listener config
COPY scripts/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
# Create config, media and savestate directories; non-root user
RUN mkdir -p /app/config /app/savestates /media /home/retroarr && \
groupadd -g 1000 retroarr && \
useradd -u 1000 -g retroarr -s /usr/sbin/nologin -M -d /home/retroarr retroarr && \
chown -R retroarr:retroarr /app /media /home/retroarr && \
(chown -R retroarr:retroarr /opt/steamprefill /opt/battlenetprefill /opt/epicprefill 2>/dev/null || true)
# The prefill tools resolve a temp/manifest cache under $HOME, so the app user needs
# a writable home. The entrypoint sets HOME=/home/retroarr after the drop instead of
# an ENV here, so a root docker exec keeps /root and never reads startup files from a
# folder the app user owns.
# No USER here: the entrypoint starts as root, fixes the config folder's ownership
# and then drops to PUID/PGID (default 1000:1000).
# 2727 = HTTP, 2728 = HTTPS (optional; only enabled when RETROARR_HTTPS_PORT is set)
EXPOSE 2727 2728
ENV DOTNET_RUNNING_IN_CONTAINER=true
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
CMD curl -fsS http://127.0.0.1:2727/api/v3/system/status > /dev/null || exit 1
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]