feat: add nginx proxy for theme injection (cleaner approach)

This commit is contained in:
2025-11-09 08:43:36 +01:00
parent 428fd70ac3
commit d37ff0de47
5 changed files with 81 additions and 84 deletions

View File

@@ -1,14 +1,13 @@
services:
asciinema:
asciinema_backend:
image: ${ASCIINEMA_IMAGE:-ghcr.io/asciinema/asciinema-server:latest}
container_name: ${ASCIINEMA_COMPOSE_PROJECT_NAME}_app
container_name: ${ASCIINEMA_COMPOSE_PROJECT_NAME}_backend
restart: unless-stopped
networks:
- compose_network
volumes:
- asciinema_data:/var/opt/asciinema
- ./custom.exs:/opt/app/etc/custom.exs:ro
- ./theme:/opt/app/etc/theme:ro
environment:
SECRET_KEY_BASE: ${ASCIINEMA_SECRET_KEY}
URL_HOST: ${ASCIINEMA_TRAEFIK_HOST}
@@ -20,6 +19,18 @@ services:
SMTP_FROM_ADDRESS: ${EMAIL_FROM}
SIGN_UP_DISABLED: ${ASCIINEMA_SIGN_UP_DISABLED:-false}
DEFAULT_AVATAR: gravatar
asciinema:
image: nginx:alpine
container_name: ${ASCIINEMA_COMPOSE_PROJECT_NAME}_app
restart: unless-stopped
networks:
- compose_network
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./theme:/theme:ro
depends_on:
- asciinema_backend
labels:
- 'traefik.enable=${ASCIINEMA_TRAEFIK_ENABLED}'
# Main web interface - HTTP to HTTPS redirect
@@ -35,7 +46,7 @@ services:
- 'traefik.http.middlewares.${ASCIINEMA_COMPOSE_PROJECT_NAME}-compress.compress=true'
- 'traefik.http.routers.${ASCIINEMA_COMPOSE_PROJECT_NAME}-web-secure.middlewares=${ASCIINEMA_COMPOSE_PROJECT_NAME}-compress,security-headers@file'
- 'traefik.http.routers.${ASCIINEMA_COMPOSE_PROJECT_NAME}-web-secure.service=${ASCIINEMA_COMPOSE_PROJECT_NAME}'
- 'traefik.http.services.${ASCIINEMA_COMPOSE_PROJECT_NAME}.loadbalancer.server.port=4000'
- 'traefik.http.services.${ASCIINEMA_COMPOSE_PROJECT_NAME}.loadbalancer.server.port=8080'
# Admin interface - HTTP to HTTPS redirect
- 'traefik.http.routers.${ASCIINEMA_COMPOSE_PROJECT_NAME}-admin-web.middlewares=${ASCIINEMA_COMPOSE_PROJECT_NAME}-redirect-web-secure'
- 'traefik.http.routers.${ASCIINEMA_COMPOSE_PROJECT_NAME}-admin-web.rule=Host(`admin.${ASCIINEMA_TRAEFIK_HOST}`)'
@@ -48,7 +59,7 @@ services:
- 'traefik.http.routers.${ASCIINEMA_COMPOSE_PROJECT_NAME}-admin-web-secure.entrypoints=web-secure'
- 'traefik.http.routers.${ASCIINEMA_COMPOSE_PROJECT_NAME}-admin-web-secure.middlewares=${ASCIINEMA_COMPOSE_PROJECT_NAME}-auth,${ASCIINEMA_COMPOSE_PROJECT_NAME}-compress,security-headers@file'
- 'traefik.http.routers.${ASCIINEMA_COMPOSE_PROJECT_NAME}-admin-web-secure.service=${ASCIINEMA_COMPOSE_PROJECT_NAME}-admin'
- 'traefik.http.services.${ASCIINEMA_COMPOSE_PROJECT_NAME}-admin.loadbalancer.server.port=4002'
- 'traefik.http.services.${ASCIINEMA_COMPOSE_PROJECT_NAME}-admin.loadbalancer.server.port=8080'
# Network
- 'traefik.docker.network=${NETWORK_NAME}'
# Watchtower

View File

@@ -15,58 +15,3 @@ config :asciinema, Asciinema.Emails.Mailer,
verify: :verify_none,
versions: [:"tlsv1.2", :"tlsv1.3"]
]
# Custom theme configuration - inject custom CSS and favicon
defmodule AsciinemaWeb.CustomThemePlug do
@moduledoc """
Plug to inject custom CSS and favicon into HTML responses
"""
import Plug.Conn
def init(opts), do: opts
def call(conn, _opts) do
register_before_send(conn, fn conn ->
if html_response?(conn) do
inject_custom_theme(conn)
else
conn
end
end)
end
defp html_response?(conn) do
case get_resp_header(conn, "content-type") do
[content_type | _] -> String.contains?(content_type, "text/html")
[] -> false
end
end
defp inject_custom_theme(conn) do
custom_head = """
<link rel="stylesheet" href="/theme/custom.css">
<link rel="icon" type="image/svg+xml" href="/theme/favicon.svg">
"""
case conn.resp_body do
body when is_binary(body) ->
new_body = String.replace(body, "</head>", "#{custom_head}</head>")
%{conn | resp_body: new_body}
_ ->
conn
end
end
end
# Configure Phoenix endpoint to serve custom theme files
config :asciinema, AsciinemaWeb.Endpoint,
# Serve theme files from /opt/app/etc/theme
static_dirs: %{
at: "/theme",
from: "/opt/app/etc/theme",
gzip: false
}
# Inject the custom theme Plug into the endpoint
config :asciinema, AsciinemaWeb.Endpoint,
plug: AsciinemaWeb.CustomThemePlug

View File

@@ -0,0 +1,40 @@
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Map to determine which backend to use based on host
map $http_host $backend_port {
default 4000;
~^admin\. 4002;
}
server {
listen 8080;
server_name _;
# Proxy to actual asciinema server (port depends on subdomain)
location / {
proxy_pass http://asciinema_backend:$backend_port;
proxy_set_header Host $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 $scheme;
# Inject custom JavaScript into HTML responses
sub_filter '</head>' '<script src="/theme/inject.js"></script></head>';
sub_filter_once on;
sub_filter_types text/html;
}
# Serve theme files directly
location /theme/ {
alias /theme/;
expires 1d;
add_header Cache-Control "public, immutable";
}
}
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 932 B

After

Width:  |  Height:  |  Size: 29 KiB

22
asciinema/theme/inject.js Normal file
View File

@@ -0,0 +1,22 @@
// Inject custom theme CSS and favicon
(function() {
// Inject custom CSS
var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = '/theme/custom.css';
document.head.appendChild(link);
// Inject custom favicon
var favicon = document.createElement('link');
favicon.rel = 'icon';
favicon.type = 'image/svg+xml';
favicon.href = '/theme/favicon.svg';
// Remove existing favicons first
var existingFavicons = document.querySelectorAll('link[rel*="icon"]');
existingFavicons.forEach(function(el) {
el.remove();
});
document.head.appendChild(favicon);
})();