Homelab Weekly: Forgejo Migration and Remote Deployment Agents

Sep 21, 2026 min read

This week was split between moving the homelab’s source-control direction toward Forgejo and making DurpDeploy’s remote-agent model more useful under real deployment conditions. The GitOps repository carried fifteen commits touching sixty-nine files across dev, dmz, infra, and prd. That is a broad footprint, but it was not a redesign of every service. The work was a platform transition and reconciliation pass: update the application inventory to point at Forgejo, preserve the environment structure that already exists, and make sure the controllers that define the cluster still have a coherent desired state.

The application work was accompanied by two related software streams. DurpDeploy gained configurable interpreters for deployment steps, continued its remote-agent rollout, and moved a lifecycle save action to a more appropriate part of the UI. The durpdeploy-agent repository received the operational half of that change: progress logging, reconnect behavior after both failed and rejected polls, persisted agent identity, local setup documentation, container updates, and CI security analysis. Together, those changes address a practical question: how do I let a deployment system execute work near the target while keeping the server, agent, and user interface honest about what happened?

The common theme is avoiding implicit state. A migration should be visible in Git and reflected in the application definitions that reconcile it. A remote agent should retain the identity it was given rather than appearing as a new machine after a restart. A deployment step should state what interpreter executes it rather than relying on an accident of the image or host. And a long-running action should report progress while it is running, not only present a final result after the useful diagnostic window has passed.

Moving the GitOps Platform Toward Forgejo

The most visible GitOps commit this week was the move to Forgejo. Source control is infrastructure in a GitOps environment, even when its deployment YAML is only one application among many. It stores the desired state, hosts the review path, triggers automation, and becomes the place an operator consults while working through an incident. Treating it as a first-class platform service means its migration needs to be approached as an operational change, not merely a replacement of one web interface with another.

The repository activity shows the scope clearly. The GitOps application inventory includes .gitlab, Argo CD, Authentik, Bitwarden, cert-manager, CloudNativePG, CrowdSec, external-dns, external-secrets, Forgejo, GitLab Runner, LittleLink, Longhorn, MetalLB, Nebula Sync, the NFS provisioner, OpenSpeedTest, Redlib, Searxng, Traefik, and Vault. Those names span control-plane services, authentication, certificates, storage, networking, external integration, developer tooling, and user-facing applications. A source-control transition sits in the middle of this ecosystem. It has to coexist with runners, application manifests, credentials, ingress, database services, and the automation that consumes repositories.

I kept the move expressed through the normal GitOps structure rather than creating an out-of-band installation path. That matters because a manually installed Forgejo instance can appear successful on its first day while leaving the actual source of truth unclear. It also makes rollback and recovery harder. With the application definition and values stored in the repository, the intended service configuration is reviewable, Argo CD has an explicit reconciliation target, and a future rebuild does not depend on reconstructing a sequence of clicks or shell history.

A migration does not mean every old integration disappears at the same instant. The application inventory still includes .gitlab and gitlab-runner, which is a useful reminder that platform transitions are staged. Keeping existing components declared while introducing Forgejo is safer than assuming the name of the new service completes all dependent work. Repositories, CI jobs, runners, OAuth clients, webhooks, package or container references, DNS entries, and user habits may each have their own transition schedule. The GitOps change establishes the target state, while the surrounding dependencies can move deliberately instead of being bundled into a single unreviewable cutover.

Forgejo is a sensible fit for this kind of homelab service because it is self-hosted source control with a small enough operational shape to reason about. That is not an argument for treating it as disposable. It still needs persistent storage, database connectivity, authentication configuration, ingress routing, backups, and a controlled upgrade path. The point is to retain ownership of the service without adding needless distance between the service configuration and the people operating it. The platform should be boring to restore and boring to update.

The value of GitOps here is not that YAML eliminates decisions. It records them. When a source-control issue occurs, I can examine the application declaration, its sync status, generated resources, ingress, secrets references, and database dependencies. I do not have to start by asking which machine received an installer or which administrator changed a value manually. That bounded investigation path is the operational reason to put the migration in the same reconciliation system that already manages the rest of the homelab.

