modified: .gitignore

new file:   apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/logs/refs/heads/codex/add-environment-setup-in-conftest.py
	new file:   apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/logs/refs/heads/codex/add-logging-to-geocode.py
	new file:   apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/logs/refs/heads/codex/add-logging-to-route_metrics.py
	new file:   apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/logs/refs/heads/codex/add-logging-to-tracking-simulator.py
	new file:   apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/logs/refs/heads/codex/extend-sqlite-tuning-in-database.py
	new file:   apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/logs/refs/heads/codex/fix-route-handling-in-routing.py
	new file:   apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/logs/refs/heads/codex/handle-api-response-errors-in-routing.py
	new file:   apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/logs/refs/heads/codex/refactor-database-path-handling-in-database.py
	new file:   apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/logs/refs/heads/codex/update-fcm-message-construction-in-notifications.py
	new file:   apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/logs/refs/heads/codex/update-role-check-in-ws.py
	new file:   apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/logs/refs/heads/codex/update-user-seed-in-database.py
	new file:   apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/refs/heads/codex/add-environment-setup-in-conftest.py
	new file:   apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/refs/heads/codex/add-logging-to-geocode.py
	new file:   apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/refs/heads/codex/add-logging-to-route_metrics.py
	new file:   apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/refs/heads/codex/add-logging-to-tracking-simulator.py
	new file:   apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/refs/heads/codex/extend-sqlite-tuning-in-database.py
	new file:   apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/refs/heads/codex/fix-route-handling-in-routing.py
	new file:   apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/refs/heads/codex/handle-api-response-errors-in-routing.py
	new file:   apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/refs/heads/codex/refactor-database-path-handling-in-database.py
	new file:   apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/refs/heads/codex/update-fcm-message-construction-in-notifications.py
	new file:   apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/refs/heads/codex/update-role-check-in-ws.py
	new file:   apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/refs/heads/codex/update-user-seed-in-database.py
	renamed:    gitea/docker-compose.yml -> apps/gitea/docker-compose.yml
	new file:   apps/gramps/docker-compose.yml
	renamed:    nextcloud/Dockerfile -> apps/nextcloud/Dockerfile
	new file:   apps/nextcloud/docker-compose.yml
	renamed:    passbolt/Dockerfile -> apps/passbolt/Dockerfile
	renamed:    passbolt/docker-compose.yml -> apps/passbolt/docker-compose.yml
	renamed:    searxng/Dockerfile -> apps/searxng/Dockerfile
	renamed:    searxng/docker-compose.yml -> apps/searxng/docker-compose.yml
	renamed:    searxng/dockerfiles/docker-entrypoint.sh -> apps/searxng/dockerfiles/docker-entrypoint.sh
	renamed:    searxng/docs/conf.py -> apps/searxng/docs/conf.py
	renamed:    searxng/docs/user/.gitignore -> apps/searxng/docs/user/.gitignore
	renamed:    searxng/examples/basic_engine.py -> apps/searxng/examples/basic_engine.py
	renamed:    searxng/searx/__init__.py -> apps/searxng/searx/__init__.py
	renamed:    searxng/searx/answerers/__init__.py -> apps/searxng/searx/answerers/__init__.py
	renamed:    searxng/searx/answerers/random/answerer.py -> apps/searxng/searx/answerers/random/answerer.py
	renamed:    searxng/searx/answerers/statistics/answerer.py -> apps/searxng/searx/answerers/statistics/answerer.py
	renamed:    searxng/searx/autocomplete.py -> apps/searxng/searx/autocomplete.py
	renamed:    searxng/searx/babel_extract.py -> apps/searxng/searx/babel_extract.py
	renamed:    searxng/searx/botdetection/__init__.py -> apps/searxng/searx/botdetection/__init__.py
	renamed:    searxng/searx/botdetection/_helpers.py -> apps/searxng/searx/botdetection/_helpers.py
	renamed:    searxng/searx/botdetection/http_accept.py -> apps/searxng/searx/botdetection/http_accept.py
	renamed:    searxng/searx/botdetection/http_accept_encoding.py -> apps/searxng/searx/botdetection/http_accept_encoding.py
	renamed:    searxng/searx/botdetection/http_accept_language.py -> apps/searxng/searx/botdetection/http_accept_language.py
	renamed:    searxng/searx/botdetection/http_connection.py -> apps/searxng/searx/botdetection/http_connection.py
	renamed:    searxng/searx/botdetection/http_user_agent.py -> apps/searxng/searx/botdetection/http_user_agent.py
	renamed:    searxng/searx/botdetection/ip_limit.py -> apps/searxng/searx/botdetection/ip_limit.py
	renamed:    searxng/searx/botdetection/ip_lists.py -> apps/searxng/searx/botdetection/ip_lists.py
	renamed:    searxng/searx/botdetection/limiter.py -> apps/searxng/searx/botdetection/limiter.py
	renamed:    searxng/searx/botdetection/link_token.py -> apps/searxng/searx/botdetection/link_token.py
	renamed:    searxng/searx/compat.py -> apps/searxng/searx/compat.py
	renamed:    searxng/searx/data/__init__.py -> apps/searxng/searx/data/__init__.py
	renamed:    searxng/searx/enginelib/__init__.py -> apps/searxng/searx/enginelib/__init__.py
	renamed:    searxng/searx/enginelib/traits.py -> apps/searxng/searx/enginelib/traits.py
	renamed:    searxng/searx/engines/1337x.py -> apps/searxng/searx/engines/1337x.py
	renamed:    searxng/searx/engines/9gag.py -> apps/searxng/searx/engines/9gag.py
	renamed:    searxng/searx/engines/__init__.py -> apps/searxng/searx/engines/__init__.py
	renamed:    searxng/searx/engines/ahmia.py -> apps/searxng/searx/engines/ahmia.py
	renamed:    searxng/searx/engines/annas_archive.py -> apps/searxng/searx/engines/annas_archive.py
	renamed:    searxng/searx/engines/apkmirror.py -> apps/searxng/searx/engines/apkmirror.py
	renamed:    searxng/searx/engines/apple_app_store.py -> apps/searxng/searx/engines/apple_app_store.py
	renamed:    searxng/searx/engines/apple_maps.py -> apps/searxng/searx/engines/apple_maps.py
	renamed:    searxng/searx/engines/archlinux.py -> apps/searxng/searx/engines/archlinux.py
	renamed:    searxng/searx/engines/artic.py -> apps/searxng/searx/engines/artic.py
	renamed:    searxng/searx/engines/arxiv.py -> apps/searxng/searx/engines/arxiv.py
	renamed:    searxng/searx/engines/bandcamp.py -> apps/searxng/searx/engines/bandcamp.py
	renamed:    searxng/searx/engines/base.py -> apps/searxng/searx/engines/base.py
	renamed:    searxng/searx/engines/bing.py -> apps/searxng/searx/engines/bing.py
	renamed:    searxng/searx/engines/bing_images.py -> apps/searxng/searx/engines/bing_images.py
	renamed:    searxng/searx/engines/bing_news.py -> apps/searxng/searx/engines/bing_news.py
	renamed:    searxng/searx/engines/bing_videos.py -> apps/searxng/searx/engines/bing_videos.py
	renamed:    searxng/searx/engines/brave.py -> apps/searxng/searx/engines/brave.py
	renamed:    searxng/searx/engines/bt4g.py -> apps/searxng/searx/engines/bt4g.py
	renamed:    searxng/searx/engines/btdigg.py -> apps/searxng/searx/engines/btdigg.py
	renamed:    searxng/searx/engines/command.py -> apps/searxng/searx/engines/command.py
	renamed:    searxng/searx/engines/core.py -> apps/searxng/searx/engines/core.py
	renamed:    searxng/searx/engines/crossref.py -> apps/searxng/searx/engines/crossref.py
	renamed:    searxng/searx/engines/currency_convert.py -> apps/searxng/searx/engines/currency_convert.py
	renamed:    searxng/searx/engines/dailymotion.py -> apps/searxng/searx/engines/dailymotion.py
	renamed:    searxng/searx/engines/deepl.py -> apps/searxng/searx/engines/deepl.py
	renamed:    searxng/searx/engines/deezer.py -> apps/searxng/searx/engines/deezer.py
	renamed:    searxng/searx/engines/demo_offline.py -> apps/searxng/searx/engines/demo_offline.py
	renamed:    searxng/searx/engines/demo_online.py -> apps/searxng/searx/engines/demo_online.py
	renamed:    searxng/searx/engines/deviantart.py -> apps/searxng/searx/engines/deviantart.py
	renamed:    searxng/searx/engines/dictzone.py -> apps/searxng/searx/engines/dictzone.py
	renamed:    searxng/searx/engines/digbt.py -> apps/searxng/searx/engines/digbt.py
	renamed:    searxng/searx/engines/docker_hub.py -> apps/searxng/searx/engines/docker_hub.py
	renamed:    searxng/searx/engines/doku.py -> apps/searxng/searx/engines/doku.py
	renamed:    searxng/searx/engines/duckduckgo.py -> apps/searxng/searx/engines/duckduckgo.py
	renamed:    searxng/searx/engines/duckduckgo_definitions.py -> apps/searxng/searx/engines/duckduckgo_definitions.py
	renamed:    searxng/searx/engines/duckduckgo_images.py -> apps/searxng/searx/engines/duckduckgo_images.py
	renamed:    searxng/searx/engines/duckduckgo_weather.py -> apps/searxng/searx/engines/duckduckgo_weather.py
	renamed:    searxng/searx/engines/duden.py -> apps/searxng/searx/engines/duden.py
	renamed:    searxng/searx/engines/dummy-offline.py -> apps/searxng/searx/engines/dummy-offline.py
	renamed:    searxng/searx/engines/dummy.py -> apps/searxng/searx/engines/dummy.py
	renamed:    searxng/searx/engines/ebay.py -> apps/searxng/searx/engines/ebay.py
	renamed:    searxng/searx/engines/elasticsearch.py -> apps/searxng/searx/engines/elasticsearch.py
	renamed:    searxng/searx/engines/emojipedia.py -> apps/searxng/searx/engines/emojipedia.py
	renamed:    searxng/searx/engines/fdroid.py -> apps/searxng/searx/engines/fdroid.py
	renamed:    searxng/searx/engines/flickr.py -> apps/searxng/searx/engines/flickr.py
	renamed:    searxng/searx/engines/flickr_noapi.py -> apps/searxng/searx/engines/flickr_noapi.py
	renamed:    searxng/searx/engines/framalibre.py -> apps/searxng/searx/engines/framalibre.py
	renamed:    searxng/searx/engines/freesound.py -> apps/searxng/searx/engines/freesound.py
	renamed:    searxng/searx/engines/frinkiac.py -> apps/searxng/searx/engines/frinkiac.py
	renamed:    searxng/searx/engines/genius.py -> apps/searxng/searx/engines/genius.py
	renamed:    searxng/searx/engines/gentoo.py -> apps/searxng/searx/engines/gentoo.py
	renamed:    searxng/searx/engines/github.py -> apps/searxng/searx/engines/github.py
	renamed:    searxng/searx/engines/google.py -> apps/searxng/searx/engines/google.py
	renamed:    searxng/searx/engines/google_images.py -> apps/searxng/searx/engines/google_images.py
	renamed:    searxng/searx/engines/google_news.py -> apps/searxng/searx/engines/google_news.py
	renamed:    searxng/searx/engines/google_play.py -> apps/searxng/searx/engines/google_play.py
	renamed:    searxng/searx/engines/google_scholar.py -> apps/searxng/searx/engines/google_scholar.py
	renamed:    searxng/searx/engines/google_videos.py -> apps/searxng/searx/engines/google_videos.py
	renamed:    searxng/searx/engines/imdb.py -> apps/searxng/searx/engines/imdb.py
	renamed:    searxng/searx/engines/ina.py -> apps/searxng/searx/engines/ina.py
	renamed:    searxng/searx/engines/invidious.py -> apps/searxng/searx/engines/invidious.py
	renamed:    searxng/searx/engines/jisho.py -> apps/searxng/searx/engines/jisho.py
	renamed:    searxng/searx/engines/json_engine.py -> apps/searxng/searx/engines/json_engine.py
	renamed:    searxng/searx/engines/kickass.py -> apps/searxng/searx/engines/kickass.py
	renamed:    searxng/searx/engines/lemmy.py -> apps/searxng/searx/engines/lemmy.py
	renamed:    searxng/searx/engines/lingva.py -> apps/searxng/searx/engines/lingva.py
	renamed:    searxng/searx/engines/loc.py -> apps/searxng/searx/engines/loc.py
	renamed:    searxng/searx/engines/mediathekviewweb.py -> apps/searxng/searx/engines/mediathekviewweb.py
	renamed:    searxng/searx/engines/mediawiki.py -> apps/searxng/searx/engines/mediawiki.py
	renamed:    searxng/searx/engines/meilisearch.py -> apps/searxng/searx/engines/meilisearch.py
	renamed:    searxng/searx/engines/metacpan.py -> apps/searxng/searx/engines/metacpan.py
	renamed:    searxng/searx/engines/mixcloud.py -> apps/searxng/searx/engines/mixcloud.py
	renamed:    searxng/searx/engines/mongodb.py -> apps/searxng/searx/engines/mongodb.py
	renamed:    searxng/searx/engines/mysql_server.py -> apps/searxng/searx/engines/mysql_server.py
	renamed:    searxng/searx/engines/nyaa.py -> apps/searxng/searx/engines/nyaa.py
	renamed:    searxng/searx/engines/opensemantic.py -> apps/searxng/searx/engines/opensemantic.py
	renamed:    searxng/searx/engines/openstreetmap.py -> apps/searxng/searx/engines/openstreetmap.py
	renamed:    searxng/searx/engines/openverse.py -> apps/searxng/searx/engines/openverse.py
	renamed:    searxng/searx/engines/pdbe.py -> apps/searxng/searx/engines/pdbe.py
	renamed:    searxng/searx/engines/peertube.py -> apps/searxng/searx/engines/peertube.py
	renamed:    searxng/searx/engines/photon.py -> apps/searxng/searx/engines/photon.py
	renamed:    searxng/searx/engines/piped.py -> apps/searxng/searx/engines/piped.py
	renamed:    searxng/searx/engines/piratebay.py -> apps/searxng/searx/engines/piratebay.py
	renamed:    searxng/searx/engines/postgresql.py -> apps/searxng/searx/engines/postgresql.py
	renamed:    searxng/searx/engines/pubmed.py -> apps/searxng/searx/engines/pubmed.py
	renamed:    searxng/searx/engines/qwant.py -> apps/searxng/searx/engines/qwant.py
	renamed:    searxng/searx/engines/recoll.py -> apps/searxng/searx/engines/recoll.py
	renamed:    searxng/searx/engines/reddit.py -> apps/searxng/searx/engines/reddit.py
	renamed:    searxng/searx/engines/redis_server.py -> apps/searxng/searx/engines/redis_server.py
	renamed:    searxng/searx/engines/rumble.py -> apps/searxng/searx/engines/rumble.py
	renamed:    searxng/searx/engines/scanr_structures.py -> apps/searxng/searx/engines/scanr_structures.py
	renamed:    searxng/searx/engines/searchcode_code.py -> apps/searxng/searx/engines/searchcode_code.py
	renamed:    searxng/searx/engines/searx_engine.py -> apps/searxng/searx/engines/searx_engine.py
	renamed:    searxng/searx/engines/semantic_scholar.py -> apps/searxng/searx/engines/semantic_scholar.py
	renamed:    searxng/searx/engines/sepiasearch.py -> apps/searxng/searx/engines/sepiasearch.py
	renamed:    searxng/searx/engines/seznam.py -> apps/searxng/searx/engines/seznam.py
	renamed:    searxng/searx/engines/sjp.py -> apps/searxng/searx/engines/sjp.py
	renamed:    searxng/searx/engines/solidtorrents.py -> apps/searxng/searx/engines/solidtorrents.py
	renamed:    searxng/searx/engines/solr.py -> apps/searxng/searx/engines/solr.py
	renamed:    searxng/searx/engines/soundcloud.py -> apps/searxng/searx/engines/soundcloud.py
	renamed:    searxng/searx/engines/spotify.py -> apps/searxng/searx/engines/spotify.py
	renamed:    searxng/searx/engines/springer.py -> apps/searxng/searx/engines/springer.py
	renamed:    searxng/searx/engines/sqlite.py -> apps/searxng/searx/engines/sqlite.py
	renamed:    searxng/searx/engines/stackexchange.py -> apps/searxng/searx/engines/stackexchange.py
	renamed:    searxng/searx/engines/startpage.py -> apps/searxng/searx/engines/startpage.py
	renamed:    searxng/searx/engines/tagesschau.py -> apps/searxng/searx/engines/tagesschau.py
	renamed:    searxng/searx/engines/tineye.py -> apps/searxng/searx/engines/tineye.py
	renamed:    searxng/searx/engines/tokyotoshokan.py -> apps/searxng/searx/engines/tokyotoshokan.py
	renamed:    searxng/searx/engines/torznab.py -> apps/searxng/searx/engines/torznab.py
	renamed:    searxng/searx/engines/translated.py -> apps/searxng/searx/engines/translated.py
	renamed:    searxng/searx/engines/twitter.py -> apps/searxng/searx/engines/twitter.py
	renamed:    searxng/searx/engines/unsplash.py -> apps/searxng/searx/engines/unsplash.py
	renamed:    searxng/searx/engines/vimeo.py -> apps/searxng/searx/engines/vimeo.py
	renamed:    searxng/searx/engines/wikidata.py -> apps/searxng/searx/engines/wikidata.py
	renamed:    searxng/searx/engines/wikipedia.py -> apps/searxng/searx/engines/wikipedia.py
	renamed:    searxng/searx/engines/wolframalpha_api.py -> apps/searxng/searx/engines/wolframalpha_api.py
	renamed:    searxng/searx/engines/wolframalpha_noapi.py -> apps/searxng/searx/engines/wolframalpha_noapi.py
	renamed:    searxng/searx/engines/wordnik.py -> apps/searxng/searx/engines/wordnik.py
	renamed:    searxng/searx/engines/wttr.py -> apps/searxng/searx/engines/wttr.py
	renamed:    searxng/searx/engines/www1x.py -> apps/searxng/searx/engines/www1x.py
	renamed:    searxng/searx/engines/xpath.py -> apps/searxng/searx/engines/xpath.py
	renamed:    searxng/searx/engines/yacy.py -> apps/searxng/searx/engines/yacy.py
	renamed:    searxng/searx/engines/yahoo.py -> apps/searxng/searx/engines/yahoo.py
	renamed:    searxng/searx/engines/yahoo_news.py -> apps/searxng/searx/engines/yahoo_news.py
	renamed:    searxng/searx/engines/youtube_api.py -> apps/searxng/searx/engines/youtube_api.py
	renamed:    searxng/searx/engines/youtube_noapi.py -> apps/searxng/searx/engines/youtube_noapi.py
	renamed:    searxng/searx/engines/zlibrary.py -> apps/searxng/searx/engines/zlibrary.py
	renamed:    searxng/searx/exceptions.py -> apps/searxng/searx/exceptions.py
	renamed:    searxng/searx/external_bang.py -> apps/searxng/searx/external_bang.py
	renamed:    searxng/searx/external_urls.py -> apps/searxng/searx/external_urls.py
	renamed:    searxng/searx/flaskfix.py -> apps/searxng/searx/flaskfix.py
	renamed:    searxng/searx/infopage/__init__.py -> apps/searxng/searx/infopage/__init__.py
	renamed:    searxng/searx/locales.py -> apps/searxng/searx/locales.py
	renamed:    searxng/searx/metrics/__init__.py -> apps/searxng/searx/metrics/__init__.py
	renamed:    searxng/searx/metrics/error_recorder.py -> apps/searxng/searx/metrics/error_recorder.py
	renamed:    searxng/searx/metrics/models.py -> apps/searxng/searx/metrics/models.py
	renamed:    searxng/searx/network/__init__.py -> apps/searxng/searx/network/__init__.py
	renamed:    searxng/searx/network/client.py -> apps/searxng/searx/network/client.py
	renamed:    searxng/searx/network/network.py -> apps/searxng/searx/network/network.py
	renamed:    searxng/searx/network/raise_for_httperror.py -> apps/searxng/searx/network/raise_for_httperror.py
	renamed:    searxng/searx/plugins/__init__.py -> apps/searxng/searx/plugins/__init__.py
	renamed:    searxng/searx/plugins/ahmia_filter.py -> apps/searxng/searx/plugins/ahmia_filter.py
	renamed:    searxng/searx/plugins/hash_plugin.py -> apps/searxng/searx/plugins/hash_plugin.py
	renamed:    searxng/searx/plugins/hostname_replace.py -> apps/searxng/searx/plugins/hostname_replace.py
	renamed:    searxng/searx/plugins/limiter.py -> apps/searxng/searx/plugins/limiter.py
	renamed:    searxng/searx/plugins/oa_doi_rewrite.py -> apps/searxng/searx/plugins/oa_doi_rewrite.py
	renamed:    searxng/searx/plugins/search_on_category_select.py -> apps/searxng/searx/plugins/search_on_category_select.py
	renamed:    searxng/searx/plugins/self_info.py -> apps/searxng/searx/plugins/self_info.py
	renamed:    searxng/searx/plugins/tor_check.py -> apps/searxng/searx/plugins/tor_check.py
	renamed:    searxng/searx/plugins/tracker_url_remover.py -> apps/searxng/searx/plugins/tracker_url_remover.py
	renamed:    searxng/searx/plugins/vim_hotkeys.py -> apps/searxng/searx/plugins/vim_hotkeys.py
	renamed:    searxng/searx/preferences.py -> apps/searxng/searx/preferences.py
	renamed:    searxng/searx/query.py -> apps/searxng/searx/query.py
	renamed:    searxng/searx/redisdb.py -> apps/searxng/searx/redisdb.py
	renamed:    searxng/searx/redislib.py -> apps/searxng/searx/redislib.py
	renamed:    searxng/searx/results.py -> apps/searxng/searx/results.py
	renamed:    searxng/searx/search/__init__.py -> apps/searxng/searx/search/__init__.py
	renamed:    searxng/searx/search/checker/__init__.py -> apps/searxng/searx/search/checker/__init__.py
	renamed:    searxng/searx/search/checker/__main__.py -> apps/searxng/searx/search/checker/__main__.py
	renamed:    searxng/searx/search/checker/background.py -> apps/searxng/searx/search/checker/background.py
	renamed:    searxng/searx/search/checker/impl.py -> apps/searxng/searx/search/checker/impl.py
	renamed:    searxng/searx/search/checker/scheduler.py -> apps/searxng/searx/search/checker/scheduler.py
	renamed:    searxng/searx/search/models.py -> apps/searxng/searx/search/models.py
	renamed:    searxng/searx/search/processors/__init__.py -> apps/searxng/searx/search/processors/__init__.py
	renamed:    searxng/searx/search/processors/abstract.py -> apps/searxng/searx/search/processors/abstract.py
	renamed:    searxng/searx/search/processors/offline.py -> apps/searxng/searx/search/processors/offline.py
	renamed:    searxng/searx/search/processors/online.py -> apps/searxng/searx/search/processors/online.py
	renamed:    searxng/searx/search/processors/online_currency.py -> apps/searxng/searx/search/processors/online_currency.py
	renamed:    searxng/searx/search/processors/online_dictionary.py -> apps/searxng/searx/search/processors/online_dictionary.py
	renamed:    searxng/searx/search/processors/online_url_search.py -> apps/searxng/searx/search/processors/online_url_search.py
	renamed:    searxng/searx/settings.yml -> apps/searxng/searx/settings.yml
	renamed:    searxng/searx/settings_defaults.py -> apps/searxng/searx/settings_defaults.py
	renamed:    searxng/searx/settings_loader.py -> apps/searxng/searx/settings_loader.py
	renamed:    searxng/searx/static/plugins/external_plugins/.gitignore -> apps/searxng/searx/static/plugins/external_plugins/.gitignore
	renamed:    searxng/searx/static/themes/simple/.gitattributes -> apps/searxng/searx/static/themes/simple/.gitattributes
	renamed:    searxng/searx/static/themes/simple/.gitignore -> apps/searxng/searx/static/themes/simple/.gitignore
	renamed:    searxng/searx/sxng_locales.py -> apps/searxng/searx/sxng_locales.py
	renamed:    searxng/searx/tools/__init__.py -> apps/searxng/searx/tools/__init__.py
	renamed:    searxng/searx/tools/config.py -> apps/searxng/searx/tools/config.py
	renamed:    searxng/searx/unixthreadname.py -> apps/searxng/searx/unixthreadname.py
	renamed:    searxng/searx/utils.py -> apps/searxng/searx/utils.py
	renamed:    searxng/searx/version.py -> apps/searxng/searx/version.py
	renamed:    searxng/searx/webadapter.py -> apps/searxng/searx/webadapter.py
	renamed:    searxng/searx/webapp.py -> apps/searxng/searx/webapp.py
	renamed:    searxng/searx/webutils.py -> apps/searxng/searx/webutils.py
	renamed:    searxng/searxng_extra/__init__.py -> apps/searxng/searxng_extra/__init__.py
	renamed:    searxng/searxng_extra/standalone_searx.py -> apps/searxng/searxng_extra/standalone_searx.py
	renamed:    searxng/searxng_extra/update/__init__.py -> apps/searxng/searxng_extra/update/__init__.py
	renamed:    searxng/searxng_extra/update/update_ahmia_blacklist.py -> apps/searxng/searxng_extra/update/update_ahmia_blacklist.py
	renamed:    searxng/searxng_extra/update/update_currencies.py -> apps/searxng/searxng_extra/update/update_currencies.py
	renamed:    searxng/searxng_extra/update/update_engine_descriptions.py -> apps/searxng/searxng_extra/update/update_engine_descriptions.py
	renamed:    searxng/searxng_extra/update/update_engine_traits.py -> apps/searxng/searxng_extra/update/update_engine_traits.py
	renamed:    searxng/searxng_extra/update/update_external_bangs.py -> apps/searxng/searxng_extra/update/update_external_bangs.py
	renamed:    searxng/searxng_extra/update/update_firefox_version.py -> apps/searxng/searxng_extra/update/update_firefox_version.py
	renamed:    searxng/searxng_extra/update/update_osm_keys_tags.py -> apps/searxng/searxng_extra/update/update_osm_keys_tags.py
	renamed:    searxng/searxng_extra/update/update_pygments.py -> apps/searxng/searxng_extra/update/update_pygments.py
	renamed:    searxng/searxng_extra/update/update_wikidata_units.py -> apps/searxng/searxng_extra/update/update_wikidata_units.py
	renamed:    searxng/setup.py -> apps/searxng/setup.py
	renamed:    searxng/tests/__init__.py -> apps/searxng/tests/__init__.py
	renamed:    searxng/tests/robot/__init__.py -> apps/searxng/tests/robot/__init__.py
	renamed:    searxng/tests/robot/__main__.py -> apps/searxng/tests/robot/__main__.py
	renamed:    searxng/tests/robot/settings_robot.yml -> apps/searxng/tests/robot/settings_robot.yml
	renamed:    searxng/tests/robot/test_webapp.py -> apps/searxng/tests/robot/test_webapp.py
	renamed:    searxng/tests/unit/__init__.py -> apps/searxng/tests/unit/__init__.py
	renamed:    searxng/tests/unit/engines/test_command.py -> apps/searxng/tests/unit/engines/test_command.py
	renamed:    searxng/tests/unit/engines/test_xpath.py -> apps/searxng/tests/unit/engines/test_xpath.py
	renamed:    searxng/tests/unit/network/__init__.py -> apps/searxng/tests/unit/network/__init__.py
	renamed:    searxng/tests/unit/network/test_network.py -> apps/searxng/tests/unit/network/test_network.py
	renamed:    searxng/tests/unit/settings/empty_settings.yml -> apps/searxng/tests/unit/settings/empty_settings.yml
	renamed:    searxng/tests/unit/settings/syntaxerror_settings.yml -> apps/searxng/tests/unit/settings/syntaxerror_settings.yml
	renamed:    searxng/tests/unit/settings/test_settings.yml -> apps/searxng/tests/unit/settings/test_settings.yml
	renamed:    searxng/tests/unit/settings/user_settings.yml -> apps/searxng/tests/unit/settings/user_settings.yml
	renamed:    searxng/tests/unit/settings/user_settings_keep_only.yml -> apps/searxng/tests/unit/settings/user_settings_keep_only.yml
	renamed:    searxng/tests/unit/settings/user_settings_remove.yml -> apps/searxng/tests/unit/settings/user_settings_remove.yml
	renamed:    searxng/tests/unit/settings/user_settings_remove2.yml -> apps/searxng/tests/unit/settings/user_settings_remove2.yml
	renamed:    searxng/tests/unit/settings/user_settings_simple.yml -> apps/searxng/tests/unit/settings/user_settings_simple.yml
	renamed:    searxng/tests/unit/test_answerers.py -> apps/searxng/tests/unit/test_answerers.py
	renamed:    searxng/tests/unit/test_engines_init.py -> apps/searxng/tests/unit/test_engines_init.py
	renamed:    searxng/tests/unit/test_exceptions.py -> apps/searxng/tests/unit/test_exceptions.py
	renamed:    searxng/tests/unit/test_external_bangs.py -> apps/searxng/tests/unit/test_external_bangs.py
	renamed:    searxng/tests/unit/test_locales.py -> apps/searxng/tests/unit/test_locales.py
	renamed:    searxng/tests/unit/test_plugins.py -> apps/searxng/tests/unit/test_plugins.py
	renamed:    searxng/tests/unit/test_preferences.py -> apps/searxng/tests/unit/test_preferences.py
	renamed:    searxng/tests/unit/test_query.py -> apps/searxng/tests/unit/test_query.py
	renamed:    searxng/tests/unit/test_results.py -> apps/searxng/tests/unit/test_results.py
	renamed:    searxng/tests/unit/test_search.py -> apps/searxng/tests/unit/test_search.py
	renamed:    searxng/tests/unit/test_settings_loader.py -> apps/searxng/tests/unit/test_settings_loader.py
	renamed:    searxng/tests/unit/test_utils.py -> apps/searxng/tests/unit/test_utils.py
	renamed:    searxng/tests/unit/test_webadapter.py -> apps/searxng/tests/unit/test_webadapter.py
	renamed:    searxng/tests/unit/test_webapp.py -> apps/searxng/tests/unit/test_webapp.py
	renamed:    searxng/tests/unit/test_webutils.py -> apps/searxng/tests/unit/test_webutils.py
	renamed:    searxng/utils/build_env.py -> apps/searxng/utils/build_env.py
	renamed:    searxng/utils/filtron.sh -> apps/searxng/utils/filtron.sh
	renamed:    searxng/utils/lib.sh -> apps/searxng/utils/lib.sh
	renamed:    searxng/utils/lib_go.sh -> apps/searxng/utils/lib_go.sh
	renamed:    searxng/utils/lib_nvm.sh -> apps/searxng/utils/lib_nvm.sh
	renamed:    searxng/utils/lib_redis.sh -> apps/searxng/utils/lib_redis.sh
	renamed:    searxng/utils/lib_sxng_data.sh -> apps/searxng/utils/lib_sxng_data.sh
	renamed:    searxng/utils/lib_sxng_node.sh -> apps/searxng/utils/lib_sxng_node.sh
	renamed:    searxng/utils/lib_sxng_static.sh -> apps/searxng/utils/lib_sxng_static.sh
	renamed:    searxng/utils/lib_sxng_test.sh -> apps/searxng/utils/lib_sxng_test.sh
	renamed:    searxng/utils/lib_sxng_themes.sh -> apps/searxng/utils/lib_sxng_themes.sh
	renamed:    searxng/utils/lib_sxng_weblate.sh -> apps/searxng/utils/lib_sxng_weblate.sh
	renamed:    searxng/utils/lxc.sh -> apps/searxng/utils/lxc.sh
	renamed:    searxng/utils/morty.sh -> apps/searxng/utils/morty.sh
	renamed:    searxng/utils/searx.sh -> apps/searxng/utils/searx.sh
	renamed:    searxng/utils/searxng.sh -> apps/searxng/utils/searxng.sh
	renamed:    searxng/utils/searxng_check.py -> apps/searxng/utils/searxng_check.py
	renamed:    searxng/utils/templates/etc/searxng/settings.yml -> apps/searxng/utils/templates/etc/searxng/settings.yml
	new file:   apps/shift-recorder
	new file:   apps/stockfill
	new file:   core/authelia/configuration.yml
	new file:   core/authelia/users_database.yml
	new file:   core/crowdsec/Dockerfile
	new file:   core/crowdsec/data/detect.yaml
	new file:   core/docker-compose.yml
	new file:   core/test/Dockerfile
	new file:   core/test/docker-compose.yml
	new file:   core/test/exporter.py
	new file:   core/traefik/data/dynamic.yaml
	renamed:    traefik/data/plugins.yaml -> core/traefik/data/plugins.yaml
	new file:   core/traefik/dynamic.yml
	new file:   core/traefik/traefik.yml
	new file:   default-network.yml
	new file:   monitoring/docker-exporter/Dockerfile
	new file:   monitoring/docker-exporter/exporter.py
	new file:   monitoring/gotify/docker-compose.yml
	new file:   monitoring/gotify/docker-health-to-gotify.sh
	new file:   monitoring/grafana/docker-compose.yml
	new file:   monitoring/node-red/Dockerfile
	new file:   monitoring/node-red/data/test-container.sh
	new file:   monitoring/node-red/docker-compose.yml
	new file:   monitoring/portainer/docker-compose.yml
	new file:   monitoring/prometheus/docker-compose.yml
	new file:   monitoring/prometheus/prometheus.yml
	new file:   monitoring/prometheus/rules/alerts.yml
	new file:   monitoring/uptime-kuma/docker-compose.yml
	deleted:    nextcloud/docker-compose.yml
	new file:   services-up.sh
	deleted:    traefik/docker-compose.yml
	deleted:    traefik/traefik.Dockerfile
	modified:   update-containers.py
	modified:   update-containers.sh

	modified:   apps/shift-recorder (modified content)
	modified:   apps/stockfill (modified content)
