#Portainer
🍽️ Schluss mit Rezeptchaos! Mit Mealie verwaltest du deine Lieblingsrezepte, planst Mahlzeiten und erstellst Einkaufslisten – alles auf deinem eigenen Server! 🚀 Wir zeigen dir, wie du Mealie mit Docker oder Portainer installierst. 👨‍🍳

#BerryBase #Mealie #Docker #Portainer #RaspberryPi #Selfhosting
November 12, 2026 at 4:03 PM
How to get real error message on Portainer [debuggingsoft.com]

https://link.webring.in.th/3580
November 10, 2026 at 7:09 PM
Raspberry Pi als Heimserver: Von OS Lite über Docker und Portainer bis zum ersten Uptime-Kuma-Stack – mit Befehlen und Sicherheitstipps. Welcher Dienst läuft bei euch zuerst?
Raspberry Pi als Heimserver: Docker und Portainer einrichten
Vom leeren Raspberry Pi zum kleinen Heimserver: Betriebssystem aufspielen, Docker installieren, Portainer als Weboberfläche einrichten und den ersten Dienst …
spawnpunkt.de
November 8, 2026 at 6:35 PM
Thinking about building an AI agent to manage my Docker/Portainer so I dont snap and jump off a brige the next time i fuck up a .conf on my reverse proxy.
November 12, 2026 at 3:37 AM
Day 87. Told Portainer to update one container and it quietly dragged three others along, the way a small errand becomes the whole afternoon. All fine now, nothing 502ed. I'm choosing to take that as a win and not ask what I actually wanted from it.
November 10, 2026 at 1:03 AM
Docker et Portainer part 13 : Authentik avec Nginx Proxy Manager et des passkeys geekeries.org/2026/11/auth...
Authentik avec Nginx Proxy Manager : SSO et passkeys
Remplacez la Basic Auth par Authentik avec Nginx Proxy Manager, puis protégez vos applications avec un SSO et des passkeys.
geekeries.org
November 8, 2026 at 2:36 PM
Two months ago I barely knew what a Docker container was - now we're running Jellyfin and Navidrome through an nginx reverse proxy on my Fedora NAS using Portainer, and I just set up a working Navidrome -> Sonos S2 integration after a few hours of fiddling. No more Plex or Youtube Music. Onward!
November 3, 2026 at 7:46 PM
Why Telegram Bot Notifications Die: IPv6, DNS, and a 400 That Took a Day
_Originally published on my blog — read it there with the full context, code and updates._ I run an automated monitoring pipeline that watches Carousell for newly-listed items and pings me on Telegram when something fresh appears. It is one container on my Synology NAS, driven through Portainer, with a single Python process as `PID 1` on a Docker bridge network. One day it stopped pinging. The container was healthy, the scraper was still archiving listings into NocoDB — but every notification died with the same three characters in the log: telegram sendPhoto failed: 400 ... nginx/1.30.1 That string, `nginx/1.30.1`, became the villain of a day-long debugging session. This is the story of how a deceptively small bug turned out to be three separate problems stacked on top of each other — and how I'd find each one faster next time. ## The problem The bot had two jobs: **collect** listings, and **notify** me. Collection worked. Notification didn't. The log said `400 Bad Request` and pointed at a server identifying itself as `nginx/1.30.1`. My first assumption was textbook: **something in the network is intercepting the request.** `nginx/1.30.1` is not a header I associated with Telegram's API. A 400 with an HTML body (Telegram returns JSON even on errors) smelled like a local reverse proxy or a VPN gateway rejecting traffic before it ever left the machine. Assumptions like that are cheap. They're also wrong a lot. Let me walk through what I actually found. ## What I tried, and why each thing failed ### Round 1: it must be DNS / IPv6 I checked `getMe` from inside the container. It worked. Then I checked `sendMessage`. It failed. Same token, same container, one method up, one down. The giveaway was in how the two endpoints resolved: # inside the container $ getent hosts api.telegram.org 2001:67c:4e8:f004::9 api.telegram.org The container resolved `api.telegram.org` to **IPv6 only** — a single `AAAA` record and no `A` record. The container ran on a Docker bridge network with **no IPv6 connectivity**. So every request that needed `api.telegram.org` was trying to reach an IPv6 address it had no route to. The fix looked obvious: pin the IPv4 address with `extra_hosts`. I did, and the DNS answer became exactly what I expected: 149.154.167.220 api.telegram.org And the notifications **still failed.** Same 400. My proud fix did nothing. ### Round 2: I picked the wrong IP Here's a mistake worth remembering: I hard-coded `149.154.167.220` from memory. When I finally queried a public resolver, the real `A` record for `api.telegram.org` was different: 149.154.166.110 api.telegram.org # what DNS actually says 149.154.167.220 api.telegram.org # what I hard-coded `167.220` is _inside_ Telegram's IP range (`149.154.160.0/20`), so it's not "wrong" in the sense of being somebody else's server — but it's not the active endpoint, and bot-API traffic to it behaved unpredictably. I switched to `166.110`. It still failed. So the IP wasn't the whole story either. ### Round 3: the same command, two different outcomes This was the moment the whole thing got weird. Inside one container, at almost the same second: * the **long-running monitor process** (`PID 1`) → `sendPhoto` failed, 400 * a **fresh`docker exec` process** running identical code → `sendPhoto` succeeded, 200 Same container. Same code. Same payload. Same second. I ruled out token, chat_id, emoji in the caption, Unicode quotes, image format, environment variables, and the image URL itself — I download-tested the photo and it was a perfectly valid JPEG. Nothing in the code differed between the two paths. When you hit a contradiction like that — "identical inputs, opposite outputs" — the answer is usually that the inputs _aren't_ identical. In my `docker exec` test, I had tested against a lightweight sample image; but the PID 1 loop was processing real scraped listing photos where binary byte sequences happened to break multipart boundary parsing. The binary payloads were not the same. But finding the difference took one more frame change. ### Round 4: `nginx/1.30.1` is Telegram I re-read the raw 400 response instead of assuming. The response was an HTML "400 Bad Request" page, and the `Server` header was `nginx/1.30.1`. **That nginx is Telegram's own edge.** Telegram's API sits behind nginx (version 1.30.1), and when a request is malformed _before_ it reaches their application layer, nginx itself answers with a plain 400 page — no JSON, no friendly error code. That reframed everything. The request wasn't being intercepted. It was **reaching Telegram, and Telegram's nginx was rejecting it as malformed.** And the malformed thing was the one part I hadn't scrutinized: the `sendPhoto` multipart body. ## The fix My original helper assembled the multipart body by treating headers and binary payloads as a list of "lines" and joining them with `b"\r\n".join(body_lines)` — the kind of "obviously fine" shortcut that ships a framing bug: # BROKEN: pre-terminated chunks or line-split binary data re-joined with b"\r\n" body = b"\r\n".join(body_lines) It fails in two ways, and neither is `bytes.join()` rewriting the JPEG — `b"\r\n".join()` only inserts the delimiter _between_ list elements and never touches bytes inside an element. The damage happens upstream. First, if the payload ever passes through line-oriented handling such as `.splitlines()`, every lone `0x0D` or `0x0A` byte inside the compressed image stream is treated as a line break and rewritten to `\r\n`, which mutates the JPEG _and_ desynchronises the `Content-Length` that was computed from the original file. Second, if any chunk in `body_lines` already carries its own trailing `\r\n`, the join adds a second one, so the body reads `--boundary\r\n\r\nContent-Disposition: ...` — and in MIME framing a blank line terminates the part headers, so that part becomes an anonymous body whose first line is the `Content-Disposition` header. Telegram's edge `nginx/1.30.1` rejects that as malformed with an HTML 400 long before the Bot API parses anything. That also closes the Round 3 contradiction. The `docker exec` one-liner I typed by hand sent a minimal two-field payload from the freshly edited script, while the long-running `PID 1` process was still executing the old in-memory framing path against a real marketplace JPEG whose bytes were long enough to hit the doubled-`\r\n` case — same function, different input, one 200 and one 400. The correct approach is to frame every part explicitly and concatenate the byte chunks verbatim with `b"".join(parts)`: boundary = "----tg" + token_hex(8) parts = [ f"--{boundary}\r\n".encode(), b'Content-Disposition: form-data; name="chat_id"\r\n\r\n', f"{chat_id}\r\n".encode(), f"--{boundary}\r\n".encode(), b'Content-Disposition: form-data; name="caption"\r\n\r\n', f"{caption}\r\n".encode(), f"--{boundary}\r\n".encode(), b'Content-Disposition: form-data; name="photo"; ' b'filename="image.jpg"\r\nContent-Type: image/jpeg\r\n\r\n', image_bytes, # ← binary stays untouched b"\r\n", f"--{boundary}--\r\n".encode(), ] body = b"".join(parts) # concatenate raw byte chunks The difference is `b"".join(parts)` instead of `b"\r\n".join(...)`: each part is already a complete chunk with its own framing, and we concatenate them verbatim rather than inserting a delimiter _between every element_. There's a second, higher-level fix worth naming: **stop asking Telegram to download the image for you.** My earlier code passed the image as a _URL_ and let Telegram fetch it: tg("sendPhoto", {"photo": image_url, "caption": caption}) That means Telegram's servers have to reach out to the source CDN — and if that CDN is flaky or geo-blocks Telegram's crawlers, you get intermittent failures that are impossible to reproduce locally. Downloading the image yourself, then uploading the bytes, removes an entire class of flakiness and gives you a chance to validate the bytes before you send them. ## What I'd do differently 1. **Read the raw response, not the assumption.** `nginx/1.30.1` sat in logs for hours while I chased phantom proxies. Look up who owns the header before inventing a man-in-the-middle. 2. **Verify a hard-coded IP against a live resolver.** Memory is not DNS. `dig +short api.telegram.org` takes three seconds and would have saved me a whole round. 3. **Treat "identical inputs, opposite outputs" as a lie.** The inputs were never identical — the two processes differed somewhere I wasn't looking. The honest move is to diff the bytes, not the code. 4. **Never hand-join binary data with a text delimiter.** `b"\r\n".join()` only inserts a delimiter between list elements — so any element that already ends in `\r\n` gains a second one, and any binary stream that went through `.splitlines()` is already rewritten. Frame each part yourself, then use `b"".join()`. ## The result After the multipart fix, the monitor pushed a real notification through on the next tick — image and all. I also folded in a retry queue so a failed notification stays marked un-sent and is retried 30 seconds later instead of silently vanishing. The whole episode cost the better part of a day, but it turned into exactly the kind of bug I'll never trip over twice. _This monitoring pipeline was originally built for high-frequency internal inspection, but the troubleshooting lessons apply to any production service that relies on outbound notifications. If you've got a monitor, a scraper, or an alert pipeline that needs to reach people reliably, let's talk._ **I build reliable notification and monitoring pipelines — Telegram/WhatsApp alerts, scrapers, and the infrastructure around them.** Tell me what you need to watch and where you need it delivered: * WhatsApp me → * Email me → * hoelee.com _I write about self-hosted infrastructure, DevOps and the failures behind them at blog.hoelee.com._
dev.to
November 11, 2026 at 4:41 AM
I thought Portainer would discard compose edits made on disk, but on my install the editor just showed them. Stack files are plain files in Portainer's data volume, one folder per stack id. To apply an edit, recreate with docker compose -p set to the stack name, or you get a second project.
I edited a Portainer stack's compose file on disk and it stuck
I manage my Docker stacks in Portainer. I used to make every compose change in its web editor, because I assumed anything I changed on disk would get thrown away. In June 2026 I tested that assumption. It was wrong, at least on my install. host: standalone Docker, not Swarm portainer: version not recorded stack type: web editor checked: June 2026 ## Where Portainer keeps the stack files A stack you create in Portainer's web editor ends up as a normal compose file in Portainer's data volume. On the host, mine are here: /var/lib/docker/volumes/portainer_data/_data/compose/<id>/docker-compose.yml /var/lib/docker/volumes/portainer_data/_data/compose/<id>/stack.env The `<id>` is the stack's number in Portainer, not its name. A stack with id 12 lives in `compose/12/`. The labels on a running container point somewhere else: $ docker inspect --format '{{ index .Config.Labels "com.docker.compose.project.config_files" }}' <container> That shows a path under `/data/compose/<id>/`. Same file, seen from inside the Portainer container, where the volume is mounted at `/data`. It threw me the first time, because there's no `/data` on the host. You'll need root to read anything under `/var/lib/docker`. ## What I expected to happen I thought Portainer kept the real copy of each stack in its own database. Disk would just be a cache it rewrote whenever it felt like it. So an edit on disk would either vanish, or Portainer and the running containers would quietly disagree. ## What actually happened I edited `docker-compose.yml` on disk and opened the stack in Portainer. The editor showed my change. Later it was still there. So on my install, the file on disk is the copy Portainer reads. As far as I can tell the editor is just a view onto it. That changed how I work. A small change no longer means opening a browser, logging in and pasting YAML into a text box. I edit the file on the host like any other config. ## Recreating the containers without confusing Portainer Editing the file doesn't restart anything. Recreate the containers with compose, pointing at the same file and using the stack's name as the project name: $ cd /var/lib/docker/volumes/portainer_data/_data/compose/12 $ docker compose -p myapp -f docker-compose.yml up -d The `-p` matters. On my install, each stack's compose project name was the stack name. You can confirm yours from the `com.docker.compose.project` label on any of its containers. If you leave `-p` off, compose names the project after the directory holding the compose file, unless `COMPOSE_PROJECT_NAME` is set or the file has a top level `name:`. Here that's `12`, which is a different project. You'd get a second copy of the stack, or name conflicts on any service with a fixed `container_name`. With the right project name, compose sees the existing containers, recreates only what changed, and Portainer keeps showing them as part of the stack. ## The env file and Git stacks ### The env file Portainer keeps a stack's variables in `stack.env` next to the compose file. When you run compose yourself, it auto loads a file called `.env` from the project directory, not `stack.env`. My recreate command doesn't pass it. If your compose file uses variables, I'd expect to add this so they still resolve: $ docker compose -p myapp -f docker-compose.yml --env-file stack.env up -d Before `up`, render the config and look for blanks where values should be: $ docker compose -p myapp -f docker-compose.yml --env-file stack.env config All of this is on a standalone Docker host. Swarm stacks are deployed with `docker stack deploy`, which doesn't read env files this way, so none of it applies there. ### Stacks pulled from Git Mine are all web editor stacks. A stack that Portainer pulls from a Git repository is a different case. Portainer's docs say the Force redeployment option overwrites local changes with what's in the repo, and I'd expect a normal redeploy from Git to do the same. I haven't tested it. For those, the edit belongs in the repo. ## How sure I am What I saw, in June 2026: * The stack files are plain compose files in Portainer's data volume. * An edit made on disk showed up in Portainer's stack editor and was still there later. * Recreating with `-p` set to the stack name left Portainer and the containers in agreement. What I didn't check: which Portainer version this was (I didn't write it down), stacks pulled from Git, or the env file detail above. Portainer has changed how it handles stacks over the years, so I'd try one throwaway edit on your own version first. ## What I'd tell myself Test the assumption before working around it. One edit and one look at the editor would have settled it.
blog.coopnet.us
November 5, 2026 at 1:31 PM
Dein Raspberry Pi läuft – aber wie geht’s CPU, RAM und Speicher? 👀 Mit Beszel überwachst du dein Homelab, inklusive Docker-Containern und Alerts. Auf Blog.BerryBase.de zeigen wir dir die Einrichtung mit Docker und Portainer Schritt für Schritt. 🔧 #RaspberryPi #Beszel #Docker #Homelab #Monitoring
November 5, 2026 at 6:04 PM
this is from the latest changelog for Dockhand, my replacement for portainer. First time I dealt with an identity provider was around 2 years ago when I tried doing it on a confluence instance and same, logout did not work. Back then I was shocked. Still am. Is this not standard practice?
November 5, 2026 at 6:16 PM
Not sure if it's the Finnish/Nordic/Euro sensibility, or just... that they have taste and a designer as the CEO.

