feat(nginx): bake default white-label routing config, closes #1 (#2)
All checks were successful
Build and publish / build (push) Successful in 20s

## Summary

- Add `nginx.conf.template` with full routing: admin/CMS surfaces → `cms:5555`, uploads → `cms:5555` (cached), `/api` → `shop:9999` (no cache), `/` → `shop:9999` (cached)
- Update `docker-entrypoint.sh` to render template via `envsubst` at startup; skip if adapter has already mounted its own config
- Update `Dockerfile` to `COPY nginx.conf.template` into image
- Bump CHANGELOG to v0.1.1

## Test plan

- [ ] Build image locally: `docker build -t libreshop/nginx:test libreshop/nginx/`
- [ ] Smoke test white-label default: no env overrides → confirm rendered `/etc/nginx/nginx.conf` contains `cms:5555` and `shop:9999`
- [ ] Smoke test env override: `NGINX_UPSTREAM_SHOP=shop:8080` → confirm substitution
- [ ] Smoke test adapter override: mount custom `nginx.conf` via compose `configs:` → confirm "adapter-provided nginx.conf detected" log line
- [ ] Deploy libreshop/demo with `nginx:v0.1.1` → confirm `/admin` returns 200

Closes #1

Co-authored-by: Michael Czechowski <michael.c@re-cinq.com>
Reviewed-on: #2
This commit is contained in:
2026-04-29 19:38:52 +02:00
parent e849480a6e
commit 9cfd4163f6
4 changed files with 140 additions and 13 deletions

View File