Preserving Environment Boundaries During Broad Reconciliation

The GitOps work touched all four environments: dev, dmz, infra, and prd. Environment names are not just organizational folders. They describe different operational responsibilities and exposure boundaries. A change applied indiscriminately across all of them can be technically valid YAML while producing a poorly understood platform. Broad repository activity therefore needs a narrower operational reading: each application change must belong to the environment where its controller, workload, and traffic pattern make sense.

The infrastructure environment remains where cluster-level building blocks naturally live. Cert-manager, external-secrets, CloudNativePG, Longhorn, MetalLB, the NFS provisioner, Vault, and likely parts of the Argo CD control plane all have concerns that are larger than a single user-facing application. They establish the services other workloads depend on. Their configuration has a wider blast radius, which is exactly why keeping their declarations explicit and reviewed is important.

The DMZ has a different job. Traefik, CrowdSec, external DNS integration, and externally reachable routes form an admission boundary between clients and internal services. The safe default is not to turn every internal application into a DMZ workload. It is to publish a specifically intended route through the appropriate proxy and keep the actual service and its state where they belong. This makes network exposure something that has to be declared, reviewed, and maintained, rather than a side effect of a convenient ingress annotation.

The development and production environments have an equally useful distinction even when a homelab’s scale does not resemble a commercial production estate. Development is where changes can be exercised with lower consequence. Production is where shared, relied-on services need the most conservative lifecycle and change handling. Declaring both makes it possible to keep that distinction real. It prevents a test value from silently becoming the production value just because the same chart happens to render in both places.

Argo CD is the piece that ties this model together. It observes the repository and reconciles the desired application state into the relevant cluster and namespace. But reconciliation is only as safe as the declarations it receives. A correct application list, correct target revisions, and appropriately scoped values matter more than clever automation around them. This week’s application updates are therefore maintenance of the control plane itself. When adding Forgejo or changing an existing component, I want Argo CD to have one unambiguous answer to what should exist and where.

There is a temptation in a multi-environment repository to standardize every value. Some standardization is good: common chart patterns, secret references, labels, and sync conventions reduce unnecessary variance. But standardization should not erase real differences in risk, exposure, storage, or lifecycle. A storage backend may be appropriate in infrastructure while an ingress rule belongs in DMZ. A production workload may need stricter resource and availability assumptions than a development test. The goal is consistency of operating model, not identical manifests everywhere.

Core Services Remain Explicit Dependencies

The application set updated this week includes the services that make the rest of the homelab usable: Authentik for identity, Bitwarden for credential access, Vault and external-secrets for secret delivery, cert-manager for certificates, external-dns for records, Traefik for ingress, and storage components such as Longhorn and the NFS provisioner. These are not independent conveniences. They form dependency chains that must remain legible during a platform change.

Take a typical public service path. A client resolves a name through DNS. The certificate is issued and renewed through cert-manager. Traffic reaches the appropriate entry point through Traefik. CrowdSec may apply its security policy at the edge. The application uses a credential delivered by external-secrets, which may ultimately originate in Vault. The workload writes to storage provisioned by Longhorn or NFS. A problem at any point can look like an application outage from a browser, even though the actual fault lives in a different controller.

That is why listing and reconciling these applications together has value. It does not mean I deploy every service as one atomic unit. It means the repository is a map of the platform’s dependencies. If Forgejo needs an authenticated external route and a database, its installation is easier to validate when the relevant identity, ingress, certificate, secret, storage, and database systems have all been intentionally managed in the same operational model.

CloudNativePG deserves special attention because source control and deployment automation are stateful systems. A PostgreSQL operator turns a database declaration into running instances, replication behavior, credentials, recovery mechanics, and generated resources. The application should consume its supported interface, not try to own the operator’s generated internals. That boundary keeps responsibility clear: Git expresses the intended cluster and application configuration, CloudNativePG manages PostgreSQL lifecycle, and the consuming service is configured with the connection information it needs.

Similarly, external-secrets and Vault should remain the path for sensitive values rather than placing credentials directly into chart values. A secret reference is operationally more valuable than a copied secret because it preserves a single owner and makes rotation or recovery less ambiguous. The Git repository can show that a service needs a credential without becoming the location where the credential is stored. That is a smaller attack surface and a clearer recovery story.

This is especially relevant during migration work. A service can be reachable and still be incomplete if its secret flow is fragile, certificate issuance is unmanaged, or storage is not part of the declared lifecycle. The success criterion is not a one-time login page. It is repeatable reconciliation, a known set of dependencies, and a service that recovers through its normal controllers after a restart, renewal, or node event.

Configurable Interpreters in DurpDeploy

DurpDeploy received a feature to make deployment-step interpreters configurable. This is a small capability with outsized practical value. Deployment systems commonly begin with the assumption that every step is a shell command. That works until a deployment needs a different runtime, a platform-specific command processor, a scripting language, or a deliberately constrained interpreter. At that point, silently assuming one shell turns the execution environment into hidden configuration.

Making the interpreter configurable moves that choice into the deployment definition. The deployment step can state how its command should be executed, and the runner or remote agent can honor the declared contract. This avoids relying on whichever shell happens to be the default in a container image or on a target machine. It also makes reviews better. A reviewer can see whether a step expects a shell, another interpreter, or a specific execution convention instead of inferring it from syntax.

This is not a reason to turn deployment definitions into arbitrary remote code execution surfaces. The existing authorization and target-selection rules still matter. An interpreter option should be treated as execution metadata with clear validation and an intentionally supported set of values. The important architectural benefit is that the server does not need to guess, and an agent does not need to reinterpret a command based on host accidents.

A configurable interpreter also makes the remote-agent rollout more realistic. Agents run on or near the systems where work is needed. They will not necessarily share every package, shell behavior, path layout, or operating-system convention with the central server. If the deployment contract makes the execution mechanism explicit, the differences become visible during definition and testing rather than surfacing as an opaque failure halfway through a rollout.

The right test for this feature is not only that a familiar shell command continues to work. It is that the selected interpreter reaches the agent correctly, commands are passed without accidental quoting transformations, failures return a useful error, and the chosen setting survives the normal lifecycle of saving, retrieving, and running a deployment definition. The feature belongs at the model boundary because every deployment execution path should see the same declared intent.

Continuing the Remote-Agent Rollout

The other DurpDeploy activity was continued work on the remote-agent rollout. A central deployment service is convenient while it executes only local tasks. Once it has to perform work on networks, hosts, or environments that should not be broadly exposed to the server, a remotely connected agent provides a cleaner boundary. The control plane can schedule and observe a task, while the agent performs the action using the access it was explicitly granted in its local environment.

This changes the failure model. A remote agent can restart, lose network connectivity, receive a rejected poll, encounter a command failure, or reconnect while the server has changed state. Those are ordinary conditions in distributed systems, not exceptional edge cases. The agent changes this week directly addressed them by reconnecting after rejected polls and after failures, and by reusing persisted identity.

Persisted identity is a foundational detail. If an agent registers as a new identity every time it restarts, the server cannot distinguish recovery from an entirely new machine. That creates stale records, complicates authorization, and makes debugging noisier. Reusing a durable identity allows the server to associate reconnects and execution history with the same intended agent. It also keeps approval and targeting decisions meaningful across normal process or container restarts.

Rejected polls deserve their own handling. A rejection can mean the server has no work, the agent’s state must be refreshed, a token is invalid, or the requested action is not currently permitted. Whatever the specific reason, treating it as a terminal crash wastes the normal recovery mechanisms already available. Reconnecting with controlled behavior gives the system a chance to recover from transient disagreement without leaving an agent permanently offline because one request was declined.

Failures during execution are equally important. A deployment task can fail for a legitimate operational reason: a service is unavailable, an image cannot be pulled, a precondition is absent, or the command itself returns an error. The agent should report that failure accurately, but it should not abandon its ability to receive later work. Separating task failure from agent liveness is the difference between a deployment system that degrades cleanly and one that requires human intervention after every unsuccessful command.