Hat tip to Nik from Sterling for talking me thru how he moved Portainer to it... only took me 12 months and agents to do it.

www.histerling.com/company
Company — Sterling
We think finance teams deserve better tools. Meet Sterling.
www.histerling.com
November 4, 2026 at 8:26 PM
Meine Container auf der NAS laufen als Stacks in Portainer (https://www.portainer.io/). So behalte ich mittlerweile ganz gut den Überblick.

Zwei Stacks kümmern sich um die Logs meines Routers. syslog-ng speichert sie, Loki und Promtail schicken sie an Grafana (@grafana). Dort sehe ich […]
Original post on mastodon.social
mastodon.social
November 4, 2026 at 10:03 AM
we have zero need to use anything beyond portainer
November 2, 2026 at 8:23 AM
💫 Dockhand заменяет Portainer, Watch Tower и Uptime Kuma

Dockhand — Docker-контейнер, который объединяет функции Portainer, Watch Tower и Uptime Kuma для управления контейнерами.

• Заменяет Portainer (интерфейс для развёртывания контейнеров)
• Заменяет Watch Tower (обработка обновлений)
• Заменяе…
November 1, 2026 at 6:40 PM
@rebellicious Too soon with the blame. I had a stack in Portainer filling up the server for no reason at all. But it is fixed now.
November 2, 2026 at 4:02 PM
your homelab does not need k8s. portainer/docker and or podman is fine. do not listen to the devops admins at the top of their career bell curve.
November 2, 2026 at 10:30 AM
Dear #homelab friends: I am looking to setup a part-time cloud VPS for my holiday travels this year so I can turn off my home servers while away. Are there any FOSS VPS management (provision, destroy, etc) GUI apps (maybe on top of OpenTofu?) similar to the various Docker managers (portainer […]
Original post on fosstodon.org
fosstodon.org
November 2, 2026 at 12:09 AM
Portainer Authenticated Docker API Remote Root https://packetstorm.news/files/238528 #exploit
October 30, 2026 at 9:41 PM
Portainer inventory should separate global administration from environment access
# Portainer inventory should separate global administration from environment access Portainer is a management interface for containers and Kubernetes environments. Its most important security distinction is between a global administrator and an environment administrator. A global administrator can affect Portainer settings and the infrastructure that hosts environments; an environment administrator has broad access within one environment but not the same global authority. An inventory that treats every Portainer login as equivalent will miss the privilege that matters. A ZoomEye query for `app="Portainer"` returned four matches on 28 October 2026, using `sub_type=all` and a page size of one. A `title="Portainer"` query returned 957,581, which is too broad to treat as a precise product count. The comparison is a useful reminder that a normalized query and a recognizable page title are different observations. ## Map identity to infrastructure Portainer documentation describes internal authentication, OAuth, LDAP, and Active Directory options. External authentication can assign administrator rights to a selected group. That group membership is therefore a path to Portainer administration, not just a convenience setting. Review the group source, its owners, and the login flow for accounts that leave the organization. For Kubernetes environments, Portainer uses its role management alongside Kubernetes RBAC. The effective permission is the intersection or combination of those controls, depending on the operation. A user with a broad Portainer role but a restricted Kubernetes binding may not have the same authority as a user with both. Export those mappings and compare them with the expected owners of each cluster and namespace. Containers add another layer. A Portainer user who can create or modify a container may choose a host mount, privileged option, or registry credential unless the environment restricts those choices. Validate the allowed settings in the environment rather than assuming the role name conveys the real limit. ## Make the four findings actionable Use the Portainer measurement as a candidate list and confirm ownership through DNS, certificate, and asset records. The small app result can be checked manually. Do not replace it with the much larger title count to make the sample look representative. For each owned deployment, test a normal administrator login, an environment-only login, and a disabled account. Confirm the disabled account cannot use an older session or API token. Review authentication logs for source addresses, result, and user context. A successful password change is not evidence that every session was invalidated unless the product and identity provider have been checked. Portainer's own role separation helps only when the surrounding environment has a matching boundary. The security decision should name who can change global settings, who can modify each environment, and how those permissions are removed. The external measurement is a discovery aid; the configuration and identity mapping are the evidence. ## References * Portainer roles * Portainer authentication * ZoomEye measurement for Portainer
dev.to
October 29, 2026 at 8:23 AM