Skip to content

Instantly share code, notes, and snippets.

@swerder
Created May 18, 2026 05:37
Show Gist options
  • Select an option

  • Save swerder/5eea4267cb116adbbb7565e2f7a95a7c to your computer and use it in GitHub Desktop.

Select an option

Save swerder/5eea4267cb116adbbb7565e2f7a95a7c to your computer and use it in GitHub Desktop.
zskarte nginx config to wrap password protected wms source incl caching
#!/bin/sh
set -e
if [ -z "${WMS_SECURE_DOMAIN}" ]; then
echo "ERROR: WMS_SECURE_DOMAIN is required"
exit 1
fi
export LOGIN_COMMENTED=""
if [ "${WMS_NO_LOGIN}" = "1" ]; then
export LOGIN_COMMENTED="#"
if [ "${WMS_CACHE_DISABLED}" = "1" ]; then
echo "ERROR: WMS_NO_LOGIN=1 and cache disabled (WMS_CACHE_DISABLED=1) does not make sense, in this case use it directly."
exit 2
fi
elif [ -n "${WMS_BASIC_USER}" ] && [ -n "${WMS_BASIC_PASS}" ]; then
export WMS_AUTH_HEADER="Basic $(printf '%s' "${WMS_BASIC_USER}:${WMS_BASIC_PASS}" | base64 | tr -d '\n')"
elif [ -n "${WMS_JWT_TOKEN}" ]; then
export WMS_AUTH_HEADER="Bearer ${WMS_JWT_TOKEN}"
elif [ "${WMS_OFFLINE_MODE}" = "0" ]; then
echo "ERROR: WMS_BASIC_USER and WMS_BASIC_PASS or an valid WMS_JWT_TOKEN is required"
exit 3
elif [ "${WMS_OFFLINE_MODE}" = "1" ] && [ "${WMS_CACHE_DISABLED}" = "1" ]; then
echo "ERROR: WMS_OFFLINE_MODE=1 and WMS_CACHE_DISABLED=1 does not make sense, nothing to server than."
exit 4
fi
if [ "${WMS_CACHE_DISABLED}" = "1" ]; then
export CACHE_COMMENTED="#"
else
export CACHE_COMMENTED=""
fi
if [ "${WMS_OFFLINE_MODE}" = "1" ]; then
export ONLINE_COMMENTED=""
export OFFLINE_COMMENTED="#"
else
export ONLINE_COMMENTED="#"
export OFFLINE_COMMENTED=""
fi
# define cache dir, keep unused/stale cache 400d
${CACHE_COMMENTED}proxy_cache_path /var/cache/nginx/wms_cache levels=1:2 keys_zone=wms_cache:20m max_size=1g inactive=400d use_temp_path=off;
server {
listen 80;
server_name localhost;
# Cache Keys based on URL with query params
${CACHE_COMMENTED}proxy_cache_key "$request_uri";
${CACHE_COMMENTED}proxy_cache wms_cache;
# cache validation
${CACHE_COMMENTED}proxy_ignore_headers Cache-Control Expires Set-Cookie Vary;
${CACHE_COMMENTED}proxy_cache_valid 200 302 ${WMS_CACHE_DURATION}; # Cache 30 day on success
${CACHE_COMMENTED}proxy_cache_valid 404 1m; # 404 only 1 min
${CACHE_COMMENTED}proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504;
${CACHE_COMMENTED}proxy_cache_revalidate off; # revalidate (by if-modified request) does not make sense, as WMS always return modified
location / {
set $cors_origin $http_origin;
# Cache bypass for OPTIONS requests
set $skip_cache 0;
if ($request_method = 'OPTIONS') {
set $skip_cache 1;
}
${CACHE_COMMENTED}proxy_cache_bypass $skip_cache;
${CACHE_COMMENTED}proxy_cache_lock on;
# 1. OPTIONS handling
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-Requested-With' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Max-Age' 1728000 always;
add_header 'Content-Type' 'text/plain; charset=UTF-8' always;
add_header 'Content-Length' 0 always;
return 204;
}
# 2. first set own CORS-Header
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-Requested-With' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
# 3. XML Substitution (return wrapper url inside capabilities xml)
proxy_set_header Accept-Encoding "";
sub_filter_once off;
sub_filter_types text/xml application/xml;
sub_filter 'http://${WMS_SECURE_DOMAIN}' '$scheme://$host:${WMS_WRAPPER_PORT}';
sub_filter 'https://${WMS_SECURE_DOMAIN}' '$scheme://$host:${WMS_WRAPPER_PORT}';
sub_filter '${WMS_SECURE_DOMAIN}' '$host:${WMS_WRAPPER_PORT}';
# 4. Proxy
${CACHE_COMMENTED}proxy_ignore_client_abort on; # continue handling also if client abort
${OFFLINE_COMMENTED}proxy_connect_timeout 5s;
${OFFLINE_COMMENTED}proxy_send_timeout 30s;
${OFFLINE_COMMENTED}proxy_read_timeout 30s;
${OFFLINE_COMMENTED}proxy_pass https://${WMS_SECURE_DOMAIN};
${ONLINE_COMMENTED}proxy_connect_timeout 1s;
${ONLINE_COMMENTED}proxy_send_timeout 1s;
${ONLINE_COMMENTED}proxy_read_timeout 1s;
${ONLINE_COMMENTED}proxy_pass http://127.0.0.1:9999; # fake endpoint directly fail
proxy_set_header Host ${WMS_SECURE_DOMAIN};
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;
${LOGIN_COMMENTED}proxy_set_header Authorization '${WMS_AUTH_HEADER}';
proxy_ssl_server_name on;
# 5. remove remote CORS-Header (after proxy_pass)
proxy_hide_header 'Access-Control-Allow-Origin';
proxy_hide_header 'Access-Control-Allow-Methods';
proxy_hide_header 'Access-Control-Allow-Headers';
proxy_hide_header 'Access-Control-Allow-Credentials';
proxy_hide_header 'Access-Control-Max-Age';
proxy_hide_header 'Access-Control-Expose-Headers';
# 6. remove Cache Header from upstream
proxy_hide_header 'Cache-Control';
${CACHE_COMMENTED}proxy_hide_header 'Pragma';
${CACHE_COMMENTED}proxy_hide_header 'Expires';
${CACHE_COMMENTED}proxy_hide_header 'Age';
${CACHE_COMMENTED}proxy_hide_header 'Vary';
${CACHE_COMMENTED}proxy_hide_header 'X-Cache';
${CACHE_COMMENTED}proxy_hide_header 'X-Varnish';
${CACHE_COMMENTED}proxy_hide_header 'Set-Cookie';
${CACHE_COMMENTED}proxy_hide_header 'Last-Modified';
# 7. add Cache Header
add_header Cache-Control "public, max-age=86400" always; # 1 day
${CACHE_COMMENTED}add_header X-Cache-Status $upstream_cache_status always;
${CACHE_COMMENTED}add_header Last-Modified $upstream_http_date always; # use WMS/Cache date as Last-Modified
}
}
version: '3.8'
services:
wms-wrapper:
image: nginx:alpine
ports:
- "${WMS_WRAPPER_PORT:-4400}:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./default.conf.template:/etc/nginx/templates/default.conf.template:ro
- ./10-prepare-env.sh:/docker-entrypoint.d/10-prepare-env.envsh
- ./cache:/var/cache/nginx/wms_cache
environment:
- WMS_SECURE_DOMAIN=${WMS_SECURE_DOMAIN:-}
- WMS_WRAPPER_PORT=${WMS_WRAPPER_PORT:-4400}
- WMS_BASIC_USER=${WMS_BASIC_USER:-}
- WMS_BASIC_PASS=${WMS_BASIC_PASS:-}
- WMS_JWT_TOKEN=${WMS_JWT_TOKEN:-}
- WMS_NO_LOGIN=${WMS_NO_LOGIN:-0}
- WMS_CACHE_DISABLED=${WMS_CACHE_DISABLED:-0}
- WMS_CACHE_DURATION=${WMS_CACHE_DURATION:-30d}
- WMS_OFFLINE_MODE=${WMS_OFFLINE_MODE:-0}
restart: unless-stopped
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# add cache info to log
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"'
'cache: "$upstream_cache_status"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
include /etc/nginx/conf.d/*.conf;
}
@echo off
set WMS_SECURE_DOMAIN=secure.geowms.my.bl.ch
set WMS_WRAPPER_PORT=4400
set WMS_BASIC_USER="BL1000XXXX"
set WMS_BASIC_PASS="secretPassword"
::if jwt is used, define valid token here and renew and restart if required
::set WMS_JWT_TOKEN=eyJhb.....
::if no login is required but caching function should be use, set to 1
set WMS_NO_LOGIN=0
mkdir cache
set WMS_CACHE_DISABLED=0
set WMS_CACHE_DURATION=30d
::to only return already cached content set to 1
set WMS_OFFLINE_MODE=0
docker compose up -d
timeout /t 1 >nul
docker compose logs --tail=200 | findstr /i "ERROR"
if %errorlevel%==0 (
docker compose down
exit 1
)
if %WMS_OFFLINE_MODE% == 1 GOTO :EOF
if %WMS_CACHE_DISABLED% == 1 GOTO :EOF
echo INFO: wait to check upstream reachable
timeout /t 5 >nul
docker compose logs --tail=200 | findstr /i "host not found in upstream"
if %errorlevel%==0 (
echo ERROR: restart in offline mode
set WMS_OFFLINE_MODE=1
docker compose up -d
)
#!/bin/bash
export WMS_SECURE_DOMAIN=secure.geowms.my.bl.ch
export WMS_WRAPPER_PORT=4400
export WMS_BASIC_USER="BL1000XXXX"
export WMS_BASIC_PASS="secretPassword"
#if jwt is used, define valid token here and renew and restart if required
#export WMS_JWT_TOKEN=eyJhb.....
#if no login is required but caching function should be use, set to 1
export WMS_NO_LOGIN=0
mkdir -p cache
export WMS_CACHE_DISABLED=0
export WMS_CACHE_DURATION=30d
#to only return already cached content set to 1
export WMS_OFFLINE_MODE=0
docker compose up -d
sleep 1
if docker compose logs --tail=200 | grep "ERROR"; then
docker compose down
exit 1
fi
([ ${WMS_OFFLINE_MODE} == 1 ] || [ ${WMS_CACHE_DISABLED} == 1 ]) && exit
echo "INFO: wait to check upstream reachable"
sleep 5
if docker compose logs --tail=200 | grep "host not found in upstream"; then
echo "ERROR: restart in offline mode" >&2
export WMS_OFFLINE_MODE=1
docker compose up -d
fi
@echo off
docker compose down
#!/bin/bash
docker compose down
@swerder
Copy link
Copy Markdown
Author

swerder commented May 18, 2026

The files in this gist can be used to access password protected WMS sources from zskarte.ch
You need to have docker installed, adjust the credentials in the start script and run it.
To add the source to zskarte, you need to switch to expert view and than add the source, see https://www.zskarte.ch/help/expert-view/wms-source

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment