This week I concentrated the homelab work in the GitOps repository, with eighteen commits across twenty-six files and changes spanning the infra and dmz environments. The visible changes touched Argo CD, CloudNativePG, Forgejo, Harbor, the internal proxy, and the NFS provisioner. The underlying theme was more consistent: make the control planes agree on who owns a piece of state, how it is applied, and how it recovers when a workload or controller does not follow the happy path.
That is a less glamorous category of work than standing up a new service, but it is where a self-hosted platform becomes reliable. GitOps only produces the intended result when Argo CD can apply the desired state safely, operators can reconcile their resources without conflicting ownership, and applications consume the credentials and routes actually intended for them. I addressed several small mismatches in those contracts this week. I also made a few targeted Forgejo changes so the service can accept the expected identity flow and continue recovering workers reliably after database-side changes.
The other repository with activity was my dotfiles repository. It received seven small update commits. Those commits are deliberately light on description, so I am not going to invent configuration details that the commit history does not establish. I treat dotfiles as operational tooling too: frequent, narrow updates are preferable to letting workstation changes become an unreviewable bundle. The substantive engineering story this week is in the GitOps work.
Server-Side Apply for the CNPG Argo CD Application
The first GitOps correction was enabling server-side apply for the CloudNativePG Argo CD application. At a glance, this is one setting in an Argo CD application definition. In practice, it changes the way Kubernetes records and resolves ownership of the fields in a resource.
Traditional client-side apply stores the last-applied configuration as an annotation and calculates a patch by comparing that stored declaration, the new declaration, and the live object. This works well for simple resources, but it becomes less comfortable around operator-managed custom resources. CloudNativePG creates and updates resources through its controller, and Kubernetes itself may default or normalize fields. The resource can therefore have several legitimate actors. Treating a controller-owned resource as though one client-side apply actor owns every field is an invitation to noisy diffs, oversized annotations, and conflicts that obscure the actual desired-state change.
Server-side apply uses Kubernetes managed fields instead. A field manager claims the fields it is responsible for, and the API server performs the merge with awareness of other managers. That is the correct direction for an Argo CD application whose resources are part of an operator-driven system. Argo CD should own the fields declared in Git. CloudNativePG should own the status and operator-controlled parts of its resources. Kubernetes admission and defaulting should be allowed to do their work without making the application appear perpetually out of sync for an incidental representation difference.
This is particularly relevant to a database operator. A CNPG cluster is not a static Pod specification. The custom resource describes a database topology and lifecycle policy. The operator then creates StatefulSets, Services, Secrets, configuration, and recovery behavior from that declaration. The more closely Argo CD’s application strategy matches Kubernetes’ field ownership model, the less likely a routine reconcile is to compete with the operator responsible for turning the declaration into running PostgreSQL instances.
I enabled this behavior specifically for the CNPG application instead of applying a global switch without evidence that every application needs it. A GitOps repository is easier to operate when application-specific behavior remains scoped to the application with the relevant controller semantics. The intent is not to make sync behavior mysterious or hide conflicts. It is to let Kubernetes report a real ownership conflict when two actors actually want the same field, while avoiding false friction from fields that are not Argo CD’s responsibility.
The practical test for this sort of configuration is not simply whether one sync completes. It is whether repeated reconciliations remain quiet, whether a controller update does not cause GitOps churn, and whether the desired changes still surface clearly in Argo CD. Server-side apply is valuable here because it makes the ownership boundary explicit in the same control plane that must enforce it.
Harbor Reaches the DMZ Through an Explicit Route
I added a DMZ ingress for Harbor and documented the cross-cluster routing that supports it. Harbor is an internal registry service with state, credentials, an API, and a browser interface. Publishing it across an environment boundary deserves more than enabling an ingress because the chart provides one. The request path must be intentional from the entry point through the DMZ proxy and into the internal service.
The important design choice is that the route is explicit. The DMZ environment is where externally reachable entry points belong, while the stateful Harbor installation and its backing dependencies remain in the internal platform environment. That separation keeps the registry’s database, storage, and operational components out of the exposed cluster while still permitting a deliberately defined access path. The ingress does not move Harbor into the DMZ. It defines how a request is admitted and forwarded across a boundary.
This distinction matters for both security and operations. An ingress object is not only a hostname and a backend. It participates in TLS termination, proxy rules, DNS, network policy, service discovery, and failure reporting. When an internal application is accessed through a DMZ component, the route must preserve the right host behavior and reach the intended backend without casually widening connectivity between clusters. Documenting the cross-cluster arrangement is part of the implementation, not an afterthought. Future troubleshooting should start with a known path instead of relying on tribal knowledge about which proxy can reach which service.
For Harbor, routing has additional consequences because a registry client is less forgiving than a normal browser. Image pulls use registry API endpoints, auth exchanges, redirects, and content downloads. A route that loads the web interface but mishandles the registry API, host headers, or TLS will fail when a container runtime actually needs it. The GitOps declaration therefore needs to match the service’s real external URL and the proxy behavior needs to preserve that contract. This is also why exposing the service only through an intentional DMZ path is preferable to placing ad hoc node-level routing or host-file exceptions around it.
The change complements the registry work from the prior week. Harbor now has a defined topology: application and persistent dependencies in the internal environment, a DMZ-facing ingress where exposure is required, and documented routing between the two. That is a design I can reason about during maintenance. If a pull fails, there are bounded places to inspect: DNS and ingress admission in DMZ, the cross-cluster proxy path, Harbor’s own service, and the registry’s authentication configuration. It is much better than treating networking as an invisible side effect of a Helm install.
I will continue to keep the boundary narrow. Harbor may be useful to developers, automation, and cluster nodes, but usefulness is not a reason to expose every internal endpoint. Any additional route should have a named consumer and a reason for existing. The lowest-risk network path is the one that never gets created.
Aligning Harbor and Forgejo With CloudNativePG Availability
I also aligned Harbor’s database availability configuration with the Forgejo pattern. This followed the earlier work to place both services on CloudNativePG-backed PostgreSQL. The goal was consistency in the actual database behavior, not merely matching YAML formatting between two applications.
Availability in a PostgreSQL deployment is a stack of choices. At the database layer, the number of instances, replication settings, storage behavior, failover behavior, and recovery configuration determine what a failure can survive. At the application layer, the chart needs correct connection details, a credential source, and behavior that tolerates a database endpoint becoming briefly unavailable. At the GitOps layer, the desired specification has to converge predictably after an operator changes the generated resources underneath it.
Forgejo already represented the intended baseline for a stateful internal application: use the supported CloudNativePG configuration, consume the operator-managed application credentials, and let the database controller own database lifecycle concerns. Harbor should not be a special case just because it is a registry. It has a similarly important stateful database dependency, and its availability assumptions need to align with the pattern already established for Forgejo. Consistency reduces the chance that two services which look similarly configured behave differently during a node loss, failover, or recovery event.
This does not mean every database-backed application must receive identical sizing or topology. Harbor and Forgejo can have different workloads, storage profiles, and performance needs. The reusable part is the ownership model: CloudNativePG manages PostgreSQL lifecycle and its generated application secret, Helm charts consume the declared connection inputs, and Argo CD reconciles the desired custom resources rather than trying to manage database internals directly. A consistent pattern reduces the cognitive load of debugging without forcing unrelated workloads into a false uniformity.
The timing of the configuration is important. Availability features only matter when the normal path stops being normal. A one-instance setup can be sufficient for a disposable development component, but it is a conscious availability tradeoff. For source control and the registry, which are shared platform services, the failure and recovery story needs more deliberation. I want an operator restart, a worker failure, or a database replica transition to be a bounded incident, not an occasion to reconstruct hidden setup steps.
The change also reinforces why versioned GitOps configuration is useful. Database behavior must be visible as a declared property of the service. A manually changed replica count or a hand-created credential can make a system look healthy while leaving the repository inaccurate. The next reconciliation, upgrade, or rebuild then reintroduces the gap. Keeping the intended availability model in Git gives me a reviewable source of truth and makes recovery repeatable.
Forgejo Database, Secrets, and Worker Recovery
Forgejo received several follow-up fixes this week. I corrected its CloudNativePG configuration and worker recovery behavior, then made the Forgejo database password consume the CNPG auto-generated application Secret. These are related changes because reliable application recovery depends on the application having a durable, authoritative database connection contract.
A database password should have one clear owner. With CloudNativePG, the operator creates and manages the application Secret associated with the database cluster and role. Forgejo should consume that Secret rather than carrying a separately created password copy or depending on bootstrap-time assumptions. A copied password value creates two sources of truth. It can work during initial installation while quietly diverging after a reinitialization, secret rotation, or recovery action. The application then fails in the least convenient condition, when the database has already had an incident.
Using the generated CNPG application Secret simplifies the data path. CloudNativePG knows the database role and generates the credential material. Kubernetes stores that material in the Secret the application is configured to reference. Forgejo consumes that Secret through its chart configuration. Argo CD sees the declared reference rather than an inline password. There is no benefit in adding a second secret-generation mechanism merely to relay a value already supplied by the database operator.
The correction also makes security review easier. The GitOps repository describes that Forgejo needs a database credential, but it does not need to carry the credential itself. The Secret reference establishes an interface between the chart and the database operator. If I need to inspect an authentication failure, I can verify the cluster’s generated secret, the role it represents, and the chart’s reference. That is much more direct than tracing a password through duplicated manifests and hoping every copy remained synchronized.
Worker recovery was the second part of this work. Forgejo has background activity that must recover cleanly when a process, database connection, or worker loop is interrupted. A source-control service cannot be treated only as an HTTP endpoint. Repository operations, hooks, queues, index work, notifications, and maintenance activity all depend on background execution. If that work fails to restart or reconnect after an infrastructure event, the UI can appear healthy while operations stall underneath it.
I fixed the configuration so worker recovery follows the corrected service and database relationship. The goal is not to implement a custom recovery controller alongside Forgejo. The existing application, Kubernetes workload controller, and CloudNativePG operator already provide the appropriate layers. The necessary work was to remove the configuration mismatch that stopped the normal recovery path from functioning as intended. That is the smallest reliable solution: use the recovery behavior the platform already supplies and make sure the application has valid credentials and configuration when it comes back.
There is an operational lesson here. A deployment should be evaluated after a controlled failure, not only after first install. Restarting a pod, reconciling a Secret, or forcing a connection loss exposes whether the pieces agree on their interfaces. For Forgejo, the application secret and worker behavior are part of that test. I want a database-side recovery event to return the service to normal operation through declarative reconciliation, without a manual password repair or a bespoke runbook step.
OAuth2 Auto-Registration for Forgejo
I enabled OAuth2 auto-registration in Forgejo. This is a focused identity-flow change, and it is valuable because it removes unnecessary manual account provisioning for users who have already authenticated through the configured OAuth2 provider.
The important qualifier is that auto-registration is not the same as unrestricted anonymous sign-up. The registration event is tied to successful authentication from the configured identity provider. The service can create the local Forgejo account when it receives a valid identity assertion, rather than requiring an administrator to pre-create every account or asking the user to navigate a second, separate registration process. That makes the expected path simpler while retaining the existing authentication boundary.
For a homelab service, manual user creation often begins as an acceptable shortcut because there are only a few users. Over time, it becomes a source of friction and inconsistent identity records. Someone can authenticate successfully but still be unable to use the service because the local account does not exist. An administrator then has to intervene for a task that the identity system already proved should be allowed. Auto-registration replaces that repetition with a declared application behavior.
The configuration needs to stay scoped to the identity provider I intend Forgejo to trust. Automatically registering users based on an unvetted identity source would be a bad trade. The desired model is straightforward: Forgejo delegates authentication to a known OAuth2 provider, it maps the returned identity into a local account according to the configured rules, and authorization inside Forgejo remains separate from the fact that an account was created. Authentication establishes who the person is. Repository access, organization membership, and administrative permissions still need their own explicit controls.
This change also reduces onboarding variance. The same successful OAuth2 login can create the initial local account during ordinary use, and the application does not need a special administrative workflow just to make that first login useful. It is a small improvement, but small identity-flow improvements are worth making when they eliminate a recurring manual handoff without weakening the authentication boundary.
NFS Reclaim Policy and Data Preservation
I changed the NFS provisioner so volumes are reclaimed and their backing files are archived after PVC deletion. Storage cleanup is one of those details where the default behavior should be chosen deliberately because the consequences are data loss or persistent clutter.
A Kubernetes PersistentVolumeClaim is an application-facing request for storage. Deleting the claim does not automatically answer what should happen to the backing data. The reclaim policy defines that behavior. If the volume is retained, data remains available for manual recovery but storage accumulates until someone cleans it up. If it is deleted, the backing data is removed with the volume, which can be appropriate for ephemeral workloads but is dangerous when a deletion was accidental or a chart upgrade replaced an object unexpectedly.
For the NFS provisioner, archiving files after PVC deletion provides a practical middle ground. The cluster can reclaim the Kubernetes storage object and avoid leaving an active volume attached to a deleted claim, while the underlying files are preserved in an archive location instead of being immediately destroyed. This makes lifecycle cleanup visible and gives me an opportunity to recover data if the PVC deletion was not intended.
The behavior is especially appropriate in a homelab where storage usage, workload experimentation, and chart changes can be more fluid than in a tightly controlled production system. I still want workloads to clean up their Kubernetes resources correctly. I do not want a typo or a Helm transition to turn into instant irreversible data removal. Archiving creates a recovery window without pretending that deleted claims should remain active forever.
There is an explicit tradeoff: archived data consumes storage and needs periodic review. This is not free retention. Capacity monitoring and a documented archival cleanup policy remain necessary, particularly for large database or registry volumes. The point is that cleanup becomes a deliberate operational task rather than an irreversible side effect of claim deletion. If the archive begins to consume meaningful capacity, I can review the contents against known retention needs and remove data with confidence.
The GitOps value is that this policy applies at the provisioner boundary. I do not need to remember a special cleanup guard for each application. Every dynamically provisioned volume gets the same predictable lifecycle handling. That is the correct place to solve the policy because all relevant claim deletions flow through it.
Documenting the Agent Workflow and Maintaining Dotfiles
I updated AGENTS.md to document the project structure and the required merge request workflow. Documentation for an automation-facing repository should be concrete about the repository’s boundaries and delivery expectations. The file is not a substitute for reading the manifests, but it helps ensure that changes begin from the right architectural context and arrive through the expected review path.
The practical guidance is valuable because this repository spans multiple environments and several infrastructure classes. A change to a DMZ ingress has a different blast radius from an internal database setting. An application chart, an Argo CD definition, and an NFS provisioner policy each reconcile through different controllers. Clear project structure makes it less likely that a focused fix is accidentally placed in the wrong environment or that a broad change is made when a local configuration is sufficient.
The merge request requirement is equally important. GitOps changes deserve reviewable diffs because YAML can be syntactically correct while still changing a cluster-wide behavior. A merge request creates a clear unit of intent: what changed, why it belongs in a particular environment, and what component will reconcile it. It also provides the moment to notice a route that is too broad, a secret reference that is wrong, or a controller setting that applies beyond the intended scope.
My dotfiles repository also saw seven update commits. The available summary does not identify individual file or configuration changes, so I will keep the record factual rather than turning generic commit messages into fabricated workstation details. The repository is still part of how I operate the homelab. Shell configuration, editor settings, terminal behavior, and local tooling influence the reliability of every administrative task. Small, regular commits keep those changes recoverable and reviewable, just as they do for cluster configuration.
Looking Ahead
The immediate follow-up is validation rather than more configuration. For the CloudNativePG application, I will watch repeated Argo CD syncs and normal operator reconciliation to confirm that server-side apply reduces ownership noise without hiding a meaningful conflict. The desired outcome is boring: the Git declaration converges, CNPG manages its fields, and diffs represent actual intended changes.
For Harbor, I will validate the DMZ route with both browser and registry-client behavior. The useful checks are TLS and host handling, authentication flow, API endpoints used by image clients, cross-cluster connectivity, and the failure behavior when the internal backend is unavailable. A route is only complete when it works for the service protocol it exposes, not merely when its landing page returns a response.
For Forgejo, I will continue treating recovery as a first-class property. I want to verify the CNPG-generated credential reference after reconciliation and exercise ordinary workload recovery so background workers reconnect and resume cleanly. OAuth2 auto-registration also warrants a controlled login test to confirm that a valid provider identity creates the expected local account without granting permissions beyond the configured authorization model.
Finally, I will keep an eye on NFS archive growth. The new reclaim behavior favors recoverability over immediate deletion, which is the right default for this environment, but it needs capacity awareness and periodic cleanup. The broader direction remains the same: keep state ownership explicit, expose services through intentional routes, and choose lifecycle behavior that is understandable when something fails.