add argocd documentation

This commit is contained in:
rezayaghobi-dev
2026-09-20 12:47:30 +03:30
parent 2b03c2c823
commit 5f75758f8f
6 changed files with 260 additions and 4 deletions

View File

@@ -12,6 +12,7 @@ A self-hosted homelab running on Docker Compose, built and maintained on an Ubun
![Kubernetes](https://img.shields.io/badge/Kubernetes-326CE5?style=flat&logo=kubernetes&logoColor=white)
![K3s](https://img.shields.io/badge/K3s-FFC61C?style=flat&logo=k3s&logoColor=white)
![Vagrant](https://img.shields.io/badge/Vagrant-1868F2?style=flat&logo=vagrant&logoColor=white)
![ArgoCD](https://img.shields.io/badge/ArgoCD-EF8B1B?style=flat&logo=argocd&logoColor=white)
![GitLab](https://img.shields.io/badge/GitLab-FC6D26?style=flat&logo=gitlab&logoColor=white)
![Traefik](https://img.shields.io/badge/Traefik-36CFD1?style=flat&logo=traefik&logoColor=white)
![Nexus](https://img.shields.io/badge/Nexus-BC1E2D?style=flat&logo=sonatype&logoColor=white)
@@ -46,7 +47,7 @@ This is a real, running homelab — not a demo. It's deployed on a home Ubuntu s
| **Custom CI images** | `docker-compose/gitlab/ci-images-reference/` | Self-built, minimal Docker images for CI pipelines, stored in a self-hosted Container Registry, rebuilt biweekly — see [Custom CI Runner Images](docs/CI-Custom-Images.md) |
| **Voting app CI/CD** | `voting-app-monorepo` (self-hosted GitLab, referenced here) | Full pipeline: build, test, vulnerability scan, 3-environment deploy with rollback, load test, DB backup to MinIO — see [Voting App — Mono-Repo](docs/Voting-App-Monorepo.md) |
| **Object storage** | `docker-compose/minio/` | Self-hosted MinIO (S3-compatible), `-cpuv1` release tag for CPU compatibility — backup target for the voting app's Postgres dumps |
| **K3s cluster** | `kubernetes/` | Single-node K3s cluster running alongside the Docker Compose stack, fronted by the same reverse proxy via NodePort, monitored through the same Prometheus/Grafana/Loki stack — see [K3s](docs/K3s.md) |
| **K3s cluster** | `kubernetes/` | Single-node K3s cluster running alongside the Docker Compose stack, fronted by the same reverse proxy via NodePort on ingress-nginx, managed via **Argo CD** GitOps (applications/ and argocd-apps/ folders) — monitored through the same Prometheus/Grafana/Loki stack — see [K3s](docs/K3s.md) and [ArgoCD](docs/argocd.md) |
## Learning labs (not always-on)
@@ -191,6 +192,7 @@ homelab/
│ ├── Architecture-and-Hardware.md
│ ├── Monitoring-Stack.md
│ ├── K3s.md
│ ├── ArgoCD.md
│ ├── Network-Services.md
│ ├── Media-Automation.md
│ ├── Workflow-Automation.md
@@ -209,6 +211,18 @@ homelab/
├── kubernetes/
│ ├── alloy-rbac/
│ │ └── alloy-rbac.yaml
│ ├── apps/
│ │ ├── README.md
│ │ └── homepage/
│ │ ├── configmap.yaml
│ │ ├── deployment.yaml
│ │ ├── ingress.yaml
│ │ ├── kustomization.yaml
│ │ └── service.yaml
│ ├── argocd-apps/
│ │ ├── README.md
│ │ └── homepage/
│ │ └── homepage-app.yaml
│ ├── kube-state-metrics/
│ │ ├── kube-state-metrics.yaml
│ │ └── prometheus-rbac.yaml
@@ -245,6 +259,8 @@ docker compose up -d
Rather than pulling and recreating manually, every always-on stack has a matching Ansible playbook that pulls the latest images, recreates containers, verifies they're healthy, and prunes unused images — run through Semaphore's web UI. See [Ansible Playbooks](docs/AnsiblePlaybooks.md) for details on how the shared role works.
For the K3s cluster, updates are GitOps-driven rather than Ansible-driven: Kubernetes manifests live in `kubernetes/apps/`, one folder per app with a `kustomization.yaml`, and [Argo CD](docs/argocd.md) continuously syncs the cluster to match whatever is on `main` — no manual `kubectl apply` after the initial setup. New apps are added by dropping the manifests in `kubernetes/apps/<name>/` and the corresponding `Application` in `kubernetes/argocd-apps/<name>/`.
## Security notes
- All `.env` files, runtime config directories, and databases are excluded via `.gitignore` — only deployment definitions (`docker-compose.yml`) and `.env.example` placeholders are tracked.

View File

@@ -8,6 +8,7 @@ Documentation for a self-hosted homelab running entirely on Docker Compose. This
- [Monitoring Stack](Monitoring-Stack.md)
- [Alerting](Alerting.md)
- [K3s](K3s.md)
- [ArgoCD](ArgoCD.md)
- [Network Services](Network-Services.md)
- [Media Automation](Media-Automation.md)
- [Workflow Automation](Workflow-Automation.md)
@@ -41,7 +42,7 @@ Documentation for a self-hosted homelab running entirely on Docker Compose. This
| **Registry** | **Nexus3** (Docker Hub pull-through cache + artifact repo) |
| **Passwords** | **Vaultwarden** (Bitwarden-compatible, 30+ char passwords) |
| **CI/CD** | Self-hosted **GitLab** (Omnibus) + Docker-executor **Runner** |
| **Orchestration (K8s)** | Single-node **K3s** cluster, monitored via the same Prometheus/Grafana/Loki stack |
| **Orchestration (K8s)** | Single-node **K3s** cluster, fronted by **Traefik** via ingress-nginx NodePort, managed via **Argo CD** GitOps (manifests in `kubernetes/apps/`, watched by `kubernetes/argocd-apps/`), monitored via the same Prometheus/Grafana/Loki stack |
This is a running system, not a demo — screenshots throughout this wiki are from the live dashboards.

View File

@@ -22,9 +22,24 @@ curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="server \
- **`--flannel-backend=vxlan`** — explicit rather than relying on the default, and chosen over `host-gw` since VXLAN works across nodes that aren't on the same L2 segment, keeping the door open for a future worker node.
- **`--write-kubeconfig-mode=644`** — makes `/etc/rancher/k3s/k3s.yaml` world-readable so a personal, non-root kubeconfig copy can be made without repeated `sudo`.
## Exposure pattern: NPM in front, NodePort behind
## Exposure pattern: Traefik → ingress-nginx → Ingress
Rather than running a second ingress controller inside the cluster (which would mean two reverse proxies stacked for no benefit), each service gets a `Service` of `type: NodePort`. The existing reverse proxy adds one more proxy host pointing at `homeserver:<nodeport>` — identical to how it already fronts every Docker container. From the proxy's perspective, a K8s pod behind a NodePort is indistinguishable from a Docker container; same TLS setup, same cert renewal flow, zero new moving parts in the HTTPS chain.
K3s is fronted by the existing Docker-side Traefik reverse proxy, which already owns ports 80/443 (see [Reverse Proxy](Reverse-Proxy.md)). Because K3s itself can't bind those ports, the current standard for exposing an in-cluster service is:
```
Client → Traefik (:80/:443) → ingress-nginx NodePort → Ingress → Service → Pod
```
1. **Traefik** (Docker container) terminates TLS (`*.home.server` via mkcert) and routes by `Host` header, same as every Docker Compose service.
2. **ingress-nginx** runs inside K3s. Its `Service` is exposed as a **NodePort** on the host, giving Traefik a stable `homeserver:<nodeport>` target to forward to — no new ports on the K3s side, no port conflicts with the host's Traefik.
3. **ingress-nginx** (the in-cluster Ingress controller) matches the request against `Ingress` resources in the cluster and forwards to the right `Service`.
4. **Service → Pod** as normal Kubernetes routing.
This keeps a single TLS termination layer and a single DNS entry point (`*.home.server` via Pi-hole), while letting in-cluster `Ingress` resources carry the routing logic as code alongside the workloads they protect. Any app deployed via [Argo CD](ArgoCD.md) follows this pattern — its `Ingress` (e.g., `kubernetes/apps/homepage/ingress.yaml`) is served by ingress-nginx behind the NodePort.
### Legacy NodePort-only services
The earlier kube-state-metrics deployment was exposed as a standalone `Service` of `type: NodePort` without an in-cluster Ingress — a simpler approach used before ingress-nginx was introduced. Services still using that legacy pattern (direct NodePort, no Ingress object) continue to work the same way: Traefik forwards to `homeserver:<nodeport>` and the Service routes to Pods directly. New work should use the ingress-nginx path above unless a service genuinely has no need for `Host`-based routing.
## Namespace and resource governance

112
docs/argocd.md Normal file
View File

@@ -0,0 +1,112 @@
# ArgoCD — GitOps for K3s
[Argo CD](https://argoproj.github.io/cd/) is a declarative continuous delivery tool for Kubernetes. In this homelab it's the bridge between this repo and the K3s cluster: **git is the source of truth**, and Argo CD continuously watches the git repository, reconciling the cluster to match whatever manifests are tracked on `main`.
Add a Deployment, change a resource limit, delete an Ingress — commit it, and Argo CD pulls the change into the cluster automatically, no manual `kubectl apply` required. It's Kubernetes-as-GitOps rather than Kubernetes-as-state-you-manually-maintain.
## Repo layout
Argo CD apps in this repo follow a split structure:
```
kubernetes/
├── apps/
│ └── homepage/
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── configmap.yaml
│ ├── ingress.yaml
│ └── kustomization.yaml
└── argocd-apps/
└── homepage/
└── homepage-app.yaml
```
- **`kubernetes/apps/<name>/`** — the actual Kubernetes workload manifests for each app. Bundled with `kustomization.yaml` (Kustomize), so common operations like renaming a namespace or swapping an image tag can be done via patches rather than editing every YAML. See [`kubernetes/apps/README.md`](../kubernetes/apps/README.md) for details.
- **`kubernetes/argocd-apps/<name>/<name>-app.yaml`** — the Argo CD `Application` object that tells Argo CD "watch this git path, deploy it into this namespace." These live one level up, deliberately separate from `kubernetes/apps/`, so Argo CD never watches its own Application definitions and can't recurse.
The first app managed this way is the **Homepage** dashboard — see `kubernetes/apps/homepage/` for the manifests and `kubernetes/argocd-apps/homepage/homepage-app.yaml` for the Application.
## Application manifest
Each app gets a thin `Application` manifest. The Homepage one looks like:
```yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: homepage
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/rezayaghobi-dev/homelab
targetRevision: main
path: kubernetes/apps/homepage
destination:
server: https://kubernetes.default.svc
namespace: homepage
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
```
- `path` points at the `kubernetes/apps/<name>/` directory — Argo CD treats the `kustomization.yaml` there as the entry point.
- `automated.prune = true` — anything deleted from git gets deleted from the cluster.
- `automated.selfHeal = true` — if someone edits the live cluster out-of-band, Argo CD notices the drift and corrects it.
- `CreateNamespace=true` — the destination namespace is created automatically if it doesn't exist.
This is the current state of the cluster's GitOps control loop: Argo CD is the reconciler, this repo is the spec, and the cluster state is the realized output.
## How traffic reaches an app
K3s itself doesn't own ports 80/443 — the Docker-side Traefik reverse proxy already binds them (see [Traefik](Traefik.md)). So the exposure pattern for an Argo CD-managed app is:
```
Client → Traefik (:443) → ingress-nginx NodePort → Ingress → Service → Pod
```
1. **Traefik** (Docker container, `network_mode`-adjacent to the host) terminates TLS (`*.home.server` via mkcert) and routes based on `Host` header.
2. **ingress-nginx** runs *inside* the K3s cluster. Its `Service` is exposed as a **NodePort** on the host, giving Traefik a stable `homeserver:<nodeport>` target to forward to — the same pattern used for pre-ArgoCD K3s workloads (see [K3s — Exposure pattern](K3s.md#Exposure-pattern)).
3. **ingress-nginx** (the in-cluster Ingress controller) receives the request, matches it against `Ingress` resources in the cluster, and forwards to the right `Service`.
4. **Service** → **Pod** as normal Kubernetes routing.
This keeps a single TLS termination layer (Traefik) and a single DNS entry point (`*.home.server` via Pi-hole), while letting in-cluster Ingress resources carry the routing logic as code alongside the workloads they protect.
![ArgoCD dashboard](images/argocd.png)
## Adding a new app
1. **Create the workload manifests.**
```bash
mkdir kubernetes/apps/<name>
```
Drop your `deployment.yaml`, `service.yaml`, `configmap.yaml`, `ingress.yaml` (if any), and a `kustomization.yaml` listing them as resources. The `kustomization.yaml` is required — Argo CD uses it as the entry point for the `path`.
2. **Create the Application manifest.**
```bash
mkdir kubernetes/argocd-apps/<name>
```
Copy `kubernetes/argocd-apps/homepage/homepage-app.yaml` as a template into `kubernetes/argocd-apps/<name>/<name>-app.yaml`. Update the three fields that vary:
- `metadata.name` → the new app's name
- `spec.source.path` → `kubernetes/apps/<name>`
- `spec.destination.namespace` → the namespace the app runs in
3. **Apply the Application once.**
```bash
kubectl apply -f kubernetes/argocd-apps/<name>/<name>-app.yaml
```
Argo CD picks it up immediately and creates the app. After this initial `kubectl apply`, **all** future changes — including edits to the manifests, resource changes, even namespace recreation — flow through git alone. Argo CD's `selfHeal` and `prune` will keep the cluster in sync with whatever is on `main`.
4. **Point Traefik at it.** The app's Ingress will typically be served by the in-cluster ingress-nginx behind a NodePort. Add or reuse a Traefik router (Docker labels on the ingress-nginx Service itself, or a `dynamic/` file-provider entry if the Service isn't on `web_net`) so `<name>.home.server` resolves to the right NodePort.
That's it — commit, push, and Argo CD handles the rest. No manual `kubectl` on the cluster; no drift between what's running and what's in git.
## Why this matters
On a single-node learning cluster this matters less for HA and more for discipline: writing a `Deployment` that actually ships is different from applying one manually and calling it done. GitOps forces the manifests and the running cluster to be the same thing — the diff in the Argo CD UI is an honest report of "what happens if I merge this PR," not a best-effort guess at what `kubectl apply` did last time.
[← Back to Home](Home.md)

43
kubernetes/apps/README.md Normal file
View File

@@ -0,0 +1,43 @@
# Kubernetes Apps
This folder holds the actual Kubernetes workload manifests for every app managed via Argo CD's GitOps flow. Each subfolder is one application, named after the app itself:
```
kubernetes/apps/
├── README.md ← you are here
├── homepage/
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── configmap.yaml
│ ├── ingress.yaml
│ └── kustomization.yaml
├── <name>/ ← new apps go here
│ ├── ...
│ └── kustomization.yaml
```
## What lives here
- **Workload manifests** — `Deployment`, `Service`, `ConfigMap`, `Ingress`, `Secret`, etc. These describe the pods, networking, and config that Argo CD applies to the cluster.
- **`kustomization.yaml`** — the Kustomize entry point for the app. Argo CD watches the path this file declares, so every app folder must include one and list every sibling manifest under `resources:`.
These are the files that end up running in the cluster. This is "what runs."
## What does *not* live here
- **Argo CD `Application` objects.** Those live in the sibling [`../argocd-apps/`](../argocd-apps/README.md) folder, deliberately kept separate so Argo CD never watches its own definitions.
- **Namespace-scoped governance objects.** The cluster-level `LimitRange` and `ResourceQuota` live in [`../resource-limits/`](../resource-limits/resource-limits.yaml), applied once at cluster setup rather than per-app.
## Relationship to the rest of the repo
Every always-on Docker Compose service in this repo (see [`docker-compose/`](../../docker-compose/)) has a self-contained folder with its own `docker-compose.yml`. The Kubernetes side mirrors that convention — one folder per app, fully self-contained — so the structure on both sides is parallel and easy to navigate.
## Adding a new app
See [ArgoCD](docs/argocd.md#adding-a-new-app) for the full walkthrough. In short:
1. Create `kubernetes/apps/<name>/` with manifests + `kustomization.yaml`.
2. Create the matching `kubernetes/argocd-apps/<name>/<name>-app.yaml` Application manifest pointing at that path.
3. `kubectl apply` the Application once — future changes flow through git.
[← Back to Home](../../docs/Home.md)

View File

@@ -0,0 +1,69 @@
# Argo CD Application Manifests
This folder holds the **Argo CD `Application` objects** — the manifests that tell Argo CD which Git path to watch and which cluster/namespace to deploy into. Each subfolder corresponds to one app, matching the name used under [`../apps/`](../apps/README.md):
```
kubernetes/argocd-apps/
├── README.md ← you are here
├── homepage/
│ └── homepage-app.yaml
└── <name>/
└── <name>-app.yaml
```
## What lives here
- **One `Application` YAML per app**, named `<name>-app.yaml`. This is the Argo CD control plane entry point — it sets the source repo, target revision, git `path`, destination server and namespace, and sync policy (automated with `prune` and `selfHeal`).
Example (from `homepage/homepage-app.yaml`):
```yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: homepage
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/rezayaghobi-dev/homelab
targetRevision: main
path: kubernetes/apps/homepage
destination:
server: https://kubernetes.default.svc
namespace: homepage
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
```
## What does *not* live here
- **Workload manifests** (`Deployment`, `Service`, `Ingress`, etc.). Those live in the sibling [`../apps/`](../apps/README.md) folder. This separation is deliberate: Argo CD should never watch its own `Application` definitions, otherwise a recursive sync loop becomes possible.
## Why this separation matters
Keeping "what tells Argo CD to run" (`<name>-app.yaml`) and "what actually runs" (the manifests in `../apps/<name>/`) in different folders means:
- **No accidental self-watch.** Argo CD watches `kubernetes/apps/`, not `kubernetes/argocd-apps/`. An Application manifest changing its own `path` would have no effect — it'd be out of the watched tree entirely.
- **Independent lifecycle.** The Application object is applied once via `kubectl`; everything after that — the Deployments, Services, Ingresses it ultimately creates — flows from git through Argo CD's reconciliation loop.
- **One job per file.** Adding an app means adding one file here (the Application) and one directory there (the manifests). Easy to audit, easy to delete.
## Applying a new Application
After creating `kubernetes/argocd-apps/<name>/<name>-app.yaml`, apply it once:
```bash
kubectl apply -f kubernetes/argocd-apps/<name>/<name>-app.yaml
```
Argo CD picks it up immediately, creates the app in its UI, and begins syncing. After this single `kubectl` call, **all** future changes flow through git — modifying the Application manifest itself is rarely needed once it's running.
## Relationship to the rest of the repo
The folder structure mirrors [`../apps/`](../apps/README.md) one-to-one: each app under `kubernetes/argocd-apps/` has a matching folder under `kubernetes/apps/`. This isn't strictly enforced by Argo CD, but keeping the two trees parallel makes it trivial to trace "which Application watches which manifests."
[← Back to Home](../../docs/Home.md)