This commit is contained in:
git
2026-03-31 19:59:49 +10:00
parent d5b6cb22cd
commit b71cd3fcbb
340 changed files with 2084 additions and 311 deletions
+54
View File
@@ -0,0 +1,54 @@
server.address: tcp://0.0.0.0:9091
log:
level: info
identity_validation.reset_password.jwt_secret: T72Xcxa4d7xpQRypFDZpunlZt0IjqspojmBlxBr69gnkRjzR144YgjZsgFYZK0gS
session:
secret: BYksO7YUAJ8gXx9Endgpe46RgB10nkeKpD1qcQPt0GuYGQm2pS2zjJtNOrCEqpav
cookies:
- domain: lan.ddnsgeek.com
authelia_url: https://auth.lan.ddnsgeek.com
storage:
encryption_key: N7mkWziClgDhLgZDRkRwU6jEHmGF6ciOt53pzoFcZ0meEV1AZCC5bWZd24jeu19y
local:
path: /config/data/db.sqlite3
authentication_backend:
file:
path: /config/users_database.yml
access_control:
default_policy: deny
rules:
# - domain: "*.lan.ddnsgeek.com"
# policy: two_factor
- domain: alertmanager.lan.ddnsgeek.com
resources:
- "^/api/.*"
policy: bypass
- domain: influxdb.lan.ddnsgeek.com
resources:
- "^/health"
policy: bypass
- domain: prometheus.lan.ddnsgeek.com
resources:
- "^/-/healthy"
policy: bypass
- domain: traefik.lan.ddnsgeek.com
resources:
- "^/metrics"
policy: bypass
- domain: "*.lan.ddnsgeek.com"
policy: two_factor
notifier:
filesystem:
filename: /config/data/notification.txt
+7
View File
@@ -0,0 +1,7 @@
users:
admin:
displayname: "Admin"
password: "$argon2id$v=19$m=65536,t=3,p=4$WWdEjxd4avk4SA6T1OMDmQ$ml7znY+R1STq4d1n+RQhIiRL2Hu6RDVPR5uC+zbjPe4"
email: wayne.bennett@live.com
groups:
- admins
+6
View File
@@ -0,0 +1,6 @@
FROM crowdsecurity/crowdsec:latest
COPY config/ /etc/crowdsec
# Install required components at build time
RUN cscli hub update && \
cscli collections install crowdsecurity/traefik --force
+1
View File
@@ -0,0 +1 @@
/staging/var/lib/crowdsec/data/detect.yaml
+123
View File
@@ -0,0 +1,123 @@
services:
traefik:
profiles: ["core","all","traefik"]
image: traefik:3
container_name: traefik
restart: always
read_only: true
hostname: traefik.lan.ddnsgeek.com
depends_on:
- error-pages
- authelia
- crowdsec
ports:
- "80:80"
- "443:443"
build:
context: ${PROJECT_ROOT}/core
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ${PROJECT_ROOT}/core/traefik/data/letsencrypt:/letsencrypt
- ${PROJECT_ROOT}/core/traefik/data/logs:/logs
- ${PROJECT_ROOT}/core/traefik/dynamic.yml:/etc/traefik/dynamic.yml:ro
- ${PROJECT_ROOT}/core/traefik/traefik.yml:/etc/traefik/traefik.yml:ro
- ${PROJECT_ROOT}/core/traefik/data/plugins:/plugins-storage
healthcheck:
test: traefik healthcheck --ping
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik.rule=Host(`traefik.lan.ddnsgeek.com`)"
- "traefik.http.routers.traefik.service=api@internal"
- "traefik.http.routers.traefik.entrypoints=websecure"
- "traefik.http.routers.traefik.tls.certresolver=myresolver"
- "traefik.http.routers.traefik.middlewares=authelia"
- "io.portainer.accesscontrol.public"
- "traefik.docker.network=core_traefik"
- "traefik.http.routers.traefik.observability.tracing=true"
networks:
# - reverse_proxy
# - prometheus_edge
- traefik
crowdsec:
# image: crowdsecurity/crowdsec:latest
profiles: ["core","all","traefik"]
build: ${PROJECT_ROOT}/core/crowdsec
container_name: crowdsec
restart: always
environment:
- COLLECTIONS=crowdsecurity/traefik
volumes:
- ${PROJECT_ROOT}/core/crowdsec/logs:/logs:ro
- ${PROJECT_ROOT}/core/crowdsec/data:/var/lib/crowdsec/data
- ${PROJECT_ROOT}/core/crowdsec/config:/etc/crowdsec
networks:
# - reverse_proxy
- traefik
healthcheck:
test: ["CMD-SHELL", "cscli metrics || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 15s
error-pages:
profiles: ["core","all","traefik"]
image: tarampampam/error-pages:3
restart: always
container_name: error-pages
read_only: true
environment:
TEMPLATE_NAME: app-down
networks:
# - reverse_proxy
- traefik
hostname: error-pages
labels:
- "traefik.enable=true"
# use as "fallback" for any NON-registered services (with priority below normal)
- "traefik.http.routers.error-pages-router.rule=HostRegexp(`{host:.+}`)"
# should say that all of your services work on https
- "traefik.http.routers.error-pages-router.entrypoints=web"
- "traefik.http.routers.error-pages-router.middlewares=error-pages-middleware"
# "errors" middleware settings
- "traefik.http.middlewares.error-pages-middleware.errors.status=400-599"
- "traefik.http.middlewares.error-pages-middleware.errors.service=error-pages-service"
- "traefik.http.middlewares.error-pages-middleware.errors.query=/{status}.html"
# define service properties
- "traefik.http.services.error-pages-service.loadbalancer.server.port=8080"
- "io.portainer.accesscontrol.public"
authelia:
profiles: ["core","all","traefik"]
image: authelia/authelia
restart: always
build:
context: ${PROJECT_ROOT}/core/authelia
volumes:
- ${PROJECT_ROOT}/core/authelia:/config
networks:
# - reverse_proxy
- traefik
container_name: authelia
labels:
- traefik.enable=true
- traefik.http.routers.authelia.rule=Host(`auth.lan.ddnsgeek.com`)
- traefik.http.routers.authelia.entrypoints=websecure
- traefik.http.routers.authelia.tls=true
- traefik.http.routers.authelia.tls.certresolver=myresolver
- io.portainer.accesscontrol.public
- traefik.http.middlewares.authelia.forwardauth.address=http://authelia:9091/api/verify?rd=https://auth.lan.ddnsgeek.com/
- traefik.http.middlewares.authelia.forwardauth.trustForwardHeader=true
- traefik.http.middlewares.authelia.forwardauth.authResponseHeaders=Remote-User,Remote-Groups
- traefik.http.middlewares.authelia.forwardauth.maxResponseBodySize=2097152
#networks:
# reverse_proxy:
# driver: bridge
# prometheus_edge:
# external: true
+17
View File
@@ -0,0 +1,17 @@
FROM debian:latest
#RUN groupadd -g 1000 appuser || true && \
# useradd -m -u 1000 -g 1000 -s /bin/bash appuser
#RUN groupadd -g 999 docker || true && usermod -aG docker appuser
WORKDIR /app
COPY exporter.py .
RUN mkdir -p /data
RUN apt update && apt install python3 pip -y
RUN pip install --no-cache-dir docker prometheus_client requests pyyaml --break-system-packages
#USER appuser
CMD ["bash"]
+60
View File
@@ -0,0 +1,60 @@
services:
update-test:
image: nginx:1.27.4
container_name: update-test
profiles: ["test"]
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost"] # returns 0 if Nginx is up
interval: 5s
timeout: 2s
retries: 3
start_period: 2s
docker-update-exporter-test:
profiles: ["test"]
build:
context: ${PROJECT_ROOT}/core/test
container_name: docker-update-exporter-test
stdin_open: true
tty: true
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ${PROJECT_ROOT}/monitoring/docker-exporter/data:/data:rw
# - ${PROJECT_ROOT}/services-up.sh:/app/services-up.sh:ro
- ${PROJECT_ROOT}:/compose
- ${PROJECT_ROOT}/default-environment.env:/compose/default-environment.env:ro
- ${PROJECT_ROOT}/default-network.yml:/compose/default-network.yml:ro
- ${PROJECT_ROOT}/core/docker-compose.yml:/compose/core/docker-compose.yml:ro
- ${PROJECT_ROOT}/monitoring/prometheus/docker-compose.yml:/compose/monitoring/prometheus/docker-compose.yml:ro
- ${PROJECT_ROOT}/monitoring/gotify/docker-compose.yml:/compose/monitoring/gotify/docker-compose.yml:ro
- ${PROJECT_ROOT}/monitoring/grafana/docker-compose.yml:/compose/monitoring/grafana/docker-compose.yml:ro
- ${PROJECT_ROOT}/monitoring/portainer/docker-compose.yml:/compose/monitoring/portainer/docker-compose.yml:ro
- ${PROJECT_ROOT}/monitoring/uptime-kuma/docker-compose.yml:/compose/monitoring/uptime-kuma/docker-compose.yml:ro
- ${PROJECT_ROOT}/apps/gitea/docker-compose.yml:/compose/apps/gitea/docker-compose.yml:ro
- ${PROJECT_ROOT}/apps/gramps/docker-compose.yml:/compose/apps/gramps/docker-compose.yml:ro
- ${PROJECT_ROOT}/apps/nextcloud/docker-compose.yml:/compose/apps/nextcloud/docker-compose.yml:ro
- ${PROJECT_ROOT}/apps/passbolt/docker-compose.yml:/compose/apps/passbolt/docker-compose.yml:ro
- ${PROJECT_ROOT}/apps/searxng/docker-compose.yml:/compose/apps/searxng/docker-compose.yml:ro
- ${PROJECT_ROOT}/apps/shift-recorder/docker-compose.yml:/compose/apps/shift-recorder/docker-compose.yml:ro
- ${PROJECT_ROOT}/apps/stockfill/docker-compose.yml:/compose/apps/stockfill/docker-compose.yml:ro
- ${PROJECT_ROOT}/monitoring/node-red/docker-compose.yml:/compose/monitoring/node-red/docker-compose.yml:ro
- ${PROJECT_ROOT}/core/test/docker-compose.yml:/compose/core/test/docker-compose.yml:ro
# ports:
# - "9105:9105"
restart: unless-stopped
networks:
# - edge
- monitor
# healthcheck:
# test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:9105/metrics')"]
# interval: 30s
# timeout: 5s
# retries: 3
# start_period: 10s
+325
View File
@@ -0,0 +1,325 @@
#!/usr/bin/env python3
import os
import re
import time
import json
import docker
import requests
import yaml
from prometheus_client import Gauge, start_http_server
# --- Config ---
EXPORTER_PORT = 9105
CHECK_INTERVAL = 60
CACHE_TTL = 6 * 3600
SERVICES_UP_SCRIPT = "/compose/services-up.sh"
CACHE_FILE = "/data/remote_digest_cache.json"
client = docker.from_env()
# --- Metrics ---
CONTAINER_UPDATE = Gauge(
"docker_container_update_available",
"1 if container image is out of date (compose drift or registry), 0 otherwise",
["container", "compose_image", "running_image", "com_docker_compose_project"]
)
LAST_CHECK = Gauge(
"docker_image_update_last_check_timestamp",
"Last time the update check ran (unix timestamp)"
)
# --- Persistent Cache ---
def load_cache():
if not os.path.exists(CACHE_FILE):
return {}
try:
with open(CACHE_FILE, "r") as f:
return json.load(f)
except Exception as e:
print(f"[cache] Failed to load cache: {e}")
return {}
def save_cache():
try:
os.makedirs(os.path.dirname(CACHE_FILE), exist_ok=True)
with open(CACHE_FILE, "w") as f:
json.dump(REMOTE_DIGEST_CACHE, f)
except Exception as e:
print(f"[cache] Failed to save cache: {e}")
REMOTE_DIGEST_CACHE = load_cache()
# --- Helpers ---
def get_project_prefix_from_script(script_path):
project_prefix = "core-" # fallback
if not os.path.exists(script_path):
return project_prefix
with open(script_path, "r") as f:
for line in f:
line = line.strip()
m = re.match(r'PROJECT\s*=\s*["\']?([^"\']+)["\']?', line)
if m:
project_prefix = m.group(1) + "-"
break
return project_prefix
def get_local_digest(image_name):
try:
img = client.images.get(image_name)
digests = img.attrs.get("RepoDigests", [])
if digests:
return digests[0].split("@")[1]
except Exception as e:
print(f"[local_digest] Error for {image_name}: {e}")
return None
def get_remote_digest(image_name):
now = time.time()
original = image_name
# Use cached value if still valid
if original in REMOTE_DIGEST_CACHE:
digest, ts = REMOTE_DIGEST_CACHE[original]
if now - ts < CACHE_TTL:
return digest
try:
if "/" not in image_name:
registry = "docker.io"
repo = "library/" + image_name
else:
parts = image_name.split("/")
if "." in parts[0] or ":" in parts[0]:
registry = parts[0]
repo = "/".join(parts[1:])
else:
registry = "docker.io"
repo = image_name
if ":" in repo:
repo, tag = repo.rsplit(":", 1)
else:
tag = "latest"
token = None
manifest_url = None
if registry in ["docker.io", "registry-1.docker.io"]:
token_res = requests.get(
"https://auth.docker.io/token",
params={
"service": "registry.docker.io",
"scope": f"repository:{repo}:pull"
},
timeout=10
)
token = token_res.json().get("token")
manifest_url = f"https://registry-1.docker.io/v2/{repo}/manifests/{tag}"
elif registry == "ghcr.io":
token_res = requests.get(
"https://ghcr.io/token",
params={"scope": f"repository:{repo}:pull"},
timeout=10
)
token = token_res.json().get("token")
manifest_url = f"https://ghcr.io/v2/{repo}/manifests/{tag}"
else:
return None
res = requests.get(
manifest_url,
headers={
"Authorization": f"Bearer {token}" if token else "",
"Accept": "application/vnd.docker.distribution.manifest.v2+json"
},
timeout=10
)
if res.status_code == 200:
digest = res.headers.get("Docker-Content-Digest")
# Save to persistent cache
REMOTE_DIGEST_CACHE[original] = (digest, now)
save_cache()
return digest
except Exception as e:
print(f"[remote_digest] Error for {image_name}: {e}")
return None
def get_compose_files_from_script(script_path):
files = []
if not os.path.exists(script_path):
print(f"[compose_files] Script not found: {script_path}")
return files
base_dir = os.path.dirname(script_path)
with open(script_path, "r") as f:
content = f.read()
match = re.search(r'FILES\s*=\s*\((.*?)\)', content, re.DOTALL)
if match:
lines = match.group(1).splitlines()
for line in lines:
line = line.strip()
if line.startswith("-f"):
rel_path = line[2:].strip()
if rel_path:
full_path = os.path.join(base_dir, rel_path)
full_path = os.path.normpath(full_path)
print(f"[compose_files] {rel_path} -> {full_path}")
files.append(full_path)
return files
def parse_compose_files(compose_files):
"""Return mapping service_name -> (expected image, is_built)"""
service_to_image = {}
for f in compose_files:
if not os.path.exists(f):
continue
try:
with open(f, "r") as stream:
data = yaml.safe_load(stream)
services = data.get("services", {})
for service_name, service_def in services.items():
image = service_def.get("image")
is_built = False
if not image:
build_ctx = service_def.get("build")
if isinstance(build_ctx, dict):
context_path = build_ctx.get("context", ".")
dockerfile_path = os.path.join(
context_path,
build_ctx.get("dockerfile", "Dockerfile")
)
elif isinstance(build_ctx, str):
context_path = build_ctx
dockerfile_path = os.path.join(context_path, "Dockerfile")
else:
dockerfile_path = None
if dockerfile_path and os.path.exists(dockerfile_path):
try:
with open(dockerfile_path, "r") as df:
for line in df:
line = line.strip()
if line.upper().startswith("LABEL") and "image=" in line:
m = re.search(
r'image=["\']?([^"\']+)["\']?',
line
)
if m:
image = m.group(1)
break
except Exception as e:
print(f"[dockerfile] Error reading {dockerfile_path}: {e}")
if not image:
image = f"{service_name}:latest"
is_built = True
service_to_image[service_name] = (image, is_built)
except Exception as e:
print(f"[compose_parse] Failed {f}: {e}")
return service_to_image
def check_containers():
CONTAINER_UPDATE.clear()
PROJECT_PREFIX = get_project_prefix_from_script(SERVICES_UP_SCRIPT)
compose_files = get_compose_files_from_script(SERVICES_UP_SCRIPT)
service_to_image = parse_compose_files(compose_files)
for container in client.containers.list():
project_label = container.labels.get("com.docker.compose.project")
if not project_label:
continue # skip non-compose containers
service_label = container.labels.get("com.docker.compose.service")
running_image = container.attrs["Config"]["Image"]
compose_image = None
is_built = False
if service_label and service_label in service_to_image:
compose_image, is_built = service_to_image[service_label]
if is_built:
compose_image_name, _, _ = compose_image.partition(":")
compose_image = f"{PROJECT_PREFIX}{compose_image_name}"
update_flag = 0
if is_built:
if running_image != compose_image:
update_flag = 1
else:
local_digest = get_local_digest(running_image)
remote_digest = get_remote_digest(service_to_image[service_label][0])
if local_digest and remote_digest and local_digest != remote_digest:
update_flag = 1
else:
if running_image != compose_image:
update_flag = 1
else:
local_digest = get_local_digest(running_image)
remote_digest = get_remote_digest(running_image)
if local_digest and remote_digest and local_digest != remote_digest:
update_flag = 1
CONTAINER_UPDATE.labels(
container=container.name,
compose_image=compose_image if compose_image else "unknown",
running_image=running_image,
com_docker_compose_project=project_label
).set(update_flag)
print(
f"{container.name} | "
f"running={running_image} | "
f"compose={compose_image} | "
f"update={update_flag}"
)
LAST_CHECK.set(time.time())
if __name__ == "__main__":
start_http_server(EXPORTER_PORT)
print(f"Docker update exporter running on :{EXPORTER_PORT}")
while True:
try:
check_containers()
except Exception as e:
print(f"[main] Error: {e}")
time.sleep(CHECK_INTERVAL)
+22
View File
@@ -0,0 +1,22 @@
http:
middlewares:
crowdsec:
plugin:
crowdsec-bouncer:
crowdsecMode: live
crowdsecLapiKey: HeneLa2mazFVzl5+DQRKOdchBuJxKdjrHsHBE/03Acs
crowdsecLapiHost: crowdsec:8080
crowdsecLapiScheme: http
secHeaders:
headers:
browserXssFilter: true
contentTypeNosniff: true
frameDeny: true
# sslRedirect: true
#HSTS Configuration
stsIncludeSubdomains: true
stsPreload: true
stsSeconds: 15552000
forceSTSHeader: true
customFrameOptionsValue: "SAMEORIGIN"
+51
View File
@@ -0,0 +1,51 @@
http:
middlewares:
fail2ban:
plugin:
fail2ban:
logLevel: "INFO"
blacklist:
ip:
- 192.168.0.0/24
rules:
bantime: 3h
enabled: "true"
findtime: 10m
logencoding: UTF-8
maxretry: "4"
ports:
- 0:3305
- 3307:8000
whitelist:
ip:
- ::1
- 127.0.0.1
- 49.177.39.82
- 2001:8003:797c:a100:455f:513:d0f:767f
geoblock:
plugin:
GeoBlock:
allowLocalRequests: true
logLocalRequests: false
logAllowedRequests: false
logApiRequests: true
api: "https://get.geojs.io/v1/ip/country/{ip}"
apiTimeoutMs: 750 # optional
cacheSize: 15
forceMonthlyUpdate: true
allowUnknownCountries: false
unknownCountryApiResponse: "nil"
countries:
- AU
secHeaders:
headers:
browserXssFilter: true
contentTypeNosniff: true
frameDeny: true
# sslRedirect: true
#HSTS Configuration
stsIncludeSubdomains: true
stsPreload: true
stsSeconds: 15552000
forceSTSHeader: true
customFrameOptionsValue: "SAMEORIGIN"
+33
View File
@@ -0,0 +1,33 @@
http:
middlewares:
crowdsec:
plugin:
crowdsec-bouncer:
crowdsecMode: live
crowdsecLapiKey: HeneLa2mazFVzl5+DQRKOdchBuJxKdjrHsHBE/03Acs
crowdsecLapiHost: crowdsec:8080
crowdsecLapiScheme: http
secHeaders:
headers:
browserXssFilter: true
contentTypeNosniff: true
frameDeny: true
# sslRedirect: true
#HSTS Configuration
stsIncludeSubdomains: true
stsPreload: true
stsSeconds: 15552000
forceSTSHeader: true
customFrameOptionsValue: "SAMEORIGIN"
# tracing-middleware:
# tracing:
# serviceName: traefik
# sampleRate: 1.0
default-chain:
chain:
middlewares:
- secHeaders@file
- crowdsec@file
# - tracing-middleware@file
- error-pages-middleware@docker
+80
View File
@@ -0,0 +1,80 @@
log:
level: INFO
accessLog:
filePath: /logs/access.log
format: json
api:
dashboard: true
insecure: false
ping: {}
providers:
docker:
exposedByDefault: false
file:
filename: /etc/traefik/dynamic.yml
watch: true
entryPoints:
web:
address: ":80"
forwardedHeaders:
insecure: true
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
forwardedHeaders:
insecure: true
http:
middlewares:
- default-chain@file
# observability:
# tracing:
# enabled: true
# metrics:
# address: ":9100"
certificatesResolvers:
myresolver:
acme:
email: wayne.bennett@live.com
storage: /letsencrypt/acme.json
httpChallenge:
entryPoint: web
experimental:
plugins:
crowdsec-bouncer:
moduleName: github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
version: v1.4.2
metrics:
prometheus:
# entryPoint: metrics:9100 # optional, default is "metrics"
buckets:
- 0.1
- 0.3
- 1.2
- 5.0
addEntryPointsLabels: true # add labels for each entrypoint
addServicesLabels: true # add labels for each service
#tracing:
# serviceName: traefik
# sampleRate: 1.0
# otlp:
# grpc:
# endpoint: tempo:4317
# insecure: true
# enabled: true
# http:
# enabled: true
# endpoint: http://tempo:4318/v1/traces