1 Causes of the Third Optimization of the Blog's Active-Active Architecture
A few days ago, I was preparing to perform a very routine maintenance operation on the blog's active-active architecture: Mac mini nodeto upgrade the version of Portainer. As it turned out, a seemingly minor issue ended up completely exposing the suitability of the entire node—running this on the Mac mini:
docker pull portainer/portainer-ce:latest
unexpectedly threw an error:
error getting credentials - err: exit status 1, out: keychain cannot be accessed because the current session does not allow user interaction. The keychain may be locked; unlock it by running "security -v unlock-keychain ~/Library/Keychains/login.keychain-db" and try again
At first glance, it looked like a Portainer or Docker issue, but a quick search revealed thatthis actually had almost nothing to do with Portainer itself.。
The real root cause is that Docker on macOS uses Keychain by default to store login credentials, and in non-interactive sessions(such as SSH, background tasks, or when no user is logged in after boot), Keychain cannot pop up a GUI to unlock. This is in long-term unattended environments like the Mac mini particularly common.
To sum up the reason in one sentence: Docker wanted to get credentials from Keychain, but the current session did not allow any user interaction, so it failed directly.
The corresponding solution is actually not complicated, as long as youdisable Docker's credential dependency on Keychain.Specifically, edit:
~/.docker/config.json
and delete:
"credsStore": "desktop"
or:
"credsStore": "osxkeychain"
After completing the operation, running docker pull again indeed restored normal image pulling.
But the real problem was not here. Because immediately after, I found that multiple containers originally running in Docker Desktop began to experience anomalies, and services could not start normally, failing to recover even after restarting the Mac mini. After tossing and turning for a while, I ultimately had no choice but to reinstall Docker Desktop and redeploy all critical containers.。
Fortunately, this process was not very time-consuming, and everything was fully restored in about twenty minutes. But it was also this experience that made me realize one thing very clearly for the first time:The combination of Docker Desktop + Mac mini is inherently unsuitable as a “node” in a blog's active-active architecture.
The reason is not complicated, nor was it caused by this single operational error, but rather it isstructural.:
- Nodes in an active-active architecture should essentially be "server-like":
- they need to be unattended,
- must be able to deterministically recover their running state after a reboot,
- cannot rely on a desktop UI,
- and certainly cannot rely on mechanisms like Keychain that are strongly bound to user sessions.
而 Docker Desktop + macOS is exactly the opposite:
- strongly relies on GUI,
- strongly relies on user login states,
- and has complex internal states, lacking predictability.
- Once an issue occurs in the environment, the recovery path is highly uncertain.
This is not because Docker Desktop is poorly made, but because its positioning was never meant to be a “server-grade runtime environment.”
The issue triggered by this Portainer upgrade was just a catalyst. What really made up my mind was that it once again validated a conclusion:The Mac mini node is no longer suitable for continuing to bear the operational responsibilities in the blog's dual-active architecture.
Based on this judgment, I began the blog's dual-active nodethird optimization。
2 Core Adjustments of the Third Optimization
From a hardware and infrastructure perspective, this third optimization was indeed not a “major overhaul”: no new cloud service providers were added, nor were any new middleware components introduced, and the overall resource scale did not change significantly. However, at the architectural level, the change was actually very clear—The status and responsibilities of each node in the dual-active system have been redefined.
A most typical change is reflected in the intermini node—in previous articles, although intermini had already appeared, it played more of an auxiliary role, such as serving as an export node for Simply Static, and did not truly enter the core vision of the blog's dual-active architecture. When macmini exported and synchronized database files, it only incidentally synchronized data to intermini via syncthing; it was more like a “tool node” rather than a service node.
In this adjustment, intermini was officially incorporated into the external service system,transforming from an optional supporting role into one of the read-only nodes equivalent to the Chicago VPS, truly participating in the external publishing and dual-active redundancy of the blog content.
In the previous dual-active architecture, macmini handled both write operations and provided external read services. This design itself was fine, but as usage time increased, its shortcomings gradually became apparent. macmini is essentially a desktop device, relying on Docker Desktop, user login states, and system components. Once it encounters system upgrades, desktop service anomalies, or unattended reboots, the overall behavior can become unpredictable. This is a potential hazard for a blog system that needs to run stably over the long term.
Therefore, the core idea of this optimization is very straightforward:Let macmini step back to the “control position” and no longer directly bear service functions.
In the new structure, the WordPress instance on macmini is clearly positioned as awrite-only node, solely responsible for active write operations, such as publishing articles, modifying pages, or adjusting site configurations. All responsibilities for “externally displayed content” are handed over to the intermini and Chicago VPS nodes. That is to say, all operations that change the system state remain concentrated on macmini, while external access is entirely handled by the read nodes:

As for transferring content from the write node to the two read-only nodes, I still adopt the previous simple but reliable method: after completing modifications, export the WordPress database as a complete wordpress.sql file, and synchronize it to both the intermini and Chicago VPS nodes via rsync. Although this method is not as “high-end” as real-time database replication,the state is clear and the behavior is controllable, with low troubleshooting and recovery costs, making it highly suitable for small-scale personal blogs.
Both intermini and the Chicago node run on Debian 12, where Docker containers do not rely on desktop UIs or user sessions. These two nodes are positioned asfully equivalent read-only dual-active nodes, responsible for providing external blog access services. If any node experiences an anomaly, traffic can naturally switch to the other node without human intervention.
On the two read-only nodes, I deployed the same monitoring script to continuously listen to the directory of the same path/docker/wordpress/db/. Once a new wordpress.sql file is detected as synchronized in place, it automatically triggers the database reconstruction and import process, and sends the import results to my iPhone and Mac via Bark. The entire process does not require direct communication between nodes; as long as the file synchronization is successful and passes the MD5 verification, the content will be automatically updated.
A key benefit of this design is:there is almost no tight coupling between nodes. macmini does not need to perceive whether the read nodes are online, and the read nodes do not need to know each other's status. Each node only reacts to “whether the file appears,” keeping the overall complexity within a controllable range.
Of course, there are still passive write operations in the blog, such as user comments. To avoid single points of failure, I adjusted the comment processing logic of Cloudflare Worker so that comments can be written to the macmini, intermini, and Chicago VPS nodes simultaneously. Multi-point writing here does not pursue strict strong consistency, but rather ensurescomment data is not lost during node switching. In the context of a personal blog, this trade-off is acceptable.
Overall, the third optimization did not increase system complexity. Instead, by reallocating responsibilities, it allows each type of node to do what it is best at and most stable: macmini is responsible for writing and control, while intermini and the Chicago node are responsible for reading and external services, with the data flow method kept as intuitive and easy to verify as possible.
An additional benefit is that read nodes can naturally scale horizontally in the future—as long as a new node can receive and import wordpress.sql, it can join the external service, while the system still retains a clear, controllable “center” without losing order due to expansion.
In terms of actual user experience, this adjustment also brings noticeable changes:The writing process and external display are completely decoupled. Previously, when macmini bore both write and read responsibilities, intermediate states of modifying articles or adjusting structures in the backend might be seen by visitors in real time. Now that macmini does not provide external read services, I can repeatedly modify, discard, and start over locally without affecting the currently published content. Only after confirming the modifications are complete and exporting the database to synchronize with the read nodes will the blog switch to the new content all at once. In terms of user experience, this feels more like aclear and controllable publishing action, rather than taking effect while editing, making writing and adjustments more composed.
For the detailed design of the dual-active architecture and the division of node responsibilities, please refer to my previous article:The Second Reconstruction of Blog Architecture: Service Migration and Active-Active Disaster Recovery Practice Triggered by VPS Relocation 和 Implementation Scheme of WordPress Multi-Active Architecture (Simplified Version) in Personal Blogs。
3 A Detailed Issue with MariaDB Database Versions Accidentally Exposed
Throughout the overall process of this third optimization, most steps proceeded relatively smoothly. The only place that actually got me stuck was:After using rsync to synchronize wordpress.sql from macmini to intermini, importing it into the database failed, which directly resulted in the wordpress database on intermini being emptied.
When I first encountered this issue, I was actually a bit bewildered. Because whether it was directory monitoring, automatic triggering, or the database import script, the logic used on intermini was exactly the same as that on the Chicago node, and the Chicago node had always run very stably without ever experiencing a similar issue.
This meant that the problem was highly likely not in the script logic itself, but in some “lower-level, yet inconspicuous” place.
To quickly locate the problem, I bypassed the monitoring script and manually executed a database import command similar to the following on intermini for troubleshooting:
docker exec -i mariadb mysql -uroot -ppassword wordpress < /docker/wordpress/db/wordpress.sql
The result immediately threw an error:
ERROR at line 1: Unknown command '\-'.
This error message itself is not very friendly, but it at least makes one thing clear:The problem lies in the first line of the SQL file. So I used the head command to view the beginning of wordpress.sql:
head -n 20 /docker/wordpress/db/wordpress.sql
The result is as follows:

As you can see, the first line of the file is a specially formatted comment:
/*M!999999\- enable the sandbox mode */
In other words, when MariaDB on intermini parsed this comment, it treated it directly as an illegal command instead of a normally skipped comment, causing the entire import process to abort on the very first line.
But here comes another very critical question:Why did the exact same wordpress.sql file import completely normally on the Chicago node?
Continuing to investigate along this clue, I compared the MariaDB versions on the three nodes, using a command similar to the following:
docker exec -it mariadb mysql --version #这里的mariadb是容器名称
- macmini:

The version is 10.11.15. - Chicago VPS:

The version is also 10.11.15. - intermini:

The version is 10.11.6.
At this point, the problem is basically clear—although 10.11.15 and 10.11.6 both belong to the major version 10.11, the parsing behavior of MariaDB between certain minor versions for special comments (especially /*M! ... */ this kind of MariaDB-specific extension comment),indeed has differences。
More specifically, the lower version of MariaDB (10.11.6) on intermini cannot correctly recognize this type of comment format, so it throws an error directly when importing SQL; whereas 10.11.15 used on the Chicago node does not have this problem.
To verify this judgment, I did not modify the SQL file itself, but chose the most direct way:Upgrade the MariaDB version on intermini to 10.11.15After the upgrade was completed, I executed the import command again with the exact same wordpress.sql, and it succeeded on the first try without any further anomalies.
At this point, the root cause of this problem has been thoroughly confirmed.
Looking back now, this is actually a very typical but easily overlooked pitfall:When creating the Docker container, I used an image tag like mariadb:10.11, without explicitly locking the minor version number. In a multi-node architecture, as long as the minor version of one node is slightly different, it may trigger this “unobvious but fatal” compatibility issue in extreme cases.
Fortunately, the cost of falling into this pit this time was not high, but it reminds me once again:In a multi-node, replicable architecture, version consistency itself is part of system stability, not an optional detail.
If you don't want to upgrade the MariaDB version on intermini, you can actually solve this import problem simply by preprocessing the wordpress.sql file before importing it—deleting the comment on the first line, which can be achieved using the following command:
sed -i '/^\/\*M!/d' /docker/wordpress/db/wordpress.sql
However, it is still recommended to directly unify the MariaDB versions of all nodes to avoid encountering baffling problems like this one.
4 Homepage Reinforcement: From Structural Stability to Access Stability
After completing the third optimization, the blog has become very clear at the structural level: centralized writing, clear data distribution, and single-responsibility read nodes. From inside the system, it already possesses the stable form that an active-active architecture should have.
But in actual operation, I did an extra thing that was not “core” but very practical—hardening the homepage.
The reason for singling out the homepage is that the homepage occupies a very special position in the entire blog access path: it carries almost the first hop of all visitors, and at the same time, it is the page most susceptible to structural adjustments. Even if the backend is already very stable, as long as there is uncertainty on the homepage, the overall experience will be significantly amplified.
In this optimization, I adopted a more conservative strategy for the homepage than for ordinary pages: through staticization and forced caching, the homepage is directly returned by edge nodes most of the time, instead of going back to the origin of any WordPress instance. In this way, the availability of the homepage has been elevated from “depending on backend status” to “depending on whether the edge cache exists.”
This is especially worth explaining separately in the scenario of using Cloudflare APO. Many people would subconsciously think that after enabling APO, the homepage must be stable and cacheable. But in terms of actual behavior,the homepage cache status of APO is still associated with the working status of WordPressOnce an anomaly is triggered during the origin-pull process—such as a database connection error, a PHP fatal error, or even some plugin-level abnormal responses—Cloudflare may not be able to obtain a “cacheable valid response” in this round of requests, resulting in the invalidation of the homepage cache.
In other words,APO is not a “black-box cache” completely detached from the health status of the origin site”—when WordPress is in an abnormal or unstable state, the homepage is often the first page to expose the problem.
Because of this, in this third optimization, I chose to move the stability goal of the homepage further forward: even if a backend node experiences an anomaly in the short term, the homepage is still directly responded to by the already existing static results, instead of being forced to pull from the origin to verify the current system status.
It should be noted that this step is not a “necessary component” of the active-active architecture, nor does it participate in the data flow logic between nodes. It is more like an extra layer of cushion: when internal structures are adjusted, nodes are switched, or even temporarily unstable, the homepage can still maintain a relatively stable and predictable external performance.
Regarding the specific implementation of homepage staticization and caching strategies, you need to set relevant rules in the cache provider you use. Taking Cloudflare as an example, you can just use a simple cache rule:

Regarding the setting details of Cloudflare cache rules, in my previous article (see:Home Data Center Series Cloudflare Tutorial (6) CF Cache Rules Feature Introduction and Detailed Configuration Tutorial) I have already introduced them in detail, so I won't repeat them here. This chapter wants to emphasize more its position in the overall structure—it does not change the division of responsibilities within the system, but effectively reduces the uncertainty of the system boundary layer.
From the results, this sequence of “stabilizing the structure first, then hardening the boundaries” is worth it. The internal system can continue to evolve, while the experience presented to visitors externally is firmly fixed within a safer range.
5 Afterword: What Constitutes the Complete Form of a Multi-Active Architecture
Looking back at this third optimization, it is not about playing tricks on the number of nodes, nor is it pursuing the superficial bustle of “simultaneous online, simultaneous writing,” but rather onControl and data flowmade a clear cut.
If it only stops at “multiple WordPress nodes providing services at the same time”, then at best it is onlymultiple replicas; if writes, database, and control logic are still entangled on the same node, it can hardly be called true active-active.
The key change in this structural adjustment is:writes are centrally controlled, data is actively separated, while external services remain redundant. macmini no longer undertakes daily read services, but it is still the de facto control center—writes happen there, database export and distribution are also completed there, and structural operations are also initiated from there; while other nodes only do one thing:stably and predictably providing content access capabilities to the outside world。
From this perspective, the “complete form” of the active-active architecture is not that every node can do everything, but rather: the control plane is clear; the data flow is unidirectional and controllable; service nodes can be added or removed at any time without affecting overall consistency.
This structure does not pursue the theoretically strongest consistency, nor does it deliberately display complexity, but it is stable in engineering. Precisely because of this, the third optimization did not introduce too many “new technologies”, but was more of areallocation of roles. Which capabilities are worth centralizing and which must be decentralized, these judgments themselves may be more important than the specific implementation.
As for whether this structure is the “end point”, the answer is obviously no. It is just a phased choice under current constraints that leans more towards stability, maintainability, and controllability. The next time we touch it, it will likely not be because of “how else to show off”, but because some new real problem forces the structure to take another step forward.
And at that time, just record it once more.
I also, because you mentioned the PostgreSQL issue, while I ran into a pitfall in MariaDB where I didn't specify the minor version precisely, felt that I should record it and learn my lesson for the future.
Running a database in Docker is still a bit of a pain. Just like our discussion at the beginning of the month, I was screwed over by PostgreSQL. Images with the same repository and version number actually had different underlying operating systems. Running a database in Docker requires specifying an image precise to the minor version and OS version.