@@ -2,7 +2,16 @@
All notable changes to libreshop/nginx are documented here.
## Unreleased
## v0.1.1 (Unreleased)
### Added
- Default white-label `nginx.conf` baked into the image. Routes the standard libreshop surface out of the box: `/admin`, `/upload`, `/content-manager`, `/content-type-builder`, `/i18n`, `/email`, `/user-permissions`, `/users-permissions`, `/documentation`, `/plugins`, `/uploads``${NGINX_UPSTREAM_CMS:-cms:5555}`; `/api` and `/``${NGINX_UPSTREAM_SHOP:-shop:9999}`. Cache zones, X-Forwarded headers, and a `/health` endpoint included.
- `docker-entrypoint.sh` envsubst pass over `nginx.conf.template` at startup with `NGINX_UPSTREAM_SHOP`, `NGINX_UPSTREAM_CMS`, `NGINX_CACHE_LIFETIME` (default `1h`).
- Adapter-override detection: if `/etc/nginx/nginx.conf` was replaced via compose `configs:` (i.e. doesn't carry the stock nginx `worker_processes auto;` line), the entrypoint leaves it untouched.
### Why
`libreshop/demo` standalone preview returned 404 on `/admin` because the image had no default routing. Adapters like mp work fine because they mount their own `nginx.conf`, but the toolkit must be runnable without an adapter.
## Unreleased
- Extracted from `mp/nginx/` (2026-04-29). The component history before
the extraction lives in the `muellerprints` repository.

View File

@@ -1,8 +1,10 @@
# Libre Shop - (Reverse) Proxy
# Version 1.0.0
# libreshop/nginx — reverse proxy fronting libreshop adapters.
FROM nginx:1.26.2
# White-label default config rendered at startup via envsubst.
# Adapters override the whole nginx.conf via compose `configs:`.
COPY nginx.conf.template /etc/nginx/nginx.conf.template
COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh

View File

@@ -1,19 +1,36 @@
#!/usr/bin/env bash
set -e
echo "Generated nginx.conf:"
echo ""
# Defaults — overridable via env in compose / docker run.
: "${NGINX_UPSTREAM_SHOP:=shop:9999}"
: "${NGINX_UPSTREAM_CMS:=cms:5555}"
: "${NGINX_CACHE_LIFETIME:=1h}"
export NGINX_UPSTREAM_SHOP NGINX_UPSTREAM_CMS NGINX_CACHE_LIFETIME
# Render the template only when the nginx.conf in place is the
# upstream nginx-image stock default. Adapters that mount their own
# config via compose `configs:` (e.g. mp) get the file replaced
# before this script runs; we detect that by the absence of the
# stock `worker_processes auto;` line and leave that file alone.
if [ -f /etc/nginx/nginx.conf.template ] \
&& grep -q "worker_processes auto;" /etc/nginx/nginx.conf 2>/dev/null; then
envsubst '${NGINX_UPSTREAM_SHOP} ${NGINX_UPSTREAM_CMS} ${NGINX_CACHE_LIFETIME}' \
< /etc/nginx/nginx.conf.template \
> /etc/nginx/nginx.conf
echo "→ libreshop default nginx.conf rendered from template"
else
echo "→ adapter-provided nginx.conf detected; leaving untouched"
fi
mkdir -p /cache/shop /cache/uploads /cache/api
echo "Effective nginx.conf:"
echo "---"
cat /etc/nginx/nginx.conf
echo "---"
mkdir -p /cache/shop
mkdir -p /cache/uploads
mkdir -p /cache/api
echo "Starting nginx"
echo "NGINX_DEBUG=${NGINX_DEBUG}"
# Start Nginx
if [ "$NGINX_DEBUG" = "true" ]; then
sed -i 's/error\.log warn/error.log debug/' /etc/nginx/nginx.conf
perl -i -pe 's|error_log /dev/stdout error|error_log /dev/stdout debug|' /etc/nginx/nginx.conf
exec nginx-debug -g 'daemon off;'
else
exec nginx -g 'daemon off;'

99
nginx.conf.template Normal file
View File

@@ -0,0 +1,99 @@
# nginx.conf.template — libreshop white-label default.
#
# The image's docker-entrypoint.sh runs envsubst over this template and
# writes the result to /etc/nginx/nginx.conf at startup. Adapters that
# need a wholly different topology can still mount their own config via
# compose `configs:` (see mp's compose.yml for an example) — the
# template is then ignored entirely.
#
# Substituted at startup:
# ${NGINX_UPSTREAM_SHOP} default shop:9999
# ${NGINX_UPSTREAM_CMS} default cms:5555
# ${NGINX_CACHE_LIFETIME} default 1h
#
# All other `$variable` tokens are nginx variables, left literal.
worker_processes 1;
events { worker_connections 1024; }
http {
log_format info '$time_iso8601 info: $request ($msec) $status $remote_addr - $remote_user $body_bytes_sent "$http_referer" "$http_user_agent"';
access_log /dev/stdout info;
error_log /dev/stdout error;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# Cache zones live under /cache (the image creates these dirs at
# startup). 2g caps each — adapters with heavier traffic can mount
# their own config to enlarge.
proxy_cache_path /cache/shop levels=1:2 keys_zone=shop_cache:5m max_size=2g inactive=30m;
proxy_cache_path /cache/uploads levels=1:2 keys_zone=uploads_cache:5m max_size=2g inactive=30m;
server {
listen 80 default_server;
server_name _;
# Trust forwarded headers when fronted by another proxy
# (e.g. caddy in the netcup edge network).
real_ip_header X-Forwarded-For;
set_real_ip_from 0.0.0.0/0;
location /health { return 200 '{"status":"ok"}'; }
# CMS admin + management surfaces.
location ~ ^/(upload|content-manager|content-type-builder|admin|i18n|email|user-permissions|users-permissions|documentation|plugins) {
proxy_pass http://${NGINX_UPSTREAM_CMS};
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 $http_x_forwarded_proto;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_cache off;
proxy_buffering off;
}
# CMS uploads (images, files).
location /uploads {
proxy_pass http://${NGINX_UPSTREAM_CMS};
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 $http_x_forwarded_proto;
proxy_cache uploads_cache;
proxy_cache_valid 200 ${NGINX_CACHE_LIFETIME};
proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504;
add_header X-Cache-Status $upstream_cache_status;
}
# Shop API (Nuxt server routes).
location /api {
proxy_pass http://${NGINX_UPSTREAM_SHOP};
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 $http_x_forwarded_proto;
proxy_cache off;
proxy_buffering off;
}
# Shop frontend (catch-all).
location / {
proxy_pass http://${NGINX_UPSTREAM_SHOP};
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 $http_x_forwarded_proto;
proxy_cache shop_cache;
proxy_cache_valid 200 10m;
proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504;
add_header X-Cache-Status $upstream_cache_status;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
}
}