[{"content":"Using systemd timers Requirements Two parts working together:\nsystemd timer systemd service Example:\nborgmatic.service (i.e. /usr/lib/systemd/system/borgmatic.service) borgmatic.timer (i.e. /usr/lib/systemd/system/borgmatic.timer) Adjusting the timer Default Timer Listing /usr/lib/systemd/system/borgmatic.timer\n[Unit] Description=Run borgmatic backup [Timer] OnCalendar=daily Persistent=true RandomizedDelaySec=3h [Install] WantedBy=timers.target \u0026#9998; - From the Docs -\u0026#9656; Persistent=true If true, the time when the service unit was last triggered is stored on disk. When the timer is activated, the service unit is triggered immediately if it would have been triggered at least once during the time when the timer was inactive.\nRandomizedDelaySec=3h Delay the timer by a randomly selected, evenly distributed amount of time between 0 and the specified time value. Defaults to 0, indicating that no randomized delay shall be applied.\nCustom Timer Improvements for a consistent backup schedule.\nListing /usr/lib/systemd/system/borgmatic.timer\n[Unit] Description=Run borgmatic backup [Timer] OnCalendar=*-*-* 4:00 #fixed time every morning at 04:00 Persistent=true #RandomizedDelaySec=3h # disabled randomization for better monitoring compatibility [Install] WantedBy=timers.target# Applying the changes after editing the file: systemctl daemon-reload systemctl restart borgmatic.timer systemctl status borgmatic.timer Using Systemd Analyze If you want to get some more information about a specific setting you can use systemd-analyze\n╭─traefik@www.vaduzz.de in repo: garden on  main [?] took 0s ╰─λ systemd-analyze calendar \u0026#34;4:00\u0026#34; Original form: 4:00 Normalized form: *-*-* 04:00:00 Next elapse: Sat 2025-09-06 04:00:00 CEST (in UTC): Sat 2025-09-06 02:00:00 UTC From now: 3h 29min left References systemd.time ","permalink":"https://www.vaduzz.de/posts/systemd-timer/","summary":"\u003ch1 id=\"using-systemd-timers\"\u003eUsing systemd timers\u003c/h1\u003e\n\u003ch2 id=\"requirements\"\u003eRequirements\u003c/h2\u003e\n\u003cp\u003eTwo parts working together:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003esystemd timer\u003c/li\u003e\n\u003cli\u003esystemd service\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eExample:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eborgmatic.service (i.e. /usr/lib/systemd/system/borgmatic.service)\u003c/li\u003e\n\u003cli\u003eborgmatic.timer (i.e. /usr/lib/systemd/system/borgmatic.timer)\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"adjusting-the-timer\"\u003eAdjusting the timer\u003c/h2\u003e\n\u003ch3 id=\"default-timer\"\u003eDefault Timer\u003c/h3\u003e\n\u003cp\u003eListing \u003ccode\u003e/usr/lib/systemd/system/borgmatic.timer\u003c/code\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003eUnit\u003cspan class=\"o\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nv\"\u003eDescription\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003eRun borgmatic backup\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003eTimer\u003cspan class=\"o\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nv\"\u003eOnCalendar\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003edaily\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nv\"\u003ePersistent\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"nb\"\u003etrue\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nv\"\u003eRandomizedDelaySec\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e3h\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003eInstall\u003cspan class=\"o\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nv\"\u003eWantedBy\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003etimers.target\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cdiv class=\"details admonition note open\" onclick=\"this.classList.toggle('open')\"\u003e\n        \u003cdiv class=\"details-summary admonition-title\"\u003e\n            \u003cspan\u003e\u0026#9998; - From the Docs -\u003c/span\u003e\u003cspan class=\"details-icon\"\u003e\u0026#9656;\u003c/span\u003e\n        \u003c/div\u003e\n        \u003cdiv class=\"details-content\"\u003e\n            \u003cdiv class=\"admonition-content\"\u003e\u003cp\u003e\u003ccode\u003ePersistent=true\u003c/code\u003e\nIf true, the time when the service unit was last triggered is stored on disk. When the timer is activated, the service unit is triggered immediately if it would have been triggered at least once during the time when the timer was inactive.\u003c/p\u003e","title":"Systemd Timer"},{"content":"Enable Http3 with Traefik using Docker Compose Change Docker Compose File Because http/3 uses UDP we need to also take care of the firewall that usually blocks UDP traffic on HTTPS Port 443.\nTo enable http3 in Traefik V3, add the following to your docker-compose.yml file:\nservices: traefik: image: traefik:v3 container_name: \u0026#34;traefik\u0026#34; restart: unless-stopped command: - \u0026#34;--providers.docker=true\u0026#34; - \u0026#34;--providers.docker.exposedbydefault=false\u0026#34; - \u0026#34;--entryPoints.websecure.address=:443\u0026#34; # Add the following lines - \u0026#34;--entrypoints.websecure.http3=true\u0026#34; - \u0026#34;--entrypoints.websecure.http3.advertisedport=443\u0026#34; ports: # Docker only exposes TCP by default, so we need to expose UDP as well - \u0026#34;443:443/tcp\u0026#34; - \u0026#34;443:443/udp\u0026#34; Test if http3 is enabled ╭─traefik@www.vaduzz.de in repo: garden on  main [!?] took 0s ╰─λ curl -Iv --http3 https://www.vaduzz.de * Host www.vaduzz.de:443 was resolved. [...] * using HTTP/3 * [HTTP/3] [0] OPENED stream for https://www.vaduzz.de/ * [HTTP/3] [0] [:method: HEAD] * [HTTP/3] [0] [:scheme: https] * [HTTP/3] [0] [:authority: www.vaduzz.de] * [HTTP/3] [0] [:path: /] * [HTTP/3] [0] [user-agent: curl/8.15.0] * [HTTP/3] [0] [accept: */*] \u0026gt; HEAD / HTTP/3 \u0026gt; Host: www.vaduzz.de [...] ","permalink":"https://www.vaduzz.de/posts/traefik-http3/","summary":"\u003ch1 id=\"enable-http3-with-traefik-using-docker-compose\"\u003eEnable Http3 with Traefik using Docker Compose\u003c/h1\u003e\n\u003ch2 id=\"change-docker-compose-file\"\u003eChange Docker Compose File\u003c/h2\u003e\n\u003cp\u003eBecause http/3 uses UDP we need to also take care of the firewall that usually blocks UDP traffic on HTTPS Port 443.\u003c/p\u003e\n\u003cp\u003eTo enable http3 in Traefik V3, add the following to your \u003ccode\u003edocker-compose.yml\u003c/code\u003e file:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003eservices\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003etraefik\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003eimage\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003etraefik:v3\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003econtainer_name\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;traefik\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003erestart\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eunless-stopped\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003ecommand\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"s2\"\u003e\u0026#34;--providers.docker=true\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"s2\"\u003e\u0026#34;--providers.docker.exposedbydefault=false\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"s2\"\u003e\u0026#34;--entryPoints.websecure.address=:443\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"c\"\u003e# Add the following lines\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"s2\"\u003e\u0026#34;--entrypoints.websecure.http3=true\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"s2\"\u003e\u0026#34;--entrypoints.websecure.http3.advertisedport=443\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003eports\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"c\"\u003e# Docker only exposes TCP by default, so we need to expose UDP as well\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"s2\"\u003e\u0026#34;443:443/tcp\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e- \u003cspan class=\"s2\"\u003e\u0026#34;443:443/udp\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"test-if-http3-is-enabled\"\u003eTest if http3 is enabled\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e ╭─traefik@www.vaduzz.de in repo: garden on  main \u003cspan class=\"o\"\u003e[\u003c/span\u003e!?\u003cspan class=\"o\"\u003e]\u003c/span\u003e took 0s\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e ╰─λ curl -Iv --http3 https://www.vaduzz.de\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e* Host www.vaduzz.de:443 was resolved.\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e...\u003cspan class=\"o\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e* using HTTP/3\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e* \u003cspan class=\"o\"\u003e[\u003c/span\u003eHTTP/3\u003cspan class=\"o\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e[\u003c/span\u003e0\u003cspan class=\"o\"\u003e]\u003c/span\u003e OPENED stream \u003cspan class=\"k\"\u003efor\u003c/span\u003e https://www.vaduzz.de/\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e* \u003cspan class=\"o\"\u003e[\u003c/span\u003eHTTP/3\u003cspan class=\"o\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e[\u003c/span\u003e0\u003cspan class=\"o\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e[\u003c/span\u003e:method: HEAD\u003cspan class=\"o\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e* \u003cspan class=\"o\"\u003e[\u003c/span\u003eHTTP/3\u003cspan class=\"o\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e[\u003c/span\u003e0\u003cspan class=\"o\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e[\u003c/span\u003e:scheme: https\u003cspan class=\"o\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e* \u003cspan class=\"o\"\u003e[\u003c/span\u003eHTTP/3\u003cspan class=\"o\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e[\u003c/span\u003e0\u003cspan class=\"o\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e[\u003c/span\u003e:authority: www.vaduzz.de\u003cspan class=\"o\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e* \u003cspan class=\"o\"\u003e[\u003c/span\u003eHTTP/3\u003cspan class=\"o\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e[\u003c/span\u003e0\u003cspan class=\"o\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e[\u003c/span\u003e:path: /\u003cspan class=\"o\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e* \u003cspan class=\"o\"\u003e[\u003c/span\u003eHTTP/3\u003cspan class=\"o\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e[\u003c/span\u003e0\u003cspan class=\"o\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e[\u003c/span\u003euser-agent: curl/8.15.0\u003cspan class=\"o\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e* \u003cspan class=\"o\"\u003e[\u003c/span\u003eHTTP/3\u003cspan class=\"o\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e[\u003c/span\u003e0\u003cspan class=\"o\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e[\u003c/span\u003eaccept: */*\u003cspan class=\"o\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u0026gt; HEAD / HTTP/3\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u0026gt; Host: www.vaduzz.de\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e...\u003cspan class=\"o\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e","title":"Traefik: Enable Http3"},{"content":"Sensirion SPS30 Experiments For some days an air purifier is doing its work here, leading to significant improvements but only if running in manual mode, the automatic mode seems lacking, at least according to my nose.\nBecause this is not very scientific, I decided to connect another sensor to my pc and see if the values somewhere else in the room differ from what the purifier measures.\nA possible reason: to measure airquality, you need to transport enough air to the sensor, but what happens if the automatic mode drops fan speeds so low that it\u0026rsquo;s not enough to produce accurate reads?\nIt may never measure high enough particle values to spin up the fan again.\nflowchart LR; Measure[\"Measure PM2.5\"] --\u003e CheckThreshold{\"PM2.5 \u003e Threshold?\"} CheckThreshold -- Yes --\u003e AccelerateFan[\"Accelerate Fan\"] CheckThreshold -- No --\u003e DecelerateFan[\"Decelerate Fan\"] AccelerateFan --\u003e Measure DecelerateFan --\u003e Measure Idea Using a sensor further away from the purifier should lead to better results.\nI decided to try out the sensirion sps30 since I still got one lying around.\nChallenge The sensor I have at hand is connected to the usb evaluation kit, so I should be able to poll those values.\nSensirion provides a GitHub repository for usage with the uart interface.\nTurns out I found no easy way to do this, partly because I lack knowledge in handling C/C++1\nExperiments The only language I can handle okayish is python, so lets see how far we can get here.\n# create and activate python venv mkdir -p sensirion cd sensirion python3 -m venv .venv ./.venv/bin/activate # we need the pyserial module so lets install it right away ./.venv/bin/python3 -m pip install pyserial Let\u0026rsquo;s try some basic stuff to see if we can interface with the usb device.\nIf you get access denied, you should find out what group has access to the usb device:\nstat /dev/ttyUSB0 File: /dev/ttyUSB0 Size: 0 Blocks: 0 IO Block: 4096 character special file Device: 0,6 Inode: 1032 Links: 1 Device type: 188,0 Access: (0660/crw-rw----) Uid: ( 0/ root) Gid: ( 986/ uucp) Access: 2025-06-09 10:27:32.294929519 +0200 Modify: 2025-06-09 10:27:32.294929519 +0200 Change: 2025-06-09 10:27:32.294929519 +0200 Birth: 2025-06-09 10:27:32.191891998 +0200 I\u0026rsquo;m on arch, so it\u0026rsquo;s uucp. This differs depending on the flavor you prefer.\nsudo usermod -aG uucp \u0026lt;username\u0026gt; Remember to logout\u0026amp;login and check the output of groups if you still get access denied. Sometimes a reboot is needed.\nLet\u0026rsquo;s see on what port the evaluation kit is connected.\nlsusb Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub Bus 001 Device 002: ID 0403:6001 Future Technology Devices International, Ltd FT232 Serial (UART) IC Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub Bus 003 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub [...] ls -al /dev/ttyUSB* crw-rw---- 188,0 root 9 Jun 11:54  /dev/ttyUSB0 The only available Port is ttyUSB0 so we can go with that one.\nimport serial ser = serial.Serial(\u0026#39;/dev/ttyUSB0\u0026#39;) # open serial port print(ser.name) # check which port was really used ser.write(b\u0026#39;hello\u0026#39;) # write a string ser.close() # close port Should print something like this:\n/dev/ttyUSB0 This means we are basically able to handle connections to the port.\nRetreiving Data The easy way now is to use an existing solution2.\nI decided to just copy the sps30.py, comment out the import for paho.mqtt.publish and give it a try.\nLooks like this:\n./.venv/bin/python3 sps30.py [\u0026#39;/dev/ttyUSB0\u0026#39;] Starting Wait: 0 Wait: 7 Wait: 0 Wait: 25 Wait: 0 Wait: 47 created: Jun2025 SPS_\u0026lt;serial\u0026gt;,06/09/25 11:40:51,0.87,0.90,0.90,0.90,6.00,6.94,6.96,6.96,6.96,0.50 Wait: 0 Wait: 47 SPS_\u0026lt;serial\u0026gt;,06/09/25 11:42:13,0.77,0.80,0.80,0.80,5.32,6.15,6.17,6.17,6.17,0.39 Wait: 0 Wait: 49 SPS_\u0026lt;serial\u0026gt;,06/09/25 11:43:35,1.89,1.95,1.95,1.95,13.06,15.11,15.15,15.15,15.15,0.43 [...] What does it mean? A hint is inside the mqtt part of the code:\nheader = [\u0026#39;sensor\u0026#39;, \u0026#39;time\u0026#39;, \u0026#39;PM1\u0026#39;, \u0026#39;PM25\u0026#39;, \u0026#39;PM4\u0026#39;, \u0026#39;PM10\u0026#39;, \u0026#39;b0305\u0026#39;, \u0026#39;b031\u0026#39;, \u0026#39;b0325\u0026#39;, \u0026#39;b034\u0026#39;, \u0026#39;b0310\u0026#39;, \u0026#39;tsize\u0026#39;] A line like SPS_\u0026lt;serial\u0026gt;,06/09/25 11:40:51,0.87,0.90,0.90,0.90,6.00,6.94,6.96,6.96,6.96,0.50 gets translated to:\nsensor: SPS_\u0026lt;serial\u0026gt; time: 06/09/25 11:40:51 PM1: 0.87 # Mass Concentration PM1.0 [μg/m³] PM25: 0.90 # Mass Concentration PM2.5 [μg/m³] PM5: 0.90 # Mass Concentration PM4.0 [μg/m³] //Note: the sps30 is measuring PM4.0 not 5.0 PM10: 0.90 # Mass Concentration PM10 [μg/m³] B0305: 6.00 # Number Concentration PM0.5 [#/cm³] B031: 6.94 # Number Concentration PM1.0 [#/cm³] B0325: 6.96 # Number Concentration PM2.5 [#/cm³] B034: 6.96 # Number Concentration PM4.0 [#/cm³] B0310: 6.96 # Number Concentration PM10 [#/cm³] tsize: 0.50 # Typical Particle Size8 [μm] To understand what we see, the datasheet3 helps along while also looking into the python code.\nWhile someone was cooking, both the air purifier and the sps30 noticed the spike in particles.\nAfter the purifier spun down again the line keeps at the minimum while the distant sensor is still able to pick up on elevated values.\nLet\u0026rsquo;s keep this running for some more hours and see if this proves our assumption.\nhttps://github.com/Sensirion/embedded-uart-sps\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://github.com/binh-bk/Sensirion_SPS30\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://sensirion.com/media/documents/8600FF88/64A3B8D6/Sensirion_PM_Sensors_Datasheet_SPS30.pdf\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","permalink":"https://www.vaduzz.de/posts/sensirion-sps30-experiments/","summary":"\u003ch1 id=\"sensirion-sps30-experiments\"\u003eSensirion SPS30 Experiments\u003c/h1\u003e\n\u003cp\u003eFor some days an air purifier is doing its work here, leading to significant improvements but only if running in manual mode, the automatic mode seems lacking, at least according to my nose.\u003c/p\u003e\n\u003cp\u003eBecause this is not very scientific, I decided to connect another sensor to my pc and see if the values somewhere else in the room differ from what the purifier measures.\u003c/p\u003e\n\u003cp\u003eA possible reason: to measure airquality, you need to transport enough air to the sensor, but what happens if the automatic mode drops fan speeds so low that it\u0026rsquo;s not enough to produce accurate reads?\u003c/p\u003e","title":"Sensirion Sps30 Experiments"},{"content":"Integrate Solix in Homeassistant This describes how I added a system with solar panels and smart meter into Homeassistant.\nGoals achieve an accurate overview of the energy generated, stored and consumed use existing hardware as much as possible Possible Result Should look like this:\nPossibilities These are possibilities in my environment without buying new stuff.\nUse Tom Luther\u0026rsquo;s Integration1 Use Smart Plugs Drawbacks ha-anker-solix uses anker cloud api, there is no local endpoint that can be used needs a second anker account that gets invited to your family so that you do not get locked out of your main anker account smart plugs can only cover a subset, thus leading to incomplete measurements. Decision Use Tom Luther\u0026rsquo;s Integration\nInstallation Requirements Homeassistant HACS all devices registered in the anker app using your main anker account Second Anker Account (to be created) Prepare Account goto Anker Solix Homepage2 hover over the person icon in the upper right section and hit the Join Now Link create an anker account with an email not already used for another anker account (that is in use with an app) finish the creating with the link(s) you get via e-mail invite the new account to your family using the account you manage your anker system with accept the invitation to the family using the new account Downloading Integration and Restart HA The official Documentation is great, I\u0026rsquo;m just repeating the minimum steps to get it up and running.\nYou should read through it though to understand the implications and requirements i.e. a second anker account to use so that you do not lock yourself out of the app while creating a token to use for the Homeassistant integration.\nIn Homeassistant:\ngoto HACS download Anker Solix wait for Homeassistant to install the integration and finish restarting you should now find the integration using the settings-\u0026gt;devices \u0026amp; services section looks like this: hitting the Devices button on the integration you should see your: account abbreviated with * Smart Meter if you have one Solarbank \u0026lt;your name of the system as shown in the anker app If you are familiar with Homeassistant you can now build your dashboards using the existing sensors.\nIntegration Ideas The anker sensors are not providing the correct units for the homeassistant energydashboard, so we need to create some integral sensors and if you also want to integrate the battery power flow we need some logic that splits positive and negative flows into different sensors.\nIn my case this looks like this:\nFor the integral sensors we are using the left riemann sum as discussed on github3.\nFor more comprehensive information about riemann sums, look at (german) heise+ articel about integrating smart plugs4\nSensor Overview Integral Sensors\nbattery-input-sum battery-output-sum solar-grid-import-sum solar-grid-export-sum solar-pv1-sum solar-pv2-sum solar-pv3-sum solar-pv4-sum Template Sensors\nbattery-input battery-output Create Template Sensors Important: You have to replace the part with the name of your own system in both lines.\n# replace \u0026lt;system\u0026gt; with correct name so that this: {% if states(\u0026#39;sensor.system_\u0026lt;system\u0026gt;_sb_battery_power\u0026#39;)|float \u0026lt;= 0 %} {{ states(\u0026#39;sensor.system_\u0026lt;system\u0026gt;_sb_battery_power\u0026#39;) |float |abs }} {% else %} 0.0 {% endif %} # becomes this: {% if states(\u0026#39;sensor.system_myname_sb_battery_power\u0026#39;)|float \u0026lt;= 0 %} {{ states(\u0026#39;sensor.system_myname_sb_battery_power\u0026#39;) |float |abs }} {% else %} 0.0 {% endif %} Battery-input\nState template*\n{% if states(\u0026#39;sensor.system_\u0026lt;system\u0026gt;_sb_battery_power\u0026#39;)|float \u0026gt;= 0 %} {{ states(\u0026#39;sensor.system_\u0026lt;system\u0026gt;_sb_battery_power\u0026#39;) |float |abs}} {% else %} 0.0 {% endif %} Battery-output\nState template*\n{% if states(\u0026#39;sensor.system_\u0026lt;system\u0026gt;_sb_battery_power\u0026#39;)|float \u0026lt;= 0 %} {{ states(\u0026#39;sensor.system_\u0026lt;system\u0026gt;_sb_battery_power\u0026#39;) |float |abs }} {% else %} 0.0 {% endif %} Create Integral Sensors If you have created all the integral sensors from the list above, you can continue to energy dashboard and add all the sensors there\nConfigure Energy Dashboard Links https://github.com/thomluther/ha-anker-solix\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://www.ankersolix.com\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://github.com/thomluther/ha-anker-solix/discussions/16\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://www.heise.de/ratgeber/Smart-Plugs-und-Home-Assistant-Kaffeemaschine-Flurlicht-und-Co-automatisieren-10395258.html?seite=3\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","permalink":"https://www.vaduzz.de/posts/integrate-solix-in-homeassistant/","summary":"\u003ch1 id=\"integrate-solix-in-homeassistant\"\u003eIntegrate Solix in Homeassistant\u003c/h1\u003e\n\u003cp\u003eThis describes how I added a system with solar panels and smart meter into Homeassistant.\u003c/p\u003e\n\u003ch2 id=\"goals\"\u003eGoals\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eachieve an accurate overview of the energy generated, stored and consumed\u003c/li\u003e\n\u003cli\u003euse existing hardware as much as possible\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"possible-result\"\u003ePossible Result\u003c/h2\u003e\n\u003cp\u003eShould look like this:\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Homeassistant Energy Dashboard showing solar power generation, energy from and to the grid as well as the amount of stored energy\" loading=\"lazy\" src=\"../Screenshot_20250608_160507.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"possibilities\"\u003ePossibilities\u003c/h2\u003e\n\u003cp\u003eThese are possibilities in my environment without buying new stuff.\u003c/p\u003e","title":"Integrate Solix in Homeassistant"},{"content":"Sometimes you should read the changelog Tags were not working properly, which ended up like this:\nHugo was complaining in the terminal like this:\nhugo server --buildDrafts --disableFastRender WARN found no layout file for \u0026#34;html\u0026#34; for kind \u0026#34;term\u0026#34;: You should create a template file which matches Hugo Layouts Lookup Rules for this combination. I went down some rabbit holes and stumpled accross an information from the Hugo Docs\nIn Hugo v0.146.0, we performed a full re-implementation of how Go templates are handled in Hugo. This includes structural changes to the layouts folder and a new, more powerful template lookup system. That reminded me of the version matrix of the LoveIt theme I\u0026rsquo;m using.\nFrom: LoveIt Docs\n\u0026#9998; LoveIt theme\u0026#39;s compatibility\u0026#9656; LoveIt branch or version Supported Hugo versions master(Unstable) ≥ 0.128.0 0.3.X(Recommended) 0.128.0 - 0.143.1 0.2.X(Outdated) 0.68.0 - 0.127.0 Turns out I was using hugo version 0.147.4 so I downgraded the cronjob to the last supported version image: docker.io/hugomods/hugo:ci-0.143.1 .\n","permalink":"https://www.vaduzz.de/posts/garden-versioning/","summary":"\u003ch1 id=\"sometimes-you-should-read-the-changelog\"\u003eSometimes you should read the changelog\u003c/h1\u003e\n\u003cp\u003eTags were not working properly, which ended up like this:\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"A random ASCII smiley with the error The page you are looking for does\u0026rsquo;nt exist. Sorry.\" loading=\"lazy\" src=\"../Screenshot_20250608_145452.png\"\u003e\u003c/p\u003e\n\u003cp\u003eHugo was complaining in the terminal like this:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ehugo server --buildDrafts --disableFastRender\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eWARN  found no layout file \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;html\u0026#34;\u003c/span\u003e \u003cspan class=\"k\"\u003efor\u003c/span\u003e kind \u003cspan class=\"s2\"\u003e\u0026#34;term\u0026#34;\u003c/span\u003e: You should create a template file which matches Hugo Layouts Lookup Rules \u003cspan class=\"k\"\u003efor\u003c/span\u003e this combination.\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eI went down some rabbit holes and stumpled accross an information from the \u003ca href=\"https://gohugo.io/templates/new-templatesystem-overview/\"\u003eHugo Docs\u003c/a\u003e\u003c/p\u003e","title":"Garden Versioning"},{"content":"Upgrading Kubernetes on Talos Introduction I pulled the short straw and the builtin talosctl upgrade-k8s did not work for me and after spending some time investigating and failing to find the solution I just decided to go the long way and update things manually.\nFor Reference: Official Docs v1.10\nUpgrade steps Set environment to the correct cluster set -gx KUBECONFIG ~/.config/kubeconfig_hcloud set -gx TALOSCONFIG ~/.config/talosconfig Ensure everything is running fine. In case someone wonders about the command, I\u0026rsquo;m using oc as kubectl replacement because my brain is hardwired through years of working with this thing.\noc get nodes oc get pods -n kube-system oc get pods -A |grep -v \u0026#34;Running|Completed\u0026#34; Upgrade the API Server talosctl -n 10.0.1.101 patch mc --mode=no-reboot -p \u0026#39;[{\u0026#34;op\u0026#34;: \u0026#34;replace\u0026#34;, \u0026#34;path\u0026#34;: \u0026#34;/cluster/apiServer/image\u0026#34;, \u0026#34;value\u0026#34;: \u0026#34;registry.k8s.io/kube-apiserver:v1.33.0\u0026#34;}]\u0026#39; watch oc get pods -n kube-system Wait until the api server pod is running and the talos-clout-controller pod is running as well.\nRepeat for every control plane\nUpgrade Kube Controller Manager talosctl -n 10.0.1.101 patch mc --mode=no-reboot -p \u0026#39;[{\u0026#34;op\u0026#34;: \u0026#34;replace\u0026#34;, \u0026#34;path\u0026#34;: \u0026#34;/cluster/controllerManager/image\u0026#34;, \u0026#34;value\u0026#34;: \u0026#34;registry.k8s.io/kube-controller-manager:v1.33.0\u0026#34;}]\u0026#39; watch oc get pods -n kube-system Repeat for every control plane\nUpgrade Scheduler talosctl -n 10.0.1.101 patch mc --mode=no-reboot -p \u0026#39;[{\u0026#34;op\u0026#34;: \u0026#34;replace\u0026#34;, \u0026#34;path\u0026#34;: \u0026#34;/cluster/scheduler/image\u0026#34;, \u0026#34;value\u0026#34;: \u0026#34;registry.k8s.io/kube-scheduler:v1.33.0\u0026#34;}]\u0026#39; watch oc get pods -n kube-system Repeat\u0026hellip;\nkube-proxy update omitted (because kube-proxy disabled) bootstrap resources omitted (because managed by fluxcd in my case)\nUpgrade Kubelet talosctl -n 10.0.1.101 patch mc --mode=no-reboot -p \u0026#39;[{\u0026#34;op\u0026#34;: \u0026#34;replace\u0026#34;, \u0026#34;path\u0026#34;: \u0026#34;/machine/kubelet/image\u0026#34;, \u0026#34;value\u0026#34;: \u0026#34;ghcr.io/siderolabs/kubelet:v1.33.0\u0026#34;}]\u0026#39; watch oc get nodes -o wide check pods, continue if everything is running again\nwatch oc get pods -A Repeat for every node, drain worker nodes before restarting kubelet if you want to be careful.\n","permalink":"https://www.vaduzz.de/posts/update-k8s-talos/","summary":"\u003ch1 id=\"upgrading-kubernetes-on-talos\"\u003eUpgrading Kubernetes on Talos\u003c/h1\u003e\n\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003eI pulled the short straw and the builtin \u003ccode\u003etalosctl upgrade-k8s\u003c/code\u003e did not work for me and after spending some time investigating and failing to find the solution I just decided to go the long way and update things manually.\u003c/p\u003e\n\u003cp\u003eFor Reference: \u003ca href=\"https://www.talos.dev/v1.10/kubernetes-guides/upgrading-kubernetes/#automated-kubernetes-upgrade\"\u003eOfficial Docs v1.10\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"upgrade-steps\"\u003eUpgrade steps\u003c/h2\u003e\n\u003ch3 id=\"set-environment-to-the-correct-cluster\"\u003eSet environment to the correct cluster\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-fish\" data-lang=\"fish\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eset\u003c/span\u003e \u003cspan class=\"na\"\u003e-gx\u003c/span\u003e \u003cspan class=\"nv\"\u003eKUBECONFIG\u003c/span\u003e ~/.config/kubeconfig_hcloud\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eset\u003c/span\u003e \u003cspan class=\"na\"\u003e-gx\u003c/span\u003e \u003cspan class=\"nv\"\u003eTALOSCONFIG\u003c/span\u003e ~/.config/talosconfig\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"ensure-everything-is-running-fine\"\u003eEnsure everything is running fine.\u003c/h3\u003e\n\u003cp\u003eIn case someone wonders about the command, I\u0026rsquo;m using \u003ccode\u003eoc\u003c/code\u003e as kubectl replacement because my brain is hardwired through years of working with this thing.\u003c/p\u003e","title":"Updating Kubernetes on Talos"},{"content":"Building Shades Goals pages written in Markdown, deployed as a static site pages pushed to git, rendered automatically without user interaction low-cost, reasonable performance solution Infrastructure kubernetes cluster object storage domain git linux workstation Steps From Zero to First Post install hugo binary create a git repository and, if not public, create an application token for read access create a bucket where your static files for the webpage are stored install kubernetes manifests for cronjob renders static site using hugo pushes to s3 nginx handles serving the static website from s3 to http ingress handles tls termination at the edge configure hugo create a post test locally served page Technical Details Hugo Stuff install hugo binary: https://gohugo.io/installation/ i.e. pacman -S hugo hugo version #does this work? create git repository create application token permissions: repository read git clone \u0026lt;repo\u0026gt; create a bucket Hetzner Cloud public access no versioning hugo new site garden --force edit hugo.toml, look for a nice theme here: https://themes.gohugo.io/ set theme in hugo.toml like theme = 'example' create a post with hugo new content content/posts/something-new.md serve locally hugo server --buildDrafts remember to change draft=true to draft=false in the header of the markdown file commit to repository Kubernetes Stuff Example for Hetzner Cloud NBG1 bucket\nIn addition to the cronjob, you also need a serviceaccount hugo-render and two secrets:\ngit-credentials with the keys username and token objectstorage with the keys access-key and secret-key Remember to adjust GIT_REPO and S3_BUCKET_NAME\nIf your bucket is not in NBG1 in Hetzner Cloud: adjust S3_REGION and S3_ENDPOINT\nCronjob --- apiVersion: batch/v1 kind: CronJob metadata: name: hugo-site-deployer spec: schedule: \u0026#34;*/15 * * * *\u0026#34; # Runs every 15 minutes jobTemplate: spec: template: spec: serviceAccountName: hugo-render volumes: - name: site-output emptyDir: {} initContainers: - name: hugo image: docker.io/hugomods/hugo:ci-0.143.1 #pinned to last supported version for the LoveIt theme command: - /bin/sh - -c - | # Construct the HTTPS URL with the Git credentials git clone https://$GIT_USERNAME:$GIT_TOKEN@$GIT_REPO /src cd /src # Build the Hugo site hugo --minify -d /output volumeMounts: - name: site-output mountPath: /output env: - name: GIT_REPO value: \u0026#34;myrepo.domain/organisation/garden.git\u0026#34; - name: GIT_USERNAME valueFrom: secretKeyRef: name: git-credentials key: username - name: GIT_TOKEN valueFrom: secretKeyRef: name: git-credentials key: token containers: - name: s3-uploader image: minio/mc # MinIO client image command: - /bin/sh - -c - | # Configure MinIO client with S3 endpoint credentials mc alias set s3 $S3_ENDPOINT $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY # Copy the Hugo site to the S3 bucket mc mirror /output s3/$S3_BUCKET_NAME --overwrite --remove # Mount the shared volume to read the Hugo build output volumeMounts: - name: site-output mountPath: /output env: - name: AWS_ACCESS_KEY_ID valueFrom: secretKeyRef: name: objectstorage key: access-key - name: AWS_SECRET_ACCESS_KEY valueFrom: secretKeyRef: name: objectstorage key: secret-key - name: S3_BUCKET_NAME value: \u0026#34;the-infamous-example-bucket\u0026#34; - name: S3_ENDPOINT value: \u0026#34;https://nbg1.your-objectstorage.com\u0026#34; - name: S3_REGION value: \u0026#34;nbg1\u0026#34; restartPolicy: OnFailure Serving Static Sites from Objectstorage Nginx Deployment --- apiVersion: apps/v1 kind: Deployment metadata: name: nginx-static-proxy spec: replicas: 2 selector: matchLabels: app: nginx-static-proxy template: metadata: labels: app: nginx-static-proxy spec: containers: - name: nginx image: nginx:latest ports: - containerPort: 8080 volumeMounts: - name: nginx-config mountPath: /etc/nginx/nginx.conf subPath: nginx.conf - name: nginx-cache mountPath: /var/cache/nginx - name: nginx-run mountPath: /run securityContext: runAsUser: 1000 runAsGroup: 1000 allowPrivilegeEscalation: false volumes: - name: nginx-config configMap: name: nginx-proxy-config - name: nginx-cache emptyDir: {} - name: nginx-run emptyDir: {} Nginx Configmap Remember to insert the correct bucket endpoint\n--- apiVersion: v1 kind: ConfigMap metadata: name: nginx-proxy-config data: nginx.conf: | events { worker_connections 1024; } http { server { listen 8080; location / { proxy_pass https://\u0026lt;bucketname\u0026gt;.\u0026lt;endpoint\u0026gt; rewrite ^(.*/)$ $1index.html break; } } } Nginx Service --- apiVersion: v1 kind: Service metadata: name: nginx-service labels: app: nginx-static-proxy spec: selector: app: nginx-static-proxy ports: - protocol: TCP port: 8080 targetPort: 8080 type: ClusterIP Expose via IngressRoute --- apiVersion: traefik.io/v1alpha1 kind: IngressRoute metadata: name: garden spec: entryPoints: - websecure routes: - match: \u0026#34;Host(`www.vaduzz.de`) \u0026amp;\u0026amp; PathPrefix(`/`)\u0026#34; kind: Rule services: - name: nginx-service port: 8080 scheme: http tls: certResolver: letsencrypt Optimization Looking closer at my nginx config I noticed that I forgot to add a cache, so here is another config. Keep in mind that on Talos emptydir uses the hosts filesystem for backing so i kept the usage to 1G max.\n--- apiVersion: v1 kind: ConfigMap metadata: name: nginx-proxy-config data: nginx.conf: | events { worker_connections 1024; } http { proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=bucket_cache:10m max_size=1g inactive=48h use_temp_path=off; server { listen 8080; location / { proxy_cache bucket_cache; proxy_cache_key \u0026#34;$scheme://$host$request_uri\u0026#34;; proxy_cache_valid 200 304 48h; proxy_cache_lock on; proxy_cache_revalidate on; expires 1y; proxy_pass https://\u0026lt;bucketname\u0026gt;.\u0026lt;endpoint\u0026gt; rewrite ^(.*/)$ $1index.html break; } } } Does it work?\nUsing siege to create 50 concurrent workers trying to hit a page\u0026hellip;\nA look into the Metrics for Requests at the Ingress Controller shows a slightly better Apdex score so caching seems to have improved the response times. ","permalink":"https://www.vaduzz.de/posts/building-shades/","summary":"\u003ch1 id=\"building-shades\"\u003eBuilding Shades\u003c/h1\u003e\n\u003ch2 id=\"goals\"\u003eGoals\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003epages written in Markdown, deployed as a static site\u003c/li\u003e\n\u003cli\u003epages pushed to git, rendered automatically without user interaction\u003c/li\u003e\n\u003cli\u003elow-cost, reasonable performance solution\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"infrastructure\"\u003eInfrastructure\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003ekubernetes cluster\u003c/li\u003e\n\u003cli\u003eobject storage\u003c/li\u003e\n\u003cli\u003edomain\u003c/li\u003e\n\u003cli\u003egit\u003c/li\u003e\n\u003cli\u003elinux workstation\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"steps\"\u003eSteps\u003c/h2\u003e\n\u003ch3 id=\"from-zero-to-first-post\"\u003eFrom Zero to First Post\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003einstall hugo binary\u003c/li\u003e\n\u003cli\u003ecreate a git repository and, if not public, create an application token for read access\u003c/li\u003e\n\u003cli\u003ecreate a bucket where your static files for the webpage are stored\u003c/li\u003e\n\u003cli\u003einstall kubernetes manifests for\n\u003cul\u003e\n\u003cli\u003ecronjob\n\u003cul\u003e\n\u003cli\u003erenders static site using hugo\u003c/li\u003e\n\u003cli\u003epushes to s3\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003enginx\n\u003cul\u003e\n\u003cli\u003ehandles serving the static website from s3 to http\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003eingress\n\u003cul\u003e\n\u003cli\u003ehandles tls termination at the edge\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003econfigure hugo\u003c/li\u003e\n\u003cli\u003ecreate a post\u003c/li\u003e\n\u003cli\u003etest locally served page\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"technical-details\"\u003eTechnical Details\u003c/h3\u003e\n\u003ch4 id=\"hugo-stuff\"\u003eHugo Stuff\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003einstall hugo binary: \u003ca href=\"https://gohugo.io/installation/\"\u003ehttps://gohugo.io/installation/\u003c/a\u003e\n\u003cul\u003e\n\u003cli\u003ei.e. \u003ccode\u003epacman -S hugo\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ehugo version #does this work?\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003ecreate git repository\n\u003cul\u003e\n\u003cli\u003ecreate application token\n\u003cul\u003e\n\u003cli\u003epermissions: repository read\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003egit clone \u0026lt;repo\u0026gt;\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003ecreate a bucket\n\u003cul\u003e\n\u003cli\u003eHetzner Cloud\n\u003cul\u003e\n\u003cli\u003epublic access\u003c/li\u003e\n\u003cli\u003eno versioning\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ehugo new site garden --force\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eedit \u003ccode\u003ehugo.toml\u003c/code\u003e, look for a nice theme here: \u003ca href=\"https://themes.gohugo.io/\"\u003ehttps://themes.gohugo.io/\u003c/a\u003e\n\u003cul\u003e\n\u003cli\u003eset theme in \u003ccode\u003ehugo.toml\u003c/code\u003e like \u003ccode\u003etheme = 'example'\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003ecreate a post with \u003ccode\u003ehugo new content content/posts/something-new.md\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eserve locally \u003ccode\u003ehugo server --buildDrafts\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eremember to change \u003ccode\u003edraft=true\u003c/code\u003e to \u003ccode\u003edraft=false\u003c/code\u003e in the header of the markdown file\u003c/li\u003e\n\u003cli\u003ecommit to repository\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"kubernetes-stuff\"\u003eKubernetes Stuff\u003c/h4\u003e\n\u003cp\u003eExample for Hetzner Cloud NBG1 bucket\u003c/p\u003e","title":"Building Shades"},{"content":"Somewhere in the corner of my mind something kept nagging me to build a space for those not-so-shiny ideas, thoughts and experiments I come across while tinkering. Lately the garbage collection of my human brain tends to be more thorough with collecting information so i think it might be interesting to see where those bits and pieces are leading to.\nAnd finally, this is the place\u0026hellip;\nShades in the Garden Linux Virtualization Containers and Pods Networks Automation Monitoring Below the surface Hugo Object Storage Kubernetes Cronjob Forgejo ","permalink":"https://www.vaduzz.de/posts/new-shade/","summary":"\u003ch1 id=\"somewhere-in-the-corner-of-my-mind\"\u003eSomewhere in the corner of my mind\u003c/h1\u003e\n\u003cp\u003esomething kept nagging me to build a space for those not-so-shiny ideas, thoughts and experiments\nI come across while tinkering. Lately the garbage collection of my human brain tends to be more thorough with collecting information so i think it might be interesting to see where those bits and pieces are leading to.\u003c/p\u003e\n\u003cp\u003eAnd finally, this is the place\u0026hellip;\u003c/p\u003e\n\u003ch2 id=\"shades-in-the-garden\"\u003eShades in the Garden\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eLinux\u003c/li\u003e\n\u003cli\u003eVirtualization\u003c/li\u003e\n\u003cli\u003eContainers and Pods\u003c/li\u003e\n\u003cli\u003eNetworks\u003c/li\u003e\n\u003cli\u003eAutomation\u003c/li\u003e\n\u003cli\u003eMonitoring\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"below-the-surface\"\u003eBelow the surface\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eHugo\u003c/li\u003e\n\u003cli\u003eObject Storage\u003c/li\u003e\n\u003cli\u003eKubernetes Cronjob\u003c/li\u003e\n\u003cli\u003eForgejo\u003c/li\u003e\n\u003c/ul\u003e","title":"New Shade"}]