I also added deployment execution-progress logging. Final status alone is insufficient for a long-running action. If an operator sees only success or failure after several minutes, there is no way to distinguish a slow package install, a blocked network operation, a command waiting on input, or a process that has already progressed to a later stage. Incremental progress creates the observability needed to make a remote operation supportable.

Progress logging needs to be useful rather than merely noisy. The useful events are state transitions and output that explain what the executor is doing: task accepted, interpreter selected, command started, meaningful output received, completion status recorded. Sensitive material must not be logged just because it appears in a command environment or response. The remote agent is often closest to credentials and infrastructure, so its logs need the same discipline as the deployment definitions themselves.

The container update in the agent repository fits this operational model. Containerized agents need a constrained but sufficient runtime, predictable filesystem behavior for persisted identity, and an execution environment that does not quietly grant host-level privileges. The compose change that rejects the host cgroup namespace is a concrete guardrail. Sharing the host cgroup namespace unnecessarily expands the container’s view into host process and resource-management details. Refusing that configuration is safer than treating it as a harmless convenience, especially for an agent designed to execute deployment work.

User Interface and Delivery Workflow Improvements

DurpDeploy also moved the lifecycle save action to the page header. That is a focused interface change, but deployment tooling benefits from small interaction corrections. Lifecycle configuration is the kind of state where users need a visible, consistent save action while they review or change a workflow. Putting the action in the page header makes it part of the stable page-level control area rather than burying it among individual form details.

The value is not cosmetic. A deployment interface should make unsaved versus saved state clear, avoid encouraging accidental navigation away from changes, and provide a predictable action location across related pages. When the data controls how automation runs, a confusing save behavior is an operational defect. The smaller the gap between what the user believes they configured and what the server persists, the less likely a later execution is surprising.

The agent repository also added Snyk and SonarCloud workflows. These serve different but complementary purposes. Dependency and vulnerability analysis helps identify known risk in the software supply chain. Static analysis and quality reporting can identify implementation problems, maintainability concerns, and patterns that deserve review before they become production behavior. Neither tool replaces code review or runtime testing, but both provide a repeatable signal on every relevant change.

I added local setup documentation as well. Documentation is part of the delivery path when software has both a central service and a locally running agent. A contributor needs to know the repository’s expected setup, how to start the component, where persistent state belongs, and how to validate a change without reverse-engineering the project from CI failures. The merged local-setup work makes that path more explicit, which reduces the cost of reproducing an agent issue and helps keep development behavior aligned with the containerized deployment model.

The practical objective across these changes is a deployment system that is observable, restart-tolerant, and explicit about its execution contract. I do not need a large abstraction layer to get there. I need durable identity, clear progress, correct retry and reconnect behavior, deliberate runtime boundaries, and a UI that persists the configuration users think they saved.

Looking Ahead

The next Forgejo work is validation of the surrounding integration rather than adding features for their own sake. I will verify that the GitOps application remains healthy through ordinary Argo CD reconciliation, that its ingress and certificate path behave as intended, and that the database, storage, secret, and identity dependencies all recover through their expected controllers. I will also keep the GitLab-to-Forgejo transition staged, moving each dependent integration only when its replacement path is explicit and tested.

For the GitOps repository, I will watch for drift across dev, dmz, infra, and prd. The goal is not zero differences between environments. It is that every difference has an operational reason and is represented in the repository. Controller updates, secret delivery, certificate renewal, database operator activity, and ingress changes should converge without manual repair or surprise cross-environment exposure.

For DurpDeploy, the immediate follow-up is to exercise remote agents under the failures the new code is intended to handle. That means restarts with a persisted identity, rejected polls, a failed deployment command followed by later work, slow commands that produce observable progress, and interpreter selection across the supported execution environments. The agent should remain available after a task failure, while the server should retain enough execution history to explain the result.

I will also keep the security boundaries narrow. The cgroup-namespace guard is one example of rejecting a convenience setting whose access implications are larger than its benefit. As the agent rollout continues, every host capability, mounted path, credential, and interpreter option should have a named purpose. The desired end state is straightforward: Git records the platform, controllers reconcile it, agents execute only the work they are assigned, and failures produce enough evidence to fix the cause instead of guessing at symptoms.