From 3f9cab66f53b4304d07f365cd8f6375f0ad1aa39 Mon Sep 17 00:00:00 2001 From: Kevin Whitaker Date: Wed, 7 Jan 2026 23:05:27 -0500 Subject: [PATCH] Redo frontend and dockerfile to use nginx and build in stages based on prod steps in romm --- Dockerfile | 129 ++++++++++++++++++++++++++++++--------- decode.js | 19 ++++++ default.conf | 73 ++++++++++++++++++++++ nginx.conf | 88 ++++++++++++++++++++++++++ supervisor/frontend.conf | 8 +-- 5 files changed, 285 insertions(+), 32 deletions(-) create mode 100644 decode.js create mode 100644 default.conf create mode 100644 nginx.conf diff --git a/Dockerfile b/Dockerfile index fa8f22b..9a5a9ff 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,46 @@ -FROM cloudron/base:5.0.0@sha256:04fd70dbd8ad6149c19de39e35718e024417c3e01dc9c6637eaf4a41ec4e596c - -RUN mkdir -p /app/code -WORKDIR /app/code ARG VERSION=4.5.0 +ARG ALPINE_VERSION=3.22 +ARG NODE_VERSION=20.19 + +# FRONTEND BUILD +FROM node:${NODE_VERSION}-trixie AS frontend-build +ARG VERSION +WORKDIR /front +RUN wget "https://github.com/rommapp/romm/archive/refs/tags/${VERSION}.tar.gz" && \ +tar xfz ${VERSION}.tar.gz --strip-components=2 -C /front romm-${VERSION}/frontend && rm ${VERSION}.tar.gz +RUN npm ci --ignore-scripts --no-audit --no-fund +RUN npm run build + +# FETCH EMULATORJS AND RUFFLE +FROM alpine:${ALPINE_VERSION} AS emulator-stage + +RUN apk add --no-cache \ + 7zip \ + wget \ + ca-certificates + +ARG EMULATORJS_VERSION=4.2.3 +ARG EMULATORJS_SHA256=07d451bc06fa3ad04ab30d9b94eb63ac34ad0babee52d60357b002bde8f3850b + +RUN wget "https://github.com/EmulatorJS/EmulatorJS/releases/download/v${EMULATORJS_VERSION}/${EMULATORJS_VERSION}.7z" && \ + echo "${EMULATORJS_SHA256} ${EMULATORJS_VERSION}.7z" | sha256sum -c - && \ + 7z x -y "${EMULATORJS_VERSION}.7z" -o/emulatorjs && \ + rm -f "${EMULATORJS_VERSION}.7z" + +ARG RUFFLE_VERSION=nightly-2025-08-14 +ARG RUFFLE_FILE=ruffle-nightly-2025_08_14-web-selfhosted.zip +ARG RUFFLE_SHA256=178870c5e7dd825a8df35920dfc5328d83e53f3c4d5d95f70b1ea9cd13494151 + +RUN wget "https://github.com/ruffle-rs/ruffle/releases/download/${RUFFLE_VERSION}/${RUFFLE_FILE}" && \ + echo "${RUFFLE_SHA256} ${RUFFLE_FILE}" | sha256sum -c - && \ + unzip -o "${RUFFLE_FILE}" -d /ruffle && \ + rm -f "${RUFFLE_FILE}" + +FROM cloudron/base:5.0.0@sha256:04fd70dbd8ad6149c19de39e35718e024417c3e01dc9c6637eaf4a41ec4e596c as cloudron-builder + +ARG NJS_VERSION=0.9.4 +ARG MODZIP_VERSION=1.3.0 # Prevent interactive prompts during installation ENV DEBIAN_FRONTEND=noninteractive @@ -31,22 +68,19 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ liblzma-dev \ libncurses5-dev \ libncursesw5-dev \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* + nginx \ + nginx-dev \ + libpcre3-dev \ + libssl-dev \ + libxml2-dev \ + libxslt-dev -# Clone release -RUN wget https://github.com/rommapp/romm/archive/refs/tags/$VERSION.tar.gz && \ -tar xfz $VERSION.tar.gz --strip-components=1 -C /app/code && rm $VERSION.tar.gz && rm -rf /app/code/backend/romm_test +WORKDIR / +RUN git clone -b ${NJS_VERSION} https://github.com/nginx/njs.git +RUN git clone -b ${MODZIP_VERSION} https://github.com/evanmiller/mod_zip.git -# Install nvm -ENV NVM_DIR="/app/code/.nvm" -RUN mkdir -p $NVM_DIR -RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash \ - && . "$NVM_DIR/nvm.sh" \ - && nvm install 18.20.8 \ - && nvm use 18.20.8 \ - && nvm alias default 18.20.8 -ENV PATH="$NVM_DIR/versions/node/v18.20.8/bin:$PATH" +WORKDIR /usr/share/nginx/src +RUN ./configure --add-dynamic-module=/njs/nginx --add-dynamic-module=/mod_zip --with-compat --builddir=. && make modules # Build and install RAHasher (optional for RA hashes) RUN git clone --recursive --branch 1.8.1 --depth 1 https://github.com/RetroAchievements/RALibretro.git /tmp/RALibretro @@ -56,20 +90,59 @@ RUN sed -i '22a #include ' ./src/Util.h \ ./src/libchdr/deps/zlib-1.3.1/gzlib.c \ ./src/libchdr/deps/zlib-1.3.1/gzread.c \ ./src/libchdr/deps/zlib-1.3.1/gzwrite.c \ - && make HAVE_CHD=1 -f ./Makefile.RAHasher \ - && cp ./bin64/RAHasher /usr/bin/RAHasher -RUN rm -rf /tmp/RALibretro + && make HAVE_CHD=1 -f ./Makefile.RAHasher -# Install frontend dependencies -WORKDIR /app/code/frontend -RUN npm install -RUN ln -s /tmp/vite-temp /app/code/frontend/node_modules/.vite-temp && ln -s /tmp/vite /app/code/frontend/node_modules/.vite && mkdir assets/romm && ln -s /app/data/assets /app/code/frontend/assets/romm/assets && ln -s /app/data/resources /app/code/frontend/assets/romm/resources -COPY --from=docker.io/rommapp/romm:$VERSION /var/www/html/assets/emulatorjs /app/code/frontend/assets/emulatorjs -COPY --from=docker.io/rommapp/romm:$VERSION /var/www/html/assets/ruffle /app/code/frontend/assets/ruffle +# Main image starts here +FROM cloudron/base:5.0.0@sha256:04fd70dbd8ad6149c19de39e35718e024417c3e01dc9c6637eaf4a41ec4e596c + +RUN mkdir -p /app/code +WORKDIR /app/code +ARG WEBSERVER_FOLDER=/app/code/frontend + +ARG UV_VERSION=0.8.24 +ARG VERSION + +# Prevent interactive prompts during installation +ENV DEBIAN_FRONTEND=noninteractive + +# Install system dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + git \ + libpq-dev \ + libmariadb-dev \ + curl \ + ca-certificates \ + 7zip \ + tzdata \ + nginx \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Clone release backend code +RUN wget "https://github.com/rommapp/romm/archive/refs/tags/${VERSION}.tar.gz" && \ +tar xfz ${VERSION}.tar.gz --strip-components=1 -C /app/code romm-${VERSION}/backend romm-${VERSION}/pyproject.toml romm-${VERSION}/uv.lock && rm ${VERSION}.tar.gz + +# Install RAHasher into image +COPY --from=cloudron-builder /tmp/RALibretro/bin64/RAHasher /usr/bin/RAHasher + +# Install frontend into image +WORKDIR ${WEBSERVER_FOLDER} +COPY --from=frontend-build /front/dist ${WEBSERVER_FOLDER} +COPY --from=frontend-build /front/assets ${WEBSERVER_FOLDER}/assets +COPY ./decode.js /etc/nginx/js/decode.js +COPY ./nginx.conf /etc/nginx/nginx.conf +COPY ./default.conf /etc/nginx/conf.d/default.conf +COPY --from=cloudron-builder /usr/share/nginx/src/ngx_http_js_module.so /usr/lib/nginx/modules/ +COPY --from=cloudron-builder /usr/share/nginx/src/ngx_http_zip_module.so /usr/lib/nginx/modules/ +RUN mkdir -p assets/romm && ln -s /app/data/assets ${WEBSERVER_FOLDER}/assets/romm/assets && ln -s /app/data/resources ${WEBSERVER_FOLDER}/assets/romm/resources + +# Copy emulator to web folder +COPY --from=emulator-stage /emulatorjs ${WEBSERVER_FOLDER}/assets/emulatorjs +COPY --from=emulator-stage /ruffle ${WEBSERVER_FOLDER}/assets/ruffle WORKDIR /app/code/ # Install uv for the non-root user -COPY --from=ghcr.io/astral-sh/uv:0.7.19 /uv /uvx /usr/local/bin/ +COPY --from=ghcr.io/astral-sh/uv:$UV_VERSION /uv /uvx /usr/local/bin/ # Install Python RUN mkdir /app/code/uv && uv python install -i /app/code/uv 3.13 diff --git a/decode.js b/decode.js new file mode 100644 index 0000000..b371fb1 --- /dev/null +++ b/decode.js @@ -0,0 +1,19 @@ +// Decode a Base64 encoded string received as a query parameter named 'value', +// and return the decoded value in the response body. +function decodeBase64(r) { + var encodedValue = r.args.value; + + if (!encodedValue) { + r.return(400, "Missing 'value' query parameter"); + return; + } + + try { + var decodedValue = atob(encodedValue); + r.return(200, decodedValue); + } catch (e) { + r.return(400, "Invalid Base64 encoding"); + } +} + +export default { decodeBase64 }; diff --git a/default.conf b/default.conf new file mode 100644 index 0000000..6d71196 --- /dev/null +++ b/default.conf @@ -0,0 +1,73 @@ +# Helper to get scheme regardless if we are behind a proxy or not +map $http_x_forwarded_proto $forwardscheme { + default $scheme; + https https; +} + +# COEP and COOP headers for cross-origin isolation, which are set only for the +# EmulatorJS player path, to enable SharedArrayBuffer support, which is needed +# for multi-threaded cores. +map $request_uri $coep_header { + default ""; + ~^/rom/.*/ejs$ "require-corp"; +} +map $request_uri $coop_header { + default ""; + ~^/rom/.*/ejs$ "same-origin"; +} + +server { + root /app/code/frontend; + listen 3000; + listen [::]:3000; + + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $forwardscheme; + + location / { + try_files $uri $uri/ /index.html; + proxy_redirect off; + add_header Access-Control-Allow-Origin *; + add_header Access-Control-Allow-Methods *; + add_header Access-Control-Allow-Headers *; + add_header Cross-Origin-Embedder-Policy $coep_header; + add_header Cross-Origin-Opener-Policy $coop_header; + } + + # Static files + location /assets { + try_files $uri $uri/ =404; + } + + # OpenAPI for swagger and redoc + location /openapi.json { + proxy_pass http://backend_server; + } + + # Backend api calls + location /api { + proxy_pass http://backend_server; + proxy_request_buffering off; + proxy_buffering off; + } + location ~ ^/(ws|netplay) { + proxy_pass http://backend_server; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + # Internally redirect download requests + location /library/ { + internal; + alias "/app/data/library/"; + } + + # Internal decoding endpoint, used to decode base64 encoded data + location /decode { + internal; + js_content decode.decodeBase64; + } +} diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..45f2a5d --- /dev/null +++ b/nginx.conf @@ -0,0 +1,88 @@ +load_module modules/ngx_http_js_module.so; +load_module modules/ngx_http_zip_module.so; + +worker_processes auto; +pid /tmp/nginx.pid; + +events { + worker_connections 768; + multi_accept on; +} + +http { + client_body_temp_path /tmp/client_body 1 2; + fastcgi_temp_path /tmp/fastcgi 1 2; + proxy_temp_path /tmp/proxy; + uwsgi_temp_path /tmp/uwsgi; + scgi_temp_path /tmp/scgi; + + sendfile on; + client_body_buffer_size 128k; + client_max_body_size 0; + client_header_buffer_size 1k; + large_client_header_buffers 4 16k; + send_timeout 600s; + keepalive_timeout 600s; + client_body_timeout 600s; + tcp_nopush on; + tcp_nodelay on; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE + ssl_prefer_server_ciphers on; + + js_import /etc/nginx/js/decode.js; + + map $time_iso8601 $date { + ~([^+]+)T $1; + } + map $time_iso8601 $time { + ~T([0-9:]+)\+ $1; + } + + # Map to extract the browser name (e.g., Chrome, Firefox, etc.) + map $http_user_agent $browser { + default "Unknown"; + "~Chrome/" "Chrome"; + "~Firefox/" "Firefox"; + "~Safari/" "Safari"; + "~Edge/" "Edge"; + "~Opera/" "Opera"; + } + + # Map to extract the OS (e.g., Windows, MacOS, Linux) + map $http_user_agent $os { + default "Unknown"; + "~Windows NT" "Windows"; + "~Macintosh" "macOS"; + "~Linux" "Linux"; + "~Android" "Android"; + "~iPhone" "iOS"; + } + + #INFO: [nginx][2023-11-14 09:20:29] 127.0.0.1 - -"GET / HTTP/1.1" 500 177 "-" "Mozilla/5.0 (X11; Linux x86_64)"rt=0.000 uct="-" uht="-" urt="-" + log_format romm_logs 'INFO: [RomM][nginx][$date $time] ' + '$remote_addr | $http_x_forwarded_for | ' + '$request_method $request_uri $status | $body_bytes_sent | ' + '$browser $os | $request_time'; + + access_log /dev/stdout romm_logs; + error_log /dev/stderr; + + gzip on; + gzip_proxied any; + gzip_vary on; + gzip_comp_level 6; + gzip_buffers 16 8k; + gzip_min_length 1024; + gzip_http_version 1.1; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + + upstream backend_server { + server 127.0.0.1:5000; + } + + include /etc/nginx/conf.d/*.conf; +} diff --git a/supervisor/frontend.conf b/supervisor/frontend.conf index 0c7cc56..e182874 100644 --- a/supervisor/frontend.conf +++ b/supervisor/frontend.conf @@ -1,12 +1,12 @@ [program:frontend] priority=50 -directory=/app/code/frontend -environment=HOME=/app/code -command=npm run dev -user=cloudron +directory=/tmp +command=/usr/sbin/nginx -g "daemon off;" +user=root autostart=true autorestart=true stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 +