Compare commits

...

44 Commits

Author SHA1 Message Date
uh-cli bot
77ea1dc571 gitops: update fuj-management
Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-12 18:01:46 +00:00
512b3cc2a6 gitops/fuj: split into per-app manifests, add fuj-management deployment
Rename generic deployment/service/httproute to per-app suffixes (_fujarna,
_fuj-management) and add fuj-management deployment, service, httproute, and
bot-credentials secret. Namespace name corrected to fuj.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 20:59:14 +02:00
3299373f3d gitops: rename namespace fujarna to fuj 2026-06-11 13:04:21 +02:00
80d0cc1168 misc: zot registry, k8s OIDC, server configs, sandbox experiments, and notes
- docker-30/zot: add Zot OCI registry with on-demand sync to docker.io,
  registry.k8s.io, ghcr.io, quay.io
- kubernetes-kvm-terraform: wire Kanidm OIDC via structured
  AuthenticationConfiguration; add reference apiserver manifest and
  join-node-02 helper
- servers: reorganize shadow/ under servers/, add saint vhost config and
  utility-101 VM definition, add shadow hrajfrisbee.cz vhost and
  storage-23 notes
- experiments: add notes and configs for e2b dev VM, kata + firecracker
  on kube, microsandbox, orb-stack k3s (terraform + cloud-init), rke2
- vms/docker: document tailscale + node-exporter setup
- blog: stub post on Gateway API
- chore: gitignore tmp/, smtp_password, and the two local-only
  credential caches; add per-project .claude/settings.json

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 18:12:38 +02:00
5ca27a832b gitops: upgrade Cilium to 1.19.1, add fujarna app, flux web UI, OIDC RBAC, and experiments
- Upgrade Cilium helm release from 1.18.5 to 1.19.1 with gatewayClass creation enabled
- Escalate gitea CI service account to cluster-admin, add OIDC cluster-admin binding
- Deploy fujarna app with full manifest set (deployment, service, PVC, httproutes, external secret)
- Add Flux web UI via flux-operator OCI repository and helm release
- Add experiments kustomization with test resources for gateway API and certificates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 22:23:12 +02:00
Jan Novak
96ba77a606 docker-30: gitea CI/CD integration with Vault and Kanidm, misc updates
vault:
- Add JWT auth backend bound to Gitea (jwks_url from gitea OIDC keys)
- Add gitea-ci-read policy scoped to secret/data/gitea/*
- Add JWT role gitea-ci (sub claim, bound to Gitea audience, 10m TTL)
- Add AppRole gitea-ci as alternative auth method for the same policy
- Add gitea-access-into-vault.md documenting the setup end-to-end
- Update terraform.tfstate (OpenTofu 1.11.5, new gitea-ci resources)

kanidm:
- Add run.sh with docker run command (pinned to v1.9.1)
- Add gitea-action-kubernetes-access.md documenting how to set up
  a Kanidm service account and OAuth2 client for Gitea CI k8s access
- readme: add upgrade procedure, recover-account command, and
  service account + API token setup for gitea-ci-token

maru-hleda-byt:
- Add --restart=always to docker run command

fuj-management:
- Add run.sh (new service config)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 23:09:52 +01:00
Jan Novak
dda6a9d032 vms: add monitoring stack and node-exporter for docker host
utility-101-shadow:
- Add full monitoring stack (Prometheus + Blackbox Exporter + Alertmanager)
  with Docker Compose and a systemd unit (monitoring.service)
- Prometheus scrapes: itself, blackbox-exporter, and node-exporter on
  the docker host (docker:9100); blackbox probes cover HTTPS endpoints
  with TLS cert monitoring
- Alertmanager routes warnings to Slack/Discord, critical alerts also
  to email (Gmail SMTP); inhibit rule suppresses SSLCertExpiringSoon
  when SSLCertExpired already fires
- Alert rules: 11 node-exporter alerts (host down, CPU, memory, disk
  fill/prediction, iowait, OOM kill, systemd failed units) + 3 blackbox
  alerts (probe failed, SSL expiring, SSL expired)
- readme: add services list and Docker Engine installation steps

docker host:
- Add node-exporter container running with host pid/network and
  read-only mounts of /proc, /sys, / for full host metrics visibility
- Enable --collector.systemd for systemd unit state metrics
- Add systemd unit (node-exporter.service) to manage the container

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 23:07:44 +01:00
Jan Novak
1b6015f732 gitops: fix kustomization: 00-rbac 2026-03-01 14:39:36 +01:00
Jan Novak
9877b093e8 gitops: add rbac kustomization + store some forgotten older changes in
repo
2026-03-01 14:33:56 +01:00
Jan Novak
0eab64c954 hosting: some config files for host: shadow, some named conf for
utility-101-shadow vm
2026-02-20 02:16:16 +01:00
Jan Novak
be362a5ab7 gitops/cilium: configure gateway and wildcard certificate it needs 2026-02-20 02:15:02 +01:00
Jan Novak
bb9f2ae3ce docker-30: several new and forgotten config files relevant to services
running in docker
2026-02-20 02:13:55 +01:00
Jan Novak
dc947165a4 gitops/ghost: add httproute resource aka gatewayApi instead of ingress 2026-02-20 02:13:09 +01:00
Jan Novak
1cd7625220 gitops/cert-manager: add dns challenger cluster issuer, add
deployment/service with socat proxy that works around my internet
provider's medling into dns traffic on port 53.
2026-02-20 02:11:50 +01:00
Jan Novak
409f8247e6 gitops/cert-manager: enable Gateway API support
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 01:43:04 +01:00
Jan Novak
8608696909 gitops/cilium: fix gateway.yaml indentation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 01:04:18 +01:00
Jan Novak
6454c893cb gitops/cilium: move gateway listeners from helm values to Gateway resource
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 01:02:14 +01:00
Jan Novak
b2daa822a6 gitops/cilium: configure gateway listeners and allow routes from all namespaces
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 00:51:37 +01:00
Jan Novak
8ae7b086a5 gitops/00-crds: add Gateway API v1.2.0 CRDs for Cilium gateway support
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:17:46 +01:00
Jan Novak
4b7ed6085b gitops/cilium: enable Gateway API and add HTTPRoute for ghost
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 11:55:49 +01:00
Jan Novak
0d97a796e9 gitops/velero: add manifests and runbook - kustomization is yet to be
created
2026-01-17 00:07:03 +01:00
Jan Novak
b9f99c2950 gitops/plane: fix issuer on ingress 2026-01-16 13:21:15 +01:00
Jan Novak
a20ae55b8f gitops/cilium: specify which interfaces it handles to not clash with
tailscaled
2026-01-15 01:24:49 +01:00
Jan Novak
36f447c39c gitops: assorted leftovers and fixes 2026-01-14 14:49:54 +01:00
Jan Novak
76e3ff9d03 kubernetes/terraform: several updates 2026-01-14 14:49:19 +01:00
Jan Novak
90a44bd59f vault: deployment manifest, some docs, backup script - expected to run
on docker host
2026-01-14 14:48:09 +01:00
Jan Novak
b5e1f4b737 gitops/external-secrets: change roleid 2026-01-13 10:28:43 +01:00
Jan Novak
099734fb6b gitops/ghost: prepare initial deployment with secrets in vault 2026-01-08 10:40:13 +01:00
Jan Novak
b081e947f5 gitops/plane: remove doc_upload_size_limit which seems to be causing
crashes
2026-01-07 22:42:26 +01:00
Jan Novak
d908e788af gitops/external-secrets: fix cloudsecretstore location where to look for
approle secret_id
2026-01-07 22:16:13 +01:00
Jan Novak
81f2e754ed gitops/external-secrets: set deployment replicas to 1 and add
cloudsecretstore
2026-01-07 22:05:31 +01:00
Jan Novak
a3a6ef79fe gitops/external-secrets do not use outdated api version of secretstore 2026-01-07 20:19:34 +01:00
Jan Novak
52089bc1b4 gitops: fix external secrets CRDs helm release 2026-01-07 20:02:57 +01:00
Jan Novak
a3c8cc9e47 gitops: move external-secrets helmrepo to 00-crds 2026-01-07 19:54:24 +01:00
Jan Novak
b6f775fd2b gitops/external-secrets: deploy CRDs first in another kustomization 2026-01-07 19:52:16 +01:00
Jan Novak
ed14d74738 gitops/external-secrets: add helmrelease + some coredns config for vault
resolving
2026-01-07 19:43:39 +01:00
Jan Novak
060a24437b gitops/plane: fix ingress 2026-01-06 10:57:11 +01:00
Jan Novak
c8011579c9 gitops: fix grafana ingress 2026-01-06 10:39:52 +01:00
Jan Novak
5bfc1f5fe5 gitops: add kube-prometheus 2026-01-06 09:57:26 +01:00
Jan Novak
7be7e0871c gitops: fix oauth kustomization 2026-01-05 22:21:12 +01:00
Jan Novak
437c94f2e1 gitops: add oauth-proxy + some changes in plane helmrelease 2026-01-05 22:19:31 +01:00
Jan Novak
edd945b709 gitops/plane: use app version v1.2.1 2026-01-05 11:48:57 +01:00
Jan Novak
1e9e981642 gitops/plane: use existing version of helm chart 2026-01-05 11:44:20 +01:00
Jan Novak
e4bc0424a7 gitops: add plane kustomization 2026-01-05 11:34:46 +01:00
169 changed files with 18957 additions and 25 deletions

10
.claude/settings.json Normal file
View File

@@ -0,0 +1,10 @@
{
"permissions": {
"allow": [
"Bash(for f:*)",
"Bash(do echo:*)",
"Read(//Users/jan.novak/srv/personal/home-kubernetes/**)",
"Bash(done)"
]
}
}

16
.gitignore vendored
View File

@@ -1,4 +1,14 @@
.terraform/ .DS_Store
./kubernetes-kvm-terraform/join-command.txt .terraform/
./kubernetes-kvm-terraform/kubeconfig .terraform.lock.hcl
kubernetes-kvm-terraform/join-command.txt
kubernetes-kvm-terraform/kubeconfig
tmp/
vms/utility-101-shadow/docker/monitoring/smtp_password
docker-30/zot/sync-credentials.json
kubernetes-kvm-terraform/gke_gcloud_auth_plugin_cache

View File

@@ -0,0 +1,7 @@
# How i did have some fun with gatewayApi
As nginx-ingress will be no longer maintained it looks like the time to upgrade is coming even for the lazy old schoolers who were very happy with it (even though annotations and custom snippets might not be the cleanest way, people experienced with nginx might have liked it for a reason). Although I tried to pretend that there is no gatewayApi and ingress is the thing for quite some time recently everything aligned for me to give it a go.
## Which implementation to choose?
There is a number of options and I'm only vaguely aware of them as I was not interested in it for a long time. But i have a tendency of trying to do something with Envoy (personal feeling that it is the right next thing here?) and I'm using the Cilium network plugin in my homelab kubernetes cluster. All this considered answer is obvious: use envoy gateway which is part of cilium.

View File

@@ -0,0 +1,10 @@
#!/bin/bash
docker rm -f fuj-management
# gitea registry login with kacerr / token
docker run -d --name fuj-management \
--restart=always \
-p 8081:5001 \
-v /srv/fuj-management/data:/app/data \
gitea.home.hrajfrisbee.cz/kacerr/fuj-management:latest

View File

@@ -57,6 +57,15 @@ services:
- GITEA__server__ROOT_URL=https://gitea.home.hrajfrisbee.cz - GITEA__server__ROOT_URL=https://gitea.home.hrajfrisbee.cz
- GITEA__security__SECRET_KEY=${GITEA_SECRET_KEY} - GITEA__security__SECRET_KEY=${GITEA_SECRET_KEY}
- GITEA__security__INTERNAL_TOKEN=${INTERNAL_TOKEN} - GITEA__security__INTERNAL_TOKEN=${INTERNAL_TOKEN}
- GITEA__mailer__ENABLED=true
- GITEA__mailer__PROTOCOL=smtps
- GITEA__mailer__SMTP_ADDR=smtp.gmail.com
- GITEA__mailer__SMTP_PORT=465
- GITEA__mailer__USER=kacerr.cz@gmail.com
- GITEA__mailer__PASSWD=${GMAIL_GITEA_APP_PASSWORD}
- GITEA__mailer__FROM=kacerr.cz+gitea@gmail.com
- GITEA__packages__ENABLED=true
#- GITEA__storage__STORAGE_TYPE=minio #- GITEA__storage__STORAGE_TYPE=minio
#- GITEA__storage__MINIO_ENDPOINT=minio:9000 #- GITEA__storage__MINIO_ENDPOINT=minio:9000
#- GITEA__storage__MINIO_ACCESS_KEY_ID=gitea #- GITEA__storage__MINIO_ACCESS_KEY_ID=gitea
@@ -83,10 +92,12 @@ services:
depends_on: depends_on:
- gitea - gitea
environment: environment:
GITEA_INSTANCE_URL: http://gitea:3000 GITEA_INSTANCE_URL: https://gitea.home.hrajfrisbee.cz/
GITEA_RUNNER_REGISTRATION_TOKEN: ${RUNNER_TOKEN} GITEA_RUNNER_REGISTRATION_TOKEN: ${RUNNER_TOKEN}
CONFIG_FILE: /config/config.yaml
volumes: volumes:
- ./runner-data:/data - ./runner-data:/data
- ./runner-config.yaml:/config/config.yaml:ro
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
networks: networks:
- gitea-network - gitea-network

View File

@@ -0,0 +1,84 @@
## 1. Create Kanidm service account + OAuth2 client
```bash
# Create a service account for CI
kanidm service-account create gitea_ci "Gitea CI Deploy" idm_admins --name idm_admin
# Create a group and add the service account
kanidm group create k8s_deployers
kanidm group add-members k8s_deployers gitea_ci
# Create the OAuth2 client (or reuse existing k8s one)
# If you already have a k8s OIDC client, just add scope maps:
kanidm system oauth2 update-scope-map k8s k8s_deployers openid groups
# Generate an API token for the service account
kanidm service-account api-token generate --name idm_admin gitea_ci "gitea-ci-token"
# ⚠️ Save the output token — this is the subject_token for exchange
```
## 2. RBAC in Kubernetes
```yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: gitea-ci-deploy
subjects:
- kind: User
name: "gitea_ci@idm.home.hrajfrisbee.cz" # matches preferred_username claim
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: edit # scope down as needed
apiGroup: rbac.authorization.k8s.io
```
## 3. Token exchange + kubeconfig setup (test in bash)
```bash
vault-login # prepared alias
#!/usr/bin/env bash
# set -euo pipefail
KANIDM_URL="https://idm.home.hrajfrisbee.cz"
OAUTH2_CLIENT_ID="k8s" # your k8s OIDC client name in Kanidm
API_TOKEN=$(vault kv get -format=json -mount="secret" "k8s_home/gitea/gitea-ci-token" |jq -r .data.data.token)
K8S_API="https://192.168.0.31:6443"
# Exchange the API token for an OIDC token via RFC 8693
RESPONSE=$(curl -sf -X POST "${KANIDM_URL}/oauth2/token" \
-d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
-d "client_id=${OAUTH2_CLIENT_ID}" \
-d "subject_token=${API_TOKEN}" \
-d "subject_token_type=urn:ietf:params:oauth:token-type:access_token" \
-d "audience=${OAUTH2_CLIENT_ID}" \
-d "scope=openid groups")
ID_TOKEN=$(echo "$RESPONSE" | jq -r '.id_token')
# Inspect claims (sanity check)
echo "$ID_TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq .
# Build kubeconfig
export KUBECONFIG=$(mktemp)
kubectl config set-cluster mycluster \
--server="${K8S_API}" \
--certificate-authority=/path/to/ca.crt
kubectl config set-credentials gitea-ci \
--token="${ID_TOKEN}"
kubectl config set-context gitea-ci \
--cluster=mycluster \
--user=gitea-ci
kubectl config use-context gitea-ci
# Test
kubectl auth whoami
kubectl get ns
```

View File

@@ -1,3 +1,27 @@
## Upgrade
```bash
docker exec -it kanidmd kanidmd domain upgrade-check
# make sure backup exists: /srv/docker/kanidm/data/kanidm/backups
# change container image in: /srv/docker/kanidm/run.sh
# kanidm data restore
docker stop kanidmd
docker run --rm -it \
-v kanidmd:/data \
-v kanidmd_backups:/backup \
kanidm/server:latest \
/sbin/kanidmd database restore -c /data/server.toml /backup/kanidm.backup.json
docker start kanidmd
```
## Recover passwords from kanidm instance
```bash
docker exec -i -t kanidmd kanidmd recover-account idm_admin
```
## add user to k8s group ## add user to k8s group
based on: https://blog.kammel.dev/post/k8s_home_lab_2025_06/ based on: https://blog.kammel.dev/post/k8s_home_lab_2025_06/
@@ -54,6 +78,50 @@ kanidm person get novakj | grep memberof
kanidm group get idm_people_self_name_write kanidm group get idm_people_self_name_write
``` ```
## configure oauth proxy
```bash
kanidm system oauth2 create oauth2-proxy "OAuth2 Proxy" https://oauth2-proxy.lab.home.hrajfrisbee.cz/oauth2/callback
kanidm system oauth2 set-landing-url oauth2-proxy https://oauth2-proxy.lab.home.hrajfrisbee.cz
kanidm system oauth2 enable-pkce oauth2-proxy
kanidm system oauth2 warning-insecure-client-disable-pkce oauth2-proxy # if proxy doesn't support PKCE
kanidm system oauth2 get oauth2-proxy # note the client secret
# update incorrect urls if needed
remove-redirect-url
kanidm system oauth2 add-redirect-url oauth2-proxy https://oauth2-proxy.lab.home.hrajfrisbee.cz/oauth2/callback
kanidm system oauth2 set-landing-url oauth2-proxy https://oauth2-proxy.lab.home.hrajfrisbee.cz
# output
✔ Multiple authentication tokens exist. Please select one · idm_admin@idm.home.hrajfrisbee.cz
---
class: account
class: key_object
class: key_object_internal
class: key_object_jwe_a128gcm
class: key_object_jwt_es256
class: memberof
class: oauth2_resource_server
class: oauth2_resource_server_basic
class: object
displayname: OAuth2 Proxy
key_internal_data: 69df0a387991455f7c9800f13b881803: valid jwe_a128gcm 0
key_internal_data: c5f61c48a9c0eb61ba993a36748826cc: valid jws_es256 0
name: oauth2-proxy
oauth2_allow_insecure_client_disable_pkce: true
oauth2_rs_basic_secret: hidden
oauth2_rs_origin_landing: https://oauth2-proxylab.home.hrajfrisbee.cz/
oauth2_strict_redirect_uri: true
spn: oauth2-proxy@idm.home.hrajfrisbee.cz
uuid: d0dcbad5-90e4-4e36-a51b-653624069009
secret: 7KJbUe5x35NVCT1VbzZfhYBU19cz9Xe9Z1fvw4WazrkHX2c8
kanidm system oauth2 update-scope-map oauth2-proxy k8s_users openid profile email
```
```bash ```bash
@@ -71,4 +139,31 @@ docker run --rm -i -t -v --restart=always \
docker.io/kanidm/server:latest \ docker.io/kanidm/server:latest \
kanidmd cert-generate kanidmd cert-generate
```
## Service account for gitea runner
```bash
# create service account
#kanidm service-account create \
# gitea_ci \ # account name
# "Gitea CI Deploy" \ # display name
# idm_admins \ # entry-managed-by (delegation group)
# --name idm_admin # authenticate as this user
kanidm service-account create gitea_ci "Gitea CI Deploy" idm_admins --name idm_admin
# Create a group and add the service account
kanidm group create k8s_deployers
kanidm group add-members k8s_deployers gitea_ci
# Create the OAuth2 client (or reuse existing k8s one)
# If you already have a k8s OIDC client, just add scope maps:
kanidm system oauth2 update-scope-map k8s k8s_deployers openid groups
# Generate an API token for the service account
kanidm service-account api-token generate --name idm_admin gitea_ci "gitea-ci-token"
# ⚠️ Save the output token — this is the subject_token for exchange
``` ```

9
docker-30/kanidm/run.sh Normal file
View File

@@ -0,0 +1,9 @@
docker rm -f kanidmd
docker run -d --name=kanidmd --restart=always \
-p '8443:8443' \
-p '3636:3636' \
--volume /srv/docker/kanidm/data:/data \
docker.io/kanidm/server:1.9.1
# previous version: 1.8.5

View File

@@ -0,0 +1,46 @@
# nginx.conf
error_log /dev/stderr;
http {
server {
listen 9080;
location / {
proxy_pass http://192.168.0.35:80;
proxy_set_header Host $host;
}
}
log_format detailed '$remote_addr - [$time_local] '
'"$request_method $host$request_uri" '
'$status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
access_log /dev/stdout detailed;
}
stream {
# Stream doesn't log by default, enable explicitly:
log_format stream_log '$remote_addr [$time_local] '
'$protocol $ssl_preread_server_name '
'$status $bytes_sent $bytes_received $session_time';
access_log /dev/stdout stream_log;
# Nginx ingress in kubernetes
server {
listen 9443;
proxy_pass 192.168.0.35:443;
}
# Gateway provided by cilium/envoy
server {
listen 9444;
proxy_pass 192.168.0.36:443;
}
}
events {}

View File

@@ -0,0 +1,9 @@
docker rm -f lab-proxy || /usr/bin/true
docker run -d --name lab-proxy \
--restart unless-stopped \
-v /srv/docker/lab-proxy/nginx.conf:/etc/nginx/nginx.conf:ro \
-p 9443:9443 \
-p 9444:9444 \
-p 9080:9080 \
nginx:alpine

View File

@@ -0,0 +1,10 @@
#!/bin/bash
docker rm -f maru-hleda-byt
# gitea registry login with kacerr / token
docker run -d --name maru-hleda-byt \
--restart=always \
-p 8080:8080 \
-v /srv/maru-hleda-byt/data:/app/data \
gitea.home.hrajfrisbee.cz/littlemeat/maru-hleda-byt:0.01

View File

@@ -0,0 +1,22 @@
server {
listen 443 ssl http2;
server_name gitea.home.hrajfrisbee.cz;
ssl_certificate /etc/letsencrypt/live/gitea.home.hrajfrisbee.cz/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/gitea.home.hrajfrisbee.cz/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
location / {
proxy_pass http://192.168.0.30:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Gitea Git over HTTP
client_max_body_size 512m;
}

View File

@@ -0,0 +1,35 @@
server {
listen 443 ssl http2;
server_name jellyfin.home.hrajfrisbee.cz;
ssl_certificate /etc/letsencrypt/live/gitea.home.hrajfrisbee.cz/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/gitea.home.hrajfrisbee.cz/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# Security headers for media streaming
add_header X-Frame-Options "SAMEORIGIN";
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options "nosniff";
# Increase body size for high-res movie posters
client_max_body_size 20M;
location / {
# Proxy to your Synology or VM IP and Jellyfin port (default 8096)
proxy_pass http://192.168.0.2:8096;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Protocol $scheme;
proxy_set_header X-Forwarded-Host $http_host;
# Disable buffering for smoother streaming
proxy_buffering off;
}
}

35
docker-30/vault/backup.md Normal file
View File

@@ -0,0 +1,35 @@
## vault-cli install
```bash
VAULT_VERSION="1.21.2"
wget https://releases.hashicorp.com/vault/${VAULT_VERSION}/vault_${VAULT_VERSION}_linux_amd64.zip
unzip vault_${VAULT_VERSION}_linux_amd64.zip
sudo mv vault /usr/local/bin/
```
## minio-cli
```bash
wget https://dl.min.io/client/mc/release/linux-amd64/mc -O /tmp/minio-cli
chmod +x /tmp/minio-cli
sudo mv /tmp/minio-cli /usr/local/bin/minio-cli
minio-cli alias set synology http://192.168.0.2:9000 k8s ----proper secret here----
```
## backup token
```bash
mkdir -p /etc/vault.d/
vault policy write backup - <<EOF
path "sys/storage/raft/snapshot" {
capabilities = ["read"]
}
EOF
vault token create -policy=backup -period=8760h -orphan > /etc/vault.d/backup-token
chmod 600 /etc/vault.d/backup-token
```

View File

@@ -0,0 +1,20 @@
{
"ui": true,
"listener": {
"tcp": {
"address": "0.0.0.0:8200",
"tls_disable": "1",
"tls_cert_file": "/vault/certs/fullchain.pem",
"tls_key_file": "/vault/certs/privkey.pem"
}
},
"backend": {
"file": {
"path": "/vault/data/file"
}
},
"default_lease_ttl": "168h",
"max_lease_ttl": "0h",
"api_addr": "https://vault.hrajfrisbee.cz"
// "api_addr": "http://0.0.0.0:8200"
}

View File

@@ -0,0 +1,20 @@
services:
vault:
image: hashicorp/vault:1.21.1
container_name: vault
restart: unless-stopped
cap_add:
- IPC_LOCK
ports:
- 8200:8200
environment:
- VAULT_ADDR=http://0.0.0.0:8200
- VAULT_API_ADDR=http://0.0.0.0:8200
- VAULT_ADDRESS=http://0.0.0.0:8200
volumes:
- ./data:/vault/data
- ./config:/vault/config
- ./logs:/vault/logs
- ./certs:/vault/certs
entrypoint: vault
command: server -config=/vault/config/vault.json -log-level=debug

View File

@@ -0,0 +1,10 @@
## 1. Enable & configure JWT auth in Vault
```bash
vault auth enable jwt
vault write auth/jwt/config \
bound_issuer="https://gitea.home.hrajfrisbee.cz" \
jwks_url="https://gitea.home.hrajfrisbee.cz/login/oauth/keys"
```

38
docker-30/vault/readme.md Normal file
View File

@@ -0,0 +1,38 @@
## deployment notes
There was a problem with "production" deployment of Vault through docker container, because default `docker-entrypoint.sh` adds argument saying where dev instance is supposed to listen and then vault crashes because it tries to listen on same port twice.
Solution: override default entrypoint
```bash
# vault helpers
alias set-vault="export VAULT_ADDR=https://docker-30:8200"
alias set-vault-ignore-tls="export VAULT_ADDR=https://docker-30:8200; export VAULT_SKIP_VERIFY=true"
export VAULT_ADDR="https://vault.hrajfrisbee.cz"
export VAULT_SKIP_VERIFY=true
```
## backup
Simple file copy initiated by cron, backend storage is minio (s3) running on synology
```bash
echo '30 2 * * * root /root/bin/vault-backup.sh >> /var/log/vault-backup.log 2>&1' > /etc/cron.d/vault-backup
```
```bash
# output role info
tofu output -raw role_id
tofu output -raw secret_id
```
## vault initialization
```bash
vault operator init -key-shares=1 -key-threshold=1
```

View File

@@ -0,0 +1,115 @@
resource "vault_mount" "kv" {
path = "secret"
type = "kv-v2"
description = "KV v2 secrets engine"
}
resource "vault_jwt_auth_backend" "gitea" {
path = "jwt"
type = "jwt"
bound_issuer = "https://gitea.home.hrajfrisbee.cz"
jwks_url = "https://gitea.home.hrajfrisbee.cz/login/oauth/keys"
}
resource "vault_policy" "eso_read" {
name = "external-secrets-read"
policy = <<-EOT
path "${vault_mount.kv.path}/data/*" {
capabilities = ["read"]
}
path "${vault_mount.kv.path}/metadata/*" {
capabilities = ["read", "list"]
}
EOT
}
# for now i allow my gitea to read everything in /v1/secret/data/gitea
resource "vault_policy" "gitea_ci_read" {
name = "gitea-ci-read"
policy = <<-EOT
path "${vault_mount.kv.path}/data/gitea/*" {
capabilities = ["read"]
}
path "${vault_mount.kv.path}/metadata/gitea/*" {
capabilities = ["read", "list"]
}
EOT
}
resource "vault_jwt_auth_backend_role" "gitea_ci" {
backend = vault_jwt_auth_backend.gitea.path
role_name = "gitea-ci"
role_type = "jwt"
token_policies = [vault_policy.gitea_ci_read.name]
user_claim = "sub"
bound_audiences = ["https://gitea.home.hrajfrisbee.cz"]
# allow any valid jwt token when commented out
# bound_claims = {
# repository = "myorg/repo1,myorg/repo3"
# }
token_ttl = 600
token_max_ttl = 1200
}
resource "vault_auth_backend" "approle" {
type = "approle"
}
resource "vault_approle_auth_backend_role" "eso" {
backend = vault_auth_backend.approle.path
role_name = "external-secrets"
token_policies = [vault_policy.eso_read.name]
token_ttl = 3600
token_max_ttl = 14400
}
data "vault_approle_auth_backend_role_id" "eso" {
backend = vault_auth_backend.approle.path
role_name = vault_approle_auth_backend_role.eso.role_name
}
resource "vault_approle_auth_backend_role_secret_id" "eso" {
backend = vault_auth_backend.approle.path
role_name = vault_approle_auth_backend_role.eso.role_name
}
output "role_id" {
value = data.vault_approle_auth_backend_role_id.eso.role_id
sensitive = true
}
output "secret_id" {
value = vault_approle_auth_backend_role_secret_id.eso.secret_id
sensitive = true
}
resource "vault_approle_auth_backend_role" "gitea_ci" {
backend = vault_auth_backend.approle.path
role_name = "gitea-ci"
token_policies = [vault_policy.gitea_ci_read.name]
token_ttl = 600
token_max_ttl = 1200
}
data "vault_approle_auth_backend_role_id" "gitea_ci" {
backend = vault_auth_backend.approle.path
role_name = vault_approle_auth_backend_role.gitea_ci.role_name
}
resource "vault_approle_auth_backend_role_secret_id" "gitea_ci" {
backend = vault_auth_backend.approle.path
role_name = vault_approle_auth_backend_role.gitea_ci.role_name
}
output "gitea_ci_role_id" {
value = data.vault_approle_auth_backend_role_id.gitea_ci.role_id
sensitive = true
}
output "gitea_ci_secret_id" {
value = vault_approle_auth_backend_role_secret_id.gitea_ci.secret_id
sensitive = true
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,12 @@
terraform {
required_providers {
vault = {
source = "hashicorp/vault"
version = "~> 4.0"
}
}
}
provider "vault" {
# Uses VAULT_ADDR and VAULT_TOKEN from env
}

View File

@@ -0,0 +1,38 @@
#!/bin/bash
set -euo pipefail
# set -x # Enable debug output
# --- Configuration ---
VAULT_DATA_DIR="${VAULT_DATA_DIR:-/srv/docker/vault/data/}"
S3_BUCKET="${S3_BUCKET:-vault-backup}"
MC_ALIAS="${MC_ALIAS:-synology}" # Pre-configured mc alias
RETENTION_DAYS="${RETENTION_DAYS:-60}"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
BACKUP_FILE="/tmp/vault-backup-${TIMESTAMP}.tar.gz"
log() { echo "[$(date -Iseconds)] $*"; }
cleanup() {
rm -f "${BACKUP_FILE}"
}
trap cleanup EXIT
# --- Create backup ---
log "Backing up ${VAULT_DATA_DIR}..."
tar -czf "${BACKUP_FILE}" -C "$(dirname "${VAULT_DATA_DIR}")" "$(basename "${VAULT_DATA_DIR}")"
BACKUP_SIZE=$(stat -c%s "${BACKUP_FILE}")
log "Backup size: ${BACKUP_SIZE} bytes"
# --- Upload to MinIO ---
log "Uploading to ${MC_ALIAS}/${S3_BUCKET}..."
set -x
minio-cli cp --quiet "${BACKUP_FILE}" "${MC_ALIAS}/${S3_BUCKET}/vault-backup-${TIMESTAMP}.tar.gz"
# --- Prune old backups ---
log "Pruning backups older than ${RETENTION_DAYS} days..."
minio-cli rm --quiet --recursive --force --older-than "${RETENTION_DAYS}d" "${MC_ALIAS}/${S3_BUCKET}/"
log "Backup complete: vault-backup-${TIMESTAMP}.tar.gz"

52
docker-30/zot/config.yaml Normal file
View File

@@ -0,0 +1,52 @@
distSpecVersion: "1.1.0"
storage:
rootDirectory: /var/lib/zot
gc: true
gcDelay: "24h"
dedupe: true
http:
address: 0.0.0.0
port: 5000
compat:
- docker2s2
log:
# level: info
level: debug
extensions:
ui:
enable: true
search:
enable: true
sync:
enable: true
credentialsFile: "/etc/zot/sync-credentials.json"
registries:
- urls: ["https://registry-1.docker.io"]
onDemand: true
tlsVerify: true
content:
- prefix: "library/**"
destination: "/docker.io/library"
- prefix: "democraticcsi/**"
destination: "/democraticcsi"
- prefix: "**"
destination: "/docker.io"
- urls: ["https://registry.k8s.io"]
onDemand: true
tlsVerify: true
content:
- prefix: "**"
destination: "/registry.k8s.io"
- urls: ["https://ghcr.io"]
onDemand: true
tlsVerify: true
content:
- prefix: "**"
destination: "/ghcr.io"
- urls: ["https://quay.io"]
onDemand: true
tlsVerify: true
content:
- prefix: "**"
destination: "/quay.io"

View File

@@ -0,0 +1,12 @@
services:
zot:
image: ghcr.io/project-zot/zot-linux-amd64:latest
container_name: zot
restart: unless-stopped
command: serve /etc/zot/config.yaml
ports:
- "5000:5000"
volumes:
- ./config.yaml:/etc/zot/config.yaml:ro
- ./sync-credentials.json:/etc/zot/sync-credentials.json:ro
- /srv/container-registry-data:/var/lib/zot

View File

@@ -0,0 +1,391 @@
# E2B Dev VM Setup on KVM Homelab
## Context
The user wants to run the E2B infrastructure dev stack on a KVM virtual machine in their homelab. E2B uses Firecracker microVMs (which need `/dev/kvm`), so the guest VM needs **nested virtualization** (KVM-in-KVM). This guide covers VM creation, OS configuration, toolchain installation, and running the full dev stack.
> **Note from upstream**: [DEV-LOCAL.md](DEV-LOCAL.md) says "Linux is required. This is a work in progress. Not everything will function as expected."
---
## Phase 1: Create the KVM VM on the Hypervisor Host
### 1.1 Enable nested virtualization on the host
**Intel:**
```bash
cat /sys/module/kvm_intel/parameters/nested # check
sudo modprobe -r kvm_intel && sudo modprobe kvm_intel nested=1
echo "options kvm_intel nested=1" | sudo tee /etc/modprobe.d/kvm-nested.conf
```
**AMD:**
```bash
cat /sys/module/kvm_amd/parameters/nested
sudo modprobe -r kvm_amd && sudo modprobe kvm_amd nested=1
echo "options kvm_amd nested=1" | sudo tee /etc/modprobe.d/kvm-nested.conf
# get details about loaded kernel module
systool -v -m kvm_amd
```
### 1.2 Create the VM
```bash
virt-install \
--name e2b-dev \
--ram 16384 \
--vcpus 8 \
--cpu host-passthrough \
--os-variant ubuntu24.04 \
--disk path=/var/lib/libvirt/images/e2b-dev.qcow2,size=100,format=qcow2,bus=virtio \
--network bridge=virbr0,model=virtio \
--graphics none \
--console pty,target_type=serial \
--cdrom /path/to/ubuntu-24.04-live-server-amd64.iso \
--extra-args 'console=ttyS0,115200n8'
```
**Why these specs:**
| Resource | Value | Rationale |
|----------|-------|-----------|
| RAM | 16 GB (24-32 better) | 4 GB for huge pages (2048 x 2MB), ~4 GB for 10 Docker containers, rest for Go services + Firecracker VMs |
| vCPUs | 8 | Firecracker VMs consume vCPUs; Go services are concurrent |
| Disk | 100 GB | Docker images, FC binaries, kernels, rootfs, Go cache, build artifacts |
| CPU | `host-passthrough` | **Mandatory** -- exposes VMX/SVM to guest so `/dev/kvm` works inside the VM |
### 1.3 Alternative: libvirt XML
If you manage VMs declaratively, the critical part is:
```xml
<cpu mode='host-passthrough' check='none' migratable='off'/>
```
### 1.4 Verify nested KVM works (after OS install)
```bash
ls -la /dev/kvm # must exist
lsmod | grep kvm # kvm + kvm_intel/kvm_amd
grep -cE '(vmx|svm)' /proc/cpuinfo # must be > 0
```
If `/dev/kvm` is missing, go back to 1.1.
---
## Phase 2: OS Configuration (inside the guest VM)
**Recommended OS:** Ubuntu 24.04 LTS Server (matches CI, well-tested with Firecracker)
### 2.1 Base packages
```bash
sudo apt update && sudo apt upgrade -y
sudo apt install -y \
build-essential git curl wget unzip jq make gcc pkg-config \
iptables iproute2 net-tools ca-certificates gnupg \
lsb-release software-properties-common gettext-base
```
### 2.2 Kernel modules
```bash
# Load now
sudo modprobe nbd nbds_max=64
sudo modprobe kvm
sudo modprobe kvm_intel # or kvm_amd
sudo modprobe tun
sudo modprobe veth
sudo modprobe nf_tables
sudo modprobe nft_nat
# Persist across reboots
cat <<'EOF' | sudo tee /etc/modules-load.d/e2b.conf
nbd
kvm
kvm_intel
tun
veth
nf_tables
nft_nat
EOF
echo "options nbd nbds_max=64" | sudo tee /etc/modprobe.d/nbd.conf
```
### 2.3 Sysctl
```bash
cat <<'EOF' | sudo tee /etc/sysctl.d/99-e2b.conf
vm.nr_hugepages=2048
vm.max_map_count=1048576
vm.swappiness=10
vm.vfs_cache_pressure=50
net.ipv4.ip_forward=1
net.core.somaxconn=65535
net.core.netdev_max_backlog=65535
net.ipv4.tcp_max_syn_backlog=65535
EOF
sudo sysctl --system
```
### 2.4 Udev rules (suppress NBD inotify noise)
```bash
cat <<'EOF' | sudo tee /etc/udev/rules.d/99-e2b-nbd.rules
KERNEL=="nbd*", OPTIONS+="nowatch"
EOF
sudo udevadm control --reload-rules && sudo udevadm trigger
```
### 2.5 File descriptor limits
```bash
cat <<'EOF' | sudo tee /etc/security/limits.d/e2b.conf
* soft nofile 1048576
* hard nofile 1048576
root soft nofile 1048576
root hard nofile 1048576
EOF
```
---
## Phase 3: Install Toolchain
### 3.1 Docker
```bash
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
# Log out and back in
docker --version && docker compose version
```
### 3.2 mise (manages all tools from `.tool-versions`)
```bash
curl https://mise.run | sh
echo 'eval "$(~/.local/bin/mise activate bash)"' >> ~/.bashrc
source ~/.bashrc
```
### 3.3 Install project tools
The repo's [.tool-versions](.tool-versions) pins:
| Tool | Version |
| --- | --- |
| golang | 1.25.4 |
| buf | 1.28.1 |
| bun | 1.3.2 |
| protoc | 29.3 |
| protoc-gen-go | 1.28.1 |
| protoc-gen-go-grpc | 1.6.1 |
| protoc-gen-connect-go | 1.18.1 |
| golangci-lint | 2.8.0 |
| terraform | 1.5.7 |
| packer | 1.13.1 |
| python | 3.13.11 |
| gcloud | 534.0.0 |
```bash
cd ~/e2b-infra # after cloning
mise install # installs everything from .tool-versions
```
> **Note:** `gcloud` is only needed for `gsutil` to download prebuilt artifacts. If you don't have GCP credentials, you can download them via HTTPS instead (see Phase 5).
---
## Phase 4: Clone Repository
```bash
git clone https://github.com/e2b-dev/infra.git ~/e2b-infra
cd ~/e2b-infra
go work sync
```
---
## Phase 5: Download Prebuilt Artifacts
Firecracker binaries and Linux kernels must be downloaded from the public GCS bucket.
**With gsutil (if gcloud is configured):**
```bash
make download-public-kernels
make download-public-firecrackers
```
**Without GCP credentials (HTTPS alternative):**
The bucket `e2b-prod-public-builds` is publicly accessible. You'll need to browse/list it to find available versions, then download manually:
```bash
# Install gsutil standalone (no GCP project needed for public buckets)
# OR use curl/wget against:
# https://storage.googleapis.com/e2b-prod-public-builds/
# to discover and download kernel and firecracker builds
```
The simplest path is to install just the `gcloud` CLI (via mise) and run the make targets -- no GCP project or auth is needed for public bucket reads.
---
## Phase 6: Prepare Local Environment
### 6.1 Start infrastructure containers
```bash
make local-infra
# Starts: PostgreSQL 17.4, Redis 7.4.2, ClickHouse 25.4.5.24,
# Grafana 12.0.0, Loki 3.4.1, Tempo 2.8.2, Mimir 2.17.1,
# OTEL Collector 0.146.0, Vector, Memcached 1.6.38
```
Wait for all containers to be healthy:
```bash
docker compose -f packages/local-dev/docker-compose.yaml ps
```
### 6.2 Initialize databases
```bash
make -C packages/db migrate-local # PostgreSQL
make -C packages/clickhouse migrate-local # ClickHouse
```
### 6.3 Build envd (in-VM daemon)
```bash
make -C packages/envd build
```
### 6.4 Seed database with dev credentials
```bash
make -C packages/local-dev seed-database
```
Creates test user, team, API key, and access token for local development.
---
## Phase 7: Run the Dev Stack
Each service runs in the foreground. Use **tmux**, **screen**, or separate SSH sessions.
| Terminal | Command | Listens on |
| --- | --- | --- |
| 1 | `make local-infra` | (Docker containers) |
| 2 | `make -C packages/api run-local` | `:3000` |
| 3 | `make -C packages/orchestrator build-debug && sudo make -C packages/orchestrator run-local` | `:5008` |
| 4 | `make -C packages/client-proxy run-local` | `:3002` |
> The orchestrator **requires sudo** -- Firecracker needs root for `/dev/kvm`, network namespaces, veth pairs, nftables rules, and NBD devices.
---
## Phase 8: Verify
### 8.1 Health checks
```bash
curl -s http://localhost:3000/health # API
curl -s -o /dev/null -w "%{http_code}" http://localhost:53000 # Grafana (expect 302)
curl -s 'http://localhost:8123/?query=SELECT%201' # ClickHouse
redis-cli -h localhost -p 6379 ping # Redis
```
### 8.2 Build the base template
```bash
make -C packages/shared/scripts local-build-base-template
```
### 8.3 Test with E2B client SDK
```bash
export E2B_API_KEY=e2b_53ae1fed82754c17ad8077fbc8bcdd90
export E2B_ACCESS_TOKEN=sk_e2b_89215020937a4c989cde33d7bc647715
export E2B_API_URL=http://localhost:3000
export E2B_SANDBOX_URL=http://localhost:3002
# Use E2B SDK/CLI to create a sandbox
```
---
## Phase 9: Access from Host (Optional)
### SSH port forwarding
```bash
ssh -N \
-L 3000:localhost:3000 \
-L 3002:localhost:3002 \
-L 53000:localhost:53000 \
-L 5432:localhost:5432 \
-L 8123:localhost:8123 \
user@<vm-ip>
```
### Or use bridged networking
If the VM has a routable IP on your LAN, services are directly accessible (Docker binds to `0.0.0.0`, Go services listen on all interfaces).
---
## Service Endpoints Reference
| Service | URL |
| --- | --- |
| E2B API | `http://localhost:3000` |
| E2B Client Proxy | `http://localhost:3002` |
| E2B Orchestrator | `http://localhost:5008` |
| Grafana | `http://localhost:53000` |
| PostgreSQL | `postgres://postgres:postgres@localhost:5432` |
| ClickHouse (HTTP) | `http://localhost:8123` |
| ClickHouse (native) | `localhost:9000` |
| Redis | `localhost:6379` |
| OTEL Collector (gRPC) | `localhost:4317` |
| OTEL Collector (HTTP) | `localhost:4318` |
| Loki | `http://localhost:3100` |
| Vector | `localhost:30006` |
---
## Troubleshooting
| Problem | Fix |
| --- | --- |
| `/dev/kvm` missing in guest | Enable nested virt on host (Phase 1.1), use `host-passthrough` CPU |
| `modprobe nbd` fails | Check kernel has NBD support: `modinfo nbd` |
| Orchestrator permission errors | Must run with `sudo` |
| Huge pages < 2048 | Not enough contiguous memory; increase VM RAM or set earlier in boot |
| Docker containers won't start | Check `systemctl status docker`, port conflicts with `ss -tlnp` |
| `gsutil` not found | Install via `mise install gcloud` or download artifacts via HTTPS |
---
## After VM Reboot Checklist
```bash
# 1. Verify kernel modules and sysctl (should be persistent)
lsmod | grep nbd
cat /proc/sys/vm/nr_hugepages # expect 2048
# 2. Start infra
cd ~/e2b-infra && make local-infra
# 3. Start services (separate terminals)
make -C packages/api run-local
sudo make -C packages/orchestrator run-local
make -C packages/client-proxy run-local
```

View File

@@ -0,0 +1,95 @@
## dev-vm on beelink
```bash
virt-install \
--name e2b-dev \
--ram 16384 \
--vcpus 8 \
--cpu host-passthrough \
--os-variant ubuntu24.04 \
--disk path=/srv/vms/e2b-dev.qcow2,size=100,format=qcow2,bus=virtio \
--network bridge=br0,model=virtio \
--graphics none \
--console pty,target_type=serial \
--location /srv/vms/isos/ubuntu-24.04.3-live-server-amd64.iso,kernel=casper/vmlinuz,initrd=casper/initrd \
--extra-args 'console=ttyS0,115200n8'
# base packages
sudo apt update && sudo apt upgrade -y
sudo apt install -y \
build-essential git curl wget unzip jq make gcc pkg-config \
iptables iproute2 net-tools ca-certificates gnupg \
lsb-release software-properties-common gettext-base
# kernel modules
# Load now
sudo modprobe nbd nbds_max=64
sudo modprobe kvm
sudo modprobe kvm_amd # or kvm_amd
sudo modprobe tun
sudo modprobe veth
sudo modprobe nf_tables
sudo modprobe nft_nat
# Persist across reboots
cat <<'EOF' | sudo tee /etc/modules-load.d/e2b.conf
nbd
kvm
kvm_amd
tun
veth
nf_tables
nft_nat
EOF
echo "options nbd nbds_max=64" | sudo tee /etc/modprobe.d/nbd.conf
# sysctl
cat <<'EOF' | sudo tee /etc/sysctl.d/99-e2b.conf
vm.nr_hugepages=2048
vm.max_map_count=1048576
vm.swappiness=10
vm.vfs_cache_pressure=50
net.ipv4.ip_forward=1
net.core.somaxconn=65535
net.core.netdev_max_backlog=65535
net.ipv4.tcp_max_syn_backlog=65535
EOF
sudo sysctl --system
# udev rules
cat <<'EOF' | sudo tee /etc/udev/rules.d/99-e2b-nbd.rules
KERNEL=="nbd*", OPTIONS+="nowatch"
EOF
sudo udevadm control --reload-rules && sudo udevadm trigger
# file descriptor limits
cat <<'EOF' | sudo tee /etc/security/limits.d/e2b.conf
* soft nofile 1048576
* hard nofile 1048576
root soft nofile 1048576
root hard nofile 1048576
EOF
```
## install toolchain
```bash
# docker
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
# Log out and back in
docker --version && docker compose version
# mise
curl https://mise.run | sh
echo 'eval "$(~/.local/bin/mise activate bash)"' >> ~/.bashrc
source ~/.bashrc
```
```

View File

@@ -0,0 +1,280 @@
# E2B Sandbox Usage Guide
A practical guide for creating, observing, and using E2B sandboxes via the API.
## Prerequisites
### Local Dev Setup
Seed the database to create a test team and API key:
```bash
make -C packages/local-dev seed-database
```
### Environment Variables
```bash
E2B_API_KEY=e2b_53ae1fed82754c17ad8077fbc8bcdd90
E2B_ACCESS_TOKEN=sk_e2b_89215020937a4c989cde33d7bc647715
E2B_API_URL=http://localhost:3000
E2B_SANDBOX_URL=http://localhost:3002
```
All examples below use `$E2B_API_URL` and `$E2B_API_KEY` — export them in your shell for convenience:
```bash
export E2B_API_URL=http://localhost:3000
export E2B_API_KEY=e2b_53ae1fed82754c17ad8077fbc8bcdd90
```
---
## 1. Create a Sandbox
```bash
curl -X POST $E2B_API_URL/sandboxes \
-H "X-API-Key: $E2B_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"templateID": "base",
"timeout": 300
}'
```
**Response:**
```json
{
"sandboxID": "i1234567890abcdef",
"templateID": "base",
"envdVersion": "0.5.8",
"domain": "i1234567890abcdef.your-domain"
}
```
Save the `sandboxID` and `domain` from the response for subsequent commands.
### Optional Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `secure` | bool | Returns an `envdAccessToken` for authenticated envd access |
| `envVars` | object | Inject environment variables into the VM |
| `metadata` | object | Tag the sandbox for filtering (e.g. `{"purpose": "test"}`) |
| `autoPause` | bool | Pause instead of kill on timeout |
| `allow_internet_access` | bool | Allow internet egress from the VM |
| `network.allowPublicTraffic` | bool | Allow inbound public traffic |
| `network.allowOut` | array | Allowed egress destinations (IPs, CIDRs, domains) |
| `network.denyOut` | array | Denied egress destinations (IPs/CIDRs only) |
| `volumeMounts` | array | Mount persistent volumes (`{"name": "vol", "path": "/data"}`) |
**Example with options:**
```bash
curl -X POST $E2B_API_URL/sandboxes \
-H "X-API-Key: $E2B_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"templateID": "base",
"timeout": 300,
"secure": true,
"envVars": {"MY_VAR": "hello"},
"metadata": {"purpose": "demo"}
}'
```
---
## 2. Observe Sandbox State
### Get details of a specific sandbox
```bash
curl $E2B_API_URL/sandboxes/{sandboxID} \
-H "X-API-Key: $E2B_API_KEY"
```
Returns state (`running`/`paused`), CPU/memory/disk config, network settings, and TTL (`endAt`).
### List all sandboxes
```bash
# Running only
curl "$E2B_API_URL/v2/sandboxes?state=running" \
-H "X-API-Key: $E2B_API_KEY"
# Running and paused
curl "$E2B_API_URL/v2/sandboxes?state=running&state=paused" \
-H "X-API-Key: $E2B_API_KEY"
# Filter by metadata
curl "$E2B_API_URL/v2/sandboxes?metadata=purpose%3Ddemo" \
-H "X-API-Key: $E2B_API_KEY"
```
### Get resource metrics (CPU, memory, disk)
```bash
curl "$E2B_API_URL/sandboxes/{sandboxID}/metrics?start=$(date -v-5M +%s)&end=$(date +%s)" \
-H "X-API-Key: $E2B_API_KEY"
```
**Response:**
```json
[
{
"timestampUnix": 1234567890,
"cpuCount": 2,
"cpuUsedPct": 25.5,
"memUsed": 268435456,
"memTotal": 536870912,
"diskUsed": 1073741824,
"diskTotal": 5368709120
}
]
```
### Get sandbox logs
```bash
curl "$E2B_API_URL/v2/sandboxes/{sandboxID}/logs?limit=100&direction=backward" \
-H "X-API-Key: $E2B_API_KEY"
```
Optional query parameters: `cursor` (ms timestamp), `level` (min log level), `search` (substring match).
---
## 3. Use the Sandbox
The in-VM daemon (**envd**) runs on port 49983 inside each sandbox, exposed via the sandbox's `domain`.
### Upload a file
```bash
curl -X POST https://{domain}/files \
-H "Content-Type: multipart/form-data" \
-F "file=@script.py" \
-F "path=/home/user/script.py"
```
### Using the E2B Python SDK
Point the SDK at your local API:
```python
from e2b import Sandbox
sbx = Sandbox("base", api_url="http://localhost:3000")
# Run a command
result = sbx.commands.run("echo 'Hello from Firecracker VM!'")
print(result.stdout)
# Write and execute a file
sbx.files.write("/home/user/hello.py", "print('Hello world')")
result = sbx.commands.run("python3 /home/user/hello.py")
print(result.stdout)
# List files
files = sbx.files.list("/home/user")
for f in files:
print(f.name)
sbx.kill()
```
### Envd Connect RPC API
The envd daemon exposes Connect RPC services for programmatic access:
**Process Service:**
- `Start(ProcessConfig)` — start a new process
- `List()` — list running processes
- `Connect(ProcessSelector)` — connect to process stdio
- `Signal(ProcessSelector, Signal)` — send signal to process
**Filesystem Service:**
- `ListDir(Path)` — list directory contents
- `Stat(Path)` — get file metadata
- `WatchDir(Path)` — watch for changes
- `Move(Source, Dest)` — move/rename file
- `RemoveDir(Path)` — remove directory
---
## 4. Lifecycle Management
### Extend timeout
```bash
# Add 60 seconds to the TTL
curl -X POST $E2B_API_URL/sandboxes/{sandboxID}/refreshes \
-H "X-API-Key: $E2B_API_KEY" \
-H "Content-Type: application/json" \
-d '{"duration": 60}'
# Or set an absolute timeout (seconds from now)
curl -X POST $E2B_API_URL/sandboxes/{sandboxID}/timeout \
-H "X-API-Key: $E2B_API_KEY" \
-H "Content-Type: application/json" \
-d '{"timeout": 120}'
```
### Pause (snapshot to disk)
```bash
curl -X POST $E2B_API_URL/sandboxes/{sandboxID}/pause \
-H "X-API-Key: $E2B_API_KEY"
```
### Resume / reconnect
```bash
curl -X POST $E2B_API_URL/sandboxes/{sandboxID}/connect \
-H "X-API-Key: $E2B_API_KEY" \
-H "Content-Type: application/json" \
-d '{"timeout": 300}'
```
Returns `200` if already running, `201` if resumed from paused state.
### Kill
```bash
curl -X DELETE $E2B_API_URL/sandboxes/{sandboxID} \
-H "X-API-Key: $E2B_API_KEY"
```
### Create a snapshot (template from running sandbox)
```bash
curl -X POST $E2B_API_URL/sandboxes/{sandboxID}/snapshots \
-H "X-API-Key: $E2B_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "my-snapshot"}'
```
---
## 5. Network Configuration
Update network rules on a running sandbox:
```bash
curl -X PUT $E2B_API_URL/sandboxes/{sandboxID}/network \
-H "X-API-Key: $E2B_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"allowOut": ["8.8.8.8", "example.com"],
"denyOut": ["0.0.0.0/0"]
}'
```
**Rules:**
- If `allowOut` contains domain names, `denyOut` must include `0.0.0.0/0`
- Domain names are not supported in `denyOut` (IPs/CIDRs only)
- `allowOut` entries take precedence over `denyOut`
- Omitting both fields clears all rules

View File

@@ -0,0 +1,46 @@
## VMs
```bash
E2B_API_URL=http://192.168.0.61:3000
# create vm
curl -X POST $E2B_API_URL/sandboxes \
-H "X-API-Key: $E2B_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"templateID": "base",
"timeout": 300
}'
# response
{"alias":"base","clientID":"6532622b","domain":null,"envdVersion":"0.5.8","sandboxID":"in3h60s6h0ie3kigrcls4","templateID":"pz1l1owhmy0w84e12eqv","trafficAccessToken":null}
sandboxID=in3h60s6h0ie3kigrcls4
# create sandboxVM and capture id
sandboxID=$(curl -s -X POST http://localhost:3000/sandboxes -H "X-API-Key: $E2B_API_KEY" -H "Content-Type: application/json" -d '{
"templateID": "base",
"timeout": 300
}' | jq -r .sandboxID
)
# get state
curl http://localhost:3000/sandboxes/${sandboxID} \
-H "X-API-Key: $E2B_API_KEY"
# list running sandboxes
curl "http://localhost:3000/v2/sandboxes?state=running" \
-H "X-API-Key: $E2B_API_KEY"
# sandbox resource metrics
curl "http://localhost:3000/sandboxes/{sandboxID}/metrics?start=$(date -v-5M +%s)&end=$(date +%s)" \
-H "X-API-Key: $E2B_API_KEY"
# sandbox logs
curl "http://localhost:3000/v2/sandboxes/{sandboxID}/logs?limit=100&direction=backward" \
-H "X-API-Key: $E2B_API_KEY"
```

View File

@@ -0,0 +1,22 @@
# Disable the default all-in-one installation
shims:
disableAll: true
firecracker:
enabled: true
# In this chart version, 'env' must be a map, not a list.
# The template converts these keys into Environment Variables for you.
env:
kataArtifacts: "firecracker"
multiInstallSuffix: "" # This fixes the specific error you saw
installArtifactsOnly: false
# Automatically create the RuntimeClass
runtimeClass:
create: true
name: kata-fc
handler: kata-fc
# Set Firecracker as the default for amd64 nodes
defaultShim:
amd64: firecracker

View File

@@ -0,0 +1,14 @@
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: kata-fc
handler: kata-fc
scheduling:
nodeSelector:
# Ensure these pods only land on your specialized node
runtime: kata-fc
tolerations:
- key: "dedicated"
operator: "Equal"
value: "kata-fc"
effect: "NoSchedule"

View File

@@ -0,0 +1,198 @@
## Node: kube-node-34
```bash
apt install cpu-checker
# check kvm availability
kvm-ok
cat /sys/module/kvm_amd/parameters/nested
lsmod | grep kvm
# install kata containers
# using kata-deploy ?
export VERSION=$(curl -sSL https://api.github.com/repos/kata-containers/kata-containers/releases/latest | jq .tag_name | tr -d '"')
export CHART="oci://ghcr.io/kata-containers/kata-deploy-charts/kata-deploy"
helm upgrade --install kata-deploy "${CHART}" --version "${VERSION}" --values experiments/kata_and_fc_on_kube/manifests/kata-deploy-values.yaml
# label only specific nodes to be used with kata-fc
kubectl label node kube-node-34 runtime=kata-fc
# taint node with NoSchedule
kubectl taint nodes kube-node-34 dedicated=kata-fc:NoSchedule
# configure runtime class
k apply -f experiments/kata_and_fc_on_kube/manifests//runtime-class_kata-fc.yaml
# devmapper configuration for firecracker in containerd
ctr plugins ls | grep devmapper
# create data and metadata files
# might want to replace this with LVM !!!!
# sudo mkdir -p /var/lib/containerd/devmapper
# sudo truncate -s 10G /var/lib/containerd/devmapper/data
# sudo truncate -s 1G /var/lib/containerd/devmapper/init_metadata
# # Associate the files with Loop Devices
# sudo losetup /dev/loop10 /var/lib/containerd/devmapper/data
# sudo losetup /dev/loop11 /var/lib/containerd/devmapper/init_metadata
# # Use dmsetup to create the pool:
# # This command creates the mapping.
# # The numbers '0 20971520' represent the size in 512-byte sectors (for a 10GB file).
# sudo dmsetup create containerd-pool --table "0 20971520 thin-pool /dev/loop11 /dev/loop10 128 32768 1"
lvcreate -L 10G -T kata-vg/kata-pool
# Add the configuration to /etc/containerd/config.toml:
# Find the [plugins."io.containerd.snapshotter.v1.devmapper"] section and update it:
[plugins."io.containerd.snapshotter.v1.devmapper"]
# LVM uses a specific naming convention in /dev/mapper/
# It is VolumeGroupName-LogicalVolumeName
pool_name = "kata--vg-kata--pool"
root_path = "/var/lib/containerd/devmapper"
base_image_size = "10GB"
discard_blocks = true
# restart containerd
systemctl restart containerd
```
### kube node preparation & deployment
```bash
# ---------------------------------------------------------------------------
# Packages
# ---------------------------------------------------------------------------
apt-get update
apt-get install -y \
qemu-guest-agent \
openssh-server \
apt-transport-https \
ca-certificates \
curl \
gnupg \
nvme-cli
systemctl enable --now qemu-guest-agent
systemctl enable --now ssh
# ---------------------------------------------------------------------------
# nvme-tcp
# ---------------------------------------------------------------------------
apt-get install -y linux-modules-extra-$(uname -r)
modprobe nvme-tcp
echo "nvme-tcp" >> /etc/modules-load.d/nvme-tcp.conf
# ---------------------------------------------------------------------------
# Kernel modules for Kubernetes
# ---------------------------------------------------------------------------
cat > /etc/modules-load.d/k8s.conf <<'EOF'
overlay
br_netfilter
EOF
modprobe overlay
modprobe br_netfilter
cat > /etc/sysctl.d/k8s.conf <<'EOF'
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
EOF
sysctl --system
# ---------------------------------------------------------------------------
# containerd
# ---------------------------------------------------------------------------
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" \
> /etc/apt/sources.list.d/docker.list
apt-get update && apt-get install -y containerd.io
cat > /etc/containerd/config.toml <<'EOF'
version = 2
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
runtime_type = "io.containerd.runc.v2"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
SystemdCgroup = true
[plugins."io.containerd.grpc.v1.cri".registry]
config_path = "/etc/containerd/certs.d"
EOF
# Registry mirrors pointing to Zot at 192.168.0.30:5000
mkdir -p \
/etc/containerd/certs.d/docker.io \
/etc/containerd/certs.d/registry.k8s.io \
/etc/containerd/certs.d/ghcr.io \
/etc/containerd/certs.d/quay.io
cat > /etc/containerd/certs.d/docker.io/hosts.toml <<'EOF'
server = "https://registry-1.docker.io"
[host."http://192.168.0.30:5000/v2/docker.io"]
capabilities = ["pull", "resolve"]
skip_verify = true
override_path = true
EOF
cat > /etc/containerd/certs.d/registry.k8s.io/hosts.toml <<'EOF'
server = "https://registry.k8s.io"
[host."http://192.168.0.30:5000/v2/registry.k8s.io"]
capabilities = ["pull", "resolve"]
skip_verify = true
override_path = true
EOF
cat > /etc/containerd/certs.d/ghcr.io/hosts.toml <<'EOF'
server = "https://ghcr.io"
[host."http://192.168.0.30:5000/v2/ghcr.io"]
capabilities = ["pull", "resolve"]
skip_verify = true
override_path = true
EOF
cat > /etc/containerd/certs.d/quay.io/hosts.toml <<'EOF'
server = "https://quay.io"
[host."http://192.168.0.30:5000/v2/quay.io"]
capabilities = ["pull", "resolve"]
skip_verify = true
override_path = true
EOF
systemctl restart containerd
# ---------------------------------------------------------------------------
# kubelet systemd drop-in
# ---------------------------------------------------------------------------
mkdir -p /etc/systemd/system/kubelet.service.d
cat > /etc/systemd/system/kubelet.service.d/10-containerd.conf <<'EOF'
[Unit]
After=containerd.service
Requires=containerd.service
[Service]
ExecStartPre=/bin/bash -c 'until [ -S /var/run/containerd/containerd.sock ]; do sleep 1; done'
ExecStartPre=/usr/bin/crictl info
EOF
# ---------------------------------------------------------------------------
# kubeadm / kubelet / kubectl v1.32
# ---------------------------------------------------------------------------
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.32/deb/Release.key \
| gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.32/deb/ /" \
> /etc/apt/sources.list.d/kubernetes.list
apt-get update && apt-get install -y kubelet kubeadm kubectl
apt-mark hold kubelet kubeadm kubectl
# join kube cluster
JOIN_COMMAND="kubeadm join 192.168.0.31:6443 --token <token> --discovery-token-ca-cert-hash sha256:<hash>"

View File

@@ -0,0 +1,14 @@
```bash
virt-install \
--name microsandbox \
--ram 8192\
--vcpus 4 \
--cpu host-passthrough \
--os-variant ubuntu24.04 \
--disk path=/srv/vms/microsandbox.qcow2,size=100,format=qcow2,bus=virtio \
--network bridge=br0,model=virtio \
--graphics none \
--console pty,target_type=serial \
--location /srv/vms/isos/ubuntu-24.04.3-live-server-amd64.iso,kernel=casper/vmlinuz,initrd=casper/initrd \
--extra-args 'console=ttyS0,115200n8'
```

View File

@@ -0,0 +1,27 @@
#cloud-config
hostname: ${hostname}
manage_etc_hosts: true
# Kernel modules for container networking
write_files:
- path: /etc/modules-load.d/k8s.conf
content: |
overlay
br_netfilter
- path: /etc/sysctl.d/k8s.conf
content: |
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
packages:
- curl
- gpg
- apt-transport-https
runcmd:
- modprobe overlay
- modprobe br_netfilter
- sysctl --system
- swapoff -a
- sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab

View File

@@ -0,0 +1,74 @@
terraform {
required_providers {
orbstack = {
source = "robertdebock/orbstack"
version = "~> 3.0"
}
cloudinit = {
source = "hashicorp/cloudinit"
version = "~> 2.0"
}
}
}
variable "node_count" {
description = "Number of nodes to deploy"
type = number
default = 3
}
variable "name" {
description = "Base name for the machines"
type = string
}
variable "distro" {
description = "OS distribution"
type = string
default = "ubuntu"
}
variable "distro_version" {
description = "OS distribution version/codename"
type = string
default = "noble"
}
variable "extra_cloud_init_parts" {
description = "Additional cloud-init parts to layer on top of the base config"
type = list(object({ content = string, content_type = string }))
default = []
}
data "cloudinit_config" "this" {
count = var.node_count
part {
content_type = "text/cloud-config"
content = templatefile("${path.module}/cloud-init-base.yaml", {
hostname = "${var.name}-${count.index + 1}"
})
}
dynamic "part" {
for_each = var.extra_cloud_init_parts
content {
content_type = part.value.content_type
content = part.value.content
}
}
}
resource "orbstack_machine" "this" {
count = var.node_count
name = "${var.name}-${count.index + 1}"
distro = var.distro
region = var.distro_version
user_data = data.cloudinit_config.this[count.index].rendered
}
output "machines" {
value = { for m in orbstack_machine.this : m.name => m.ip_address }
}

View File

@@ -0,0 +1,11 @@
#cloud-config
merge_how:
- name: list
settings: [append]
- name: dict
settings: [recurse_array]
runcmd:
- curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="server" K3S_TOKEN="${k3s_token}" sh -
- until kubectl get nodes; do sleep 2; done
- cp /etc/rancher/k3s/k3s.yaml /root/kubeconfig.yaml

View File

@@ -0,0 +1,9 @@
#cloud-config
merge_how:
- name: list
settings: [append]
- name: dict
settings: [recurse_array]
runcmd:
- curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="agent" K3S_URL="https://${cp_ip}:6443" K3S_TOKEN="${join_token}" sh -

View File

@@ -0,0 +1,39 @@
variable "k3s_token" {
description = "Shared secret for k3s cluster join (set via TF_VAR or tfvars)"
type = string
sensitive = true
}
module "control_plane" {
source = "../../modules/base-template"
name = "k3s-cp"
node_count = 1
extra_cloud_init_parts = [{
content_type = "text/cloud-config"
content = templatefile("${path.module}/cloud-init-cp.yaml", {
k3s_token = var.k3s_token
})
}]
}
module "workers" {
source = "../../modules/base-template"
name = "k3s-worker"
node_count = 2
extra_cloud_init_parts = [{
content_type = "text/cloud-config"
content = templatefile("${path.module}/cloud-init-worker.yaml", {
cp_ip = values(module.control_plane.machines)[0]
join_token = var.k3s_token
})
}]
}
output "cluster" {
value = {
control_plane = module.control_plane.machines
workers = module.workers.machines
}
}

View File

@@ -0,0 +1,219 @@
## install
```bash
# master node
curl -sfL https://get.rke2.io | INSTALL_RKE2_VERSION=v1.32.12+rke2r1 sh -
systemctl enable rke2-server.service
systemctl start rke2-server.service
journalctl -u rke2-server -f
# open firewalld
sudo firewall-cmd --permanent --add-port=9345/tcp
sudo firewall-cmd --permanent --add-port=6443/tcp
sudo firewall-cmd --permanent --add-port=10250/tcp # Kubelet
sudo firewall-cmd --reload
# install nerdctl
# Set the version
VERSION="2.2.1" # Check GitHub for the latest version
# Download the tarball
wget https://github.com/containerd/nerdctl/releases/download/v${VERSION}/nerdctl-${VERSION}-linux-arm64.tar.gz
# Extract to your path
sudo tar -C /usr/local/bin -xzvf nerdctl-${VERSION}-linux-arm64.tar.gz nerdctl
# configure nerdctl
sudo mkdir -p /etc/nerdctl
sudo tee /etc/nerdctl/nerdctl.toml <<EOF
address = "unix:///run/k3s/containerd/containerd.sock"
namespace = "k8s.io"
EOF
# install buildkit
# Set current stable version
BK_VER="0.28.0"
# Download arm64 binary
wget https://github.com/moby/buildkit/releases/download/v${BK_VER}/buildkit-v${BK_VER}.linux-arm64.tar.gz
# Extract only the binaries to /usr/local/bin
sudo tar -C /usr/local/bin -xzvf buildkit-v${BK_VER}.linux-arm64.tar.gz --strip-components=1 bin/
# Create the service file
sudo tee /etc/systemd/system/buildkit.service <<EOF
[Unit]
Description=BuildKit
Documentation=https://github.com/moby/buildkit
[Service]
ExecStart=/usr/local/bin/buildkitd --addr unix:///run/buildkit/buildkitd.sock
[Install]
WantedBy=multi-user.target
EOF
# Reload and Start
sudo systemctl daemon-reload
sudo systemctl enable --now buildkit
# ---------------------------------------------
# agent/worker node
curl -sfL https://get.rke2.io | INSTALL_RKE2_TYPE="agent" INSTALL_RKE2_VERSION=v1.32.12+rke2r1 sh -
systemctl enable rke2-agent.service
mkdir -p /etc/rancher/rke2/
# token from master node
# cat /var/lib/rancher/rke2/server/node-token
cat <<EOF | sudo tee /etc/rancher/rke2/config.yaml
server: https://192.168.64.3:9345
token: K107618960f87b9efb3a3255ce00a9743d29f1db9376820c9144cb85fa3c554dc69::server:06b2effdf0c9ce3952efc8a5d80bf084
EOF
systemctl start rke2-agent.service
journalctl -u rke2-agent -f
# Set up kubectl on the server node
echo 'export KUBECONFIG=/etc/rancher/rke2/rke2.yaml' >> ~/.bashrc
echo 'export PATH=$PATH:/var/lib/rancher/rke2/bin' >> ~/.bashrc
source ~/.bashrc
```
## build and deploy application
```bash
# build container with nerdctl
nerdctl --namespace k8s.io build --tag hello-world:latest .
# export image as tar on master node
nerdctl save hello-world:latest -o hello-world.tar
# copy it over to worker node
scp hello-world.tar novakj@192.168.64.4:~/
# import image on the agent node
sudo /var/lib/rancher/rke2/bin/ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images import hello-world.tar
kubectl create namespace rke2-apps
cat <<EOF > deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello-world-deployment
namespace: rke2-apps
labels:
type: staticwebapp
spec:
replicas: 1
selector:
matchLabels:
type: staticwebapp
template:
metadata:
labels:
type: staticwebapp
spec:
containers:
- name: staticwebapp
image: hello-world:latest
imagePullPolicy: Never
ports:
- containerPort: 80
resources:
requests:
memory: "32Mi"
cpu: "200m"
limits:
memory: "64Mi"
cpu: "300m"
EOF
kubectl create -f deployment.yaml
# expose deployment
kubectl expose deployment hello-world-deployment --name hello-world-service --port=8080 --target-port=80 -n rke2-apps
# install ingress-nginx (even though i thought that there is ingress controller already deployed)
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.1.2/deploy/static/provider/cloud/deploy.yaml
# create ingress with "localhost" as host
kubectl create ingress hello-world-ingress --class=nginx --rule="test-host/*=hello-world-service:8080" -n rke2-apps
kubectl port-forward -n ingress-nginx service/ingress-nginx-controller 8081:80
# incomplete completion configuration ;-)
dnf install bash-completion -y
alias 'k=kubectl'
# ~/.bashrc
# 1. Load the main bash-completion package first
# On Rocky/RHEL, it's usually at this path:
[[ -r "/usr/share/bash-completion/bash_completion" ]] && . "/usr/share/bash-completion/bash_completion"
# Enable kubectl bash completion
source <(kubectl completion bash)
# Set up the alias
alias k=kubectl
# Link the kubectl completion logic to the 'k' alias
complete -o default -F __start_kubectl k
```
## upgrading RKE2
```bash
# install upgrade controller
kubectl apply -f https://github.com/rancher/system-upgrade-controller/releases/download/v0.9.1/system-upgrade-controller.yaml
# server upgrade
cat <<EOF | kubectl apply -f -
apiVersion: upgrade.cattle.io/v1
kind: Plan
metadata:
name: rke2-server-upgrade
namespace: system-upgrade
spec:
concurrency: 1
cordon: true
nodeSelector:
matchExpressions:
- key: node-role.kubernetes.io/control-plane
operator: In
values: ["true"]
serviceAccountName: system-upgrade
upgrade:
image: rancher/rke2-upgrade
version: v1.33.9+rke2r1
EOF
# agent upgrade
cat <<EOF | kubectl apply -f -
apiVersion: upgrade.cattle.io/v1
kind: Plan
metadata:
name: rke2-agent-upgrade
namespace: system-upgrade
spec:
concurrency: 1
cordon: true
nodeSelector:
matchExpressions:
- key: node-role.kubernetes.io/control-plane
operator: DoesNotExist
prepare:
# Logic: "Don't start workers until servers are done"
args: ["wait-for-plan", "rke2-server-upgrade"]
image: rancher/rke2-upgrade
serviceAccountName: system-upgrade
upgrade:
image: rancher/rke2-upgrade
version: v1.33.9+rke2r1
EOF
```

View File

@@ -0,0 +1,215 @@
## VMS creation
```bash
# hypervisor: beelink (192.168.0.6)
vms_path=/srv/vms/images
isos_path=/srv/vms/isos
cd $isos_path
# Grab Ubuntu 24.04 cloud image
wget https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img
# Create a base disk from the cloud image (repeat per node)
for NODE in rke2-server rke2-agent1 rke2-agent2; do
DISK_SIZE="30G"
[[ "$NODE" == rke2-agent* ]] && DISK_SIZE="50G"
qemu-img create -f qcow2 -F qcow2 -b $isos_path/noble-server-cloudimg-amd64.img $vms_path/${NODE}.qcow2
qemu-img resize $vms_path/${NODE}.qcow2 ${DISK_SIZE}
done
```
```bash
# prepare cloud-init/user-data
apt install cloud-image-utils
# prepare cloud-init and launch VMs
declare -A nodes=(
[rke2-server]="192.168.0.51"
[rke2-agent1]="192.168.0.52"
[rke2-agent2]="192.168.0.53"
)
for node in "${!nodes[@]}"; do
ip="${nodes[$node]}"
# user-data
cat <<EOF > user-data-${node}
#cloud-config
hostname: ${node}
manage_etc_hosts: false
users:
- name: sre
sudo: ALL=(ALL) NOPASSWD:ALL
shell: /bin/bash
ssh_authorized_keys:
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINQxxkqmvtVI+8c5BkTaJ5c2HfBFRXJWMmEcevvfP9tV jan.novak@Jans-MacBook-Air.local
write_files:
- path: /etc/hosts
append: true
content: |
192.168.0.51 rke2-server
192.168.0.52 rke2-agent1
192.168.0.53 rke2-agent2
- path: /etc/modules-load.d/rke2.conf
content: |
br_netfilter
overlay
- path: /etc/sysctl.d/99-rke2.conf
content: |
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
swap:
filename: /swap.img
size: 0
maxsize: 0
runcmd:
- swapoff -a
- sed -i '/swap/d' /etc/fstab
- modprobe br_netfilter
- modprobe overlay
- sysctl --system
package_update: true
packages:
- qemu-guest-agent
- nfs-common
- open-iscsi
power_state:
mode: reboot
EOF
# network config
cat <<EOF > network-config-${node}
network:
version: 2
ethernets:
eth0:
match:
driver: "virtio_net"
addresses:
- "${ip}/24"
nameservers:
addresses:
- 8.8.8.8
routes:
- to: "default"
via: "192.168.0.4"
EOF
cloud-localds --network-config=./network-config-${node} \
$vms_path/${node}-seed.iso ./user-data-${node}
done
# Launch VMs
for node in "${!nodes[@]}"; do
virt-install \
--name ${node} \
--ram 4096 --vcpus 2 \
--os-variant ubuntu24.04 \
--disk $vms_path/${node}.qcow2,bus=virtio \
--disk $vms_path/${node}-seed.iso,device=cdrom \
--network bridge=br0,model=virtio \
--graphics none \
--console pty,target_type=serial \
--noautoconsole \
--import
done
```
## RKE2 installation
```bash
# there are no .deb packages - only rpm or tarball
# "magic" install script can handle that
curl -sfL https://get.rke2.io | INSTALL_RKE2_CHANNEL=v1.32 sudo sh -
# Create config directory
sudo mkdir -p /etc/rancher/rke2
# Server configuration
cat <<EOF | sudo tee /etc/rancher/rke2/config.yaml
# Bind the API to the node's IP (not 127.0.0.1)
tls-san:
- rke2-server
- 192.168.0.51
# Write kubeconfig readable by non-root
write-kubeconfig-mode: "0644"
# CNI - canal is default and fine for home lab
# Alternatives: cilium, calico, multus
cni:
- canal
# Disable servicelb if you plan to use metallb
disable:
- rke2-service-lb
EOF
# Enable and start
sudo systemctl enable rke2-server.service
sudo systemctl start rke2-server.service
# Watch bootstrap (takes 2-3 min on first run)
sudo journalctl -u rke2-server -f
```
## Grab the join token and kubeconfig
```bash
# Token for agents to join
sudo cat /var/lib/rancher/rke2/server/node-token
# Save this somewhere - you'll need it on every agent
# Set up kubectl on the server node
echo 'export KUBECONFIG=/etc/rancher/rke2/rke2.yaml' >> ~/.bashrc
echo 'export PATH=$PATH:/var/lib/rancher/rke2/bin' >> ~/.bashrc
source ~/.bashrc
# Verify
kubectl get nodes
```
## Install RKE2 Agents (Workers)
```bash
# Install RKE2 agent - same channel as server
curl -sfL https://get.rke2.io | INSTALL_RKE2_CHANNEL=v1.32 INSTALL_RKE2_TYPE=agent sudo sh -
# Create config
sudo mkdir -p /etc/rancher/rke2
cat <<EOF | sudo tee /etc/rancher/rke2/config.yaml
server: https://192.168.0.51:9345
token: K10dba0bfff01d610ffed41c6b82a1b8861ee19e5af34a3bdd21970936823a846da::server:1f02fb36a288dcd06770cab28b015bd7
EOF
# Enable and start
sudo systemctl enable rke2-agent.service
sudo systemctl start rke2-agent.service
# Watch join
sudo journalctl -u rke2-agent -f
```
## Copy Kubeconfig to Your Workstation
```bash
# On your workstation (not the VMs)
scp sre@192.168.0.51:/etc/rancher/rke2/rke2.yaml ~/.kube/rke2-homelab.yaml
# Fix the server address (it'll say 127.0.0.1)
gsed -i 's/127.0.0.1/192.168.0.51/' ~/.kube/rke2-homelab.yaml
export KUBECONFIG=~/.kube/rke2-homelab.yaml
kubectl get nodes
```

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,28 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: external-secrets-crds
namespace: external-secrets
spec:
interval: 1h
chart:
spec:
chart: external-secrets
sourceRef:
kind: HelmRepository
name: external-secrets
namespace: flux-system
version: "1.2.1"
values:
installCRDs: true
webhook:
create: false
certController:
create: false
serviceAccount:
create: false
resources: {}
crds:
createClusterExternalSecret: true
createClusterSecretStore: true
createPushSecret: true

View File

@@ -0,0 +1,8 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: external-secrets
namespace: flux-system
spec:
interval: 1h
url: https://charts.external-secrets.io

View File

@@ -0,0 +1,20 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: gitea-ci-deploy
subjects:
- kind: User
name: "gitea_ci@idm.home.hrajfrisbee.cz" # matches preferred_username claim
apiGroup: rbac.authorization.k8s.io
roleRef:
# kind: ClusterRole
# name: edit # scope down as needed
# apiGroup: rbac.authorization.k8s.io
# this is obviously too much permissions
# but we can live with it for homelab
kind: ClusterRole
name: cluster-admin
apiGroup: rbac.authorization.k8s.io

View File

@@ -0,0 +1,15 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: oidc-cluster-admin
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: User
name: https://idm.home.hrajfrisbee.cz/oauth2/openid/k8s#35842461-a1c4-4ad6-8b29-697c5ddbfe84
- apiGroup: rbac.authorization.k8s.io
kind: User
name: novakj@idm.home.hrajfrisbee.cz

View File

@@ -0,0 +1,22 @@
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod-dns
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: kacerr.cz@gmail.com
privateKeySecretRef:
name: letsencrypt-dns-account-key
solvers:
- dns01:
rfc2136:
nameserver: dns-update-proxy.cert-manager.svc.cluster.local:53
tsigKeyName: acme-update-key
tsigAlgorithm: HMACSHA512
tsigSecretSecretRef:
name: acme-update-key
key: acme-update-key
selector:
dnsZones:
- "lab.home.hrajfrisbee.cz"

View File

@@ -0,0 +1,33 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: dns-update-proxy
namespace: cert-manager
spec:
replicas: 1
selector:
matchLabels:
app: dns-update-proxy
template:
metadata:
labels:
app: dns-update-proxy
spec:
containers:
- name: socat-tcp
image: alpine/socat
args:
- TCP-LISTEN:53,fork,reuseaddr
- TCP:87.236.195.209:5353
ports:
- containerPort: 53
protocol: TCP
- name: socat-udp
image: alpine/socat
args:
- -T5
- UDP-RECVFROM:53,fork,reuseaddr
- UDP:87.236.195.209:5353
ports:
- containerPort: 53
protocol: UDP

View File

@@ -0,0 +1,18 @@
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: acme-update-key
namespace: cert-manager
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend # or your store
kind: ClusterSecretStore
target:
name: acme-update-key
creationPolicy: Owner
data:
- secretKey: acme-update-key
remoteRef:
key: k8s_home/cert-manager
property: acme-update-key

View File

@@ -19,8 +19,14 @@ spec:
upgrade: upgrade:
crds: CreateReplace crds: CreateReplace
values: values:
global:
logLevel: 6
crds: crds:
enabled: false enabled: false
config:
apiVersion: controller.config.cert-manager.io/v1alpha1
kind: ControllerConfiguration
enableGatewayAPI: true
prometheus: prometheus:
enabled: true enabled: true
extraObjects: extraObjects:

View File

@@ -0,0 +1,17 @@
apiVersion: v1
kind: Service
metadata:
name: dns-update-proxy
namespace: cert-manager
spec:
selector:
app: dns-update-proxy
ports:
- name: dns-tcp
port: 53
targetPort: 53
protocol: TCP
- name: dns-udp
port: 53
targetPort: 53
protocol: UDP

View File

@@ -0,0 +1,12 @@
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-lab-home-hrajfrisbee
namespace: kube-system
spec:
secretName: wildcard-lab-home-hrajfrisbee-tls
issuerRef:
name: letsencrypt-prod-dns
kind: ClusterIssuer
dnsNames:
- "*.lab.home.hrajfrisbee.cz"

View File

@@ -0,0 +1,27 @@
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: cilium-gateway
namespace: kube-system
spec:
gatewayClassName: cilium
listeners:
- name: http
port: 80
protocol: HTTP
allowedRoutes:
namespaces:
from: All
- name: lab-home-hrajfrisbee-https-wildcard
hostname: "*.lab.home.hrajfrisbee.cz"
port: 443
protocol: HTTPS
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: wildcard-lab-home-hrajfrisbee-tls
allowedRoutes:
namespaces:
from: All

View File

@@ -13,16 +13,19 @@ spec:
kind: HelmRepository kind: HelmRepository
name: cilium name: cilium
namespace: flux-system namespace: flux-system
version: 1.18.5 version: 1.19.1
interval: 5m0s interval: 5m0s
values: values:
cluster: cluster:
name: "home-kube" name: "home-kube"
devices: "eth+ bond+ en+"
hubble: hubble:
relay: relay:
enabled: true enabled: true
ui: ui:
enabled: true enabled: true
ingressController:
enabled: true
ipam: ipam:
mode: cluster-pool mode: cluster-pool
operator: operator:
@@ -30,6 +33,15 @@ spec:
clusterPoolIPv4PodCIDRList: "10.96.0.0/16" clusterPoolIPv4PodCIDRList: "10.96.0.0/16"
l2announcements: l2announcements:
enabled: true enabled: true
gatewayAPI:
enabled: true
gatewayClass:
create: "true"
kubeProxyReplacement: true kubeProxyReplacement: true
k8sServiceHost: 192.168.0.31 # or LB IP k8sServiceHost: 192.168.0.31 # or LB IP
k8sServicePort: 6443 k8sServicePort: 6443
# disable envoy daemonset - i guess that is stupid idea anyway
# envoy:
# enabled: false
# l7Proxy: false

View File

@@ -0,0 +1,17 @@
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: operator-test-lab-home-hrajfrisbee-2
namespace: default
annotations:
gateway-cert-operator.io/gateway-name: "cilium-gateway"
gateway-cert-operator.io/gateway-namespace: "kube-system"
spec:
secretName: operator-test-lab-home-hrajfrisbee-2
issuerRef:
group: cert-manager.io
kind: ClusterIssuer
name: letsencrypt-prod
dnsNames:
- "operator-test-2.lab.home.hrajfrisbee.cz"

View File

@@ -0,0 +1,17 @@
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: operator-test-lab-home-hrajfrisbee
namespace: default
annotations:
gateway-cert-operator.io/gateway-name: "cilium-gateway"
gateway-cert-operator.io/gateway-namespace: "kube-system"
spec:
secretName: operator-test-lab-home-hrajfrisbee
issuerRef:
group: cert-manager.io
kind: ClusterIssuer
name: letsencrypt-prod
dnsNames:
- "operator-test.lab.home.hrajfrisbee.cz"

View File

@@ -0,0 +1,30 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: operator-test
namespace: default
labels:
app: operator-test
spec:
replicas: 1
selector:
matchLabels:
app: operator-test
template:
metadata:
labels:
app: operator-test
spec:
containers:
- name: whoami
image: traefik/whoami
ports:
- containerPort: 80
name: http
resources:
requests:
cpu: 10m
memory: 16Mi
limits:
memory: 32Mi

View File

@@ -0,0 +1,25 @@
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: operator-test-redirect
namespace: default
labels:
app: operator-test
spec:
parentRefs:
- name: cilium-gateway
namespace: kube-system
sectionName: http
hostnames:
- operator-test.lab.home.hrajfrisbee.cz
rules:
- matches:
- path:
type: PathPrefix
value: /
filters:
- type: RequestRedirect
requestRedirect:
scheme: https
statusCode: 301

View File

@@ -0,0 +1,24 @@
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: operator-test
namespace: default
labels:
app: operator-test
spec:
parentRefs:
- name: cilium-gateway
namespace: kube-system
sectionName: auto-operator-test-lab-home-hrajfrisbee-operator-test-lab-home-hrajfrisbee-cz
hostnames:
- operator-test.lab.home.hrajfrisbee.cz
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: operator-test
namespace: default
port: 80

View File

@@ -0,0 +1,14 @@
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
name: allow-kube-system-gateway
namespace: default
spec:
from:
- group: gateway.networking.k8s.io
kind: Gateway
namespace: kube-system
to:
- group: ""
kind: Secret
name: operator-test-lab-home-hrajfrisbee

View File

@@ -0,0 +1,15 @@
---
apiVersion: v1
kind: Service
metadata:
name: operator-test
namespace: default
spec:
type: ClusterIP
selector:
app: operator-test
ports:
- port: 80
targetPort: 80
protocol: TCP
name: http

View File

@@ -0,0 +1,19 @@
apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
name: vault-backend
namespace: external-secrets
spec:
provider:
vault:
server: "https://vault.hrajfrisbee.cz"
path: "secret"
version: "v2"
auth:
appRole:
path: "approle"
roleId: "864e352d-2064-2bf9-2c73-dbd676a95368" # or reference a secret
secretRef:
name: vault-approle
key: secret-id
namespace: external-secrets

View File

@@ -0,0 +1,63 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: external-secrets
namespace: external-secrets
spec:
interval: 30m
chart:
spec:
chart: external-secrets
version: "1.2.1" # latest stable 1.x
sourceRef:
kind: HelmRepository
name: external-secrets
namespace: flux-system
install:
createNamespace: true
remediation:
retries: 3
upgrade:
remediation:
retries: 3
values:
replicaCount: 1
leaderElect: true
# Resources (adjust to your cluster)
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
memory: 256Mi
webhook:
replicaCount: 1
resources:
requests:
cpu: 25m
memory: 64Mi
limits:
memory: 128Mi
podDisruptionBudget:
enabled: true
minAvailable: 1
certController:
replicaCount: 1
resources:
requests:
cpu: 25m
memory: 64Mi
limits:
memory: 128Mi
# Metrics (enable if prometheus-operator is present)
serviceMonitor:
enabled: false
# Pod disruption budgets
podDisruptionBudget:
enabled: true
minAvailable: 1

View File

@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: external-secrets

View File

@@ -0,0 +1,10 @@
apiVersion: v1
kind: Secret
metadata:
name: vault-approle
namespace: external-secrets
annotations:
kustomize.toolkit.fluxcd.io/reconcile: disabled
type: Opaque
stringData:
secret-id: --- fill in the secret_id ---

View File

@@ -1,6 +1,32 @@
--- ---
apiVersion: kustomize.toolkit.fluxcd.io/v1 apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization kind: Kustomization
metadata:
name: 00-crds
namespace: flux-system
spec:
interval: 10m0s
path: ./gitops/home-kubernetes/00-crds
prune: true
sourceRef:
kind: GitRepository
name: flux-system
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: 00-rbac
namespace: flux-system
spec:
interval: 10m0s
path: ./gitops/home-kubernetes/00-rbac
prune: true
sourceRef:
kind: GitRepository
name: flux-system
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata: metadata:
name: cilium name: cilium
namespace: flux-system namespace: flux-system
@@ -27,6 +53,34 @@ spec:
--- ---
apiVersion: kustomize.toolkit.fluxcd.io/v1 apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization kind: Kustomization
metadata:
name: external-secrets
namespace: flux-system
spec:
interval: 10m0s
path: ./gitops/home-kubernetes/external-secrets
prune: true
sourceRef:
kind: GitRepository
name: flux-system
dependsOn:
- name: 00-crds
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: kube-prometheus
namespace: flux-system
spec:
interval: 10m0s
path: ./gitops/home-kubernetes/kube-prometheus
prune: true
sourceRef:
kind: GitRepository
name: flux-system
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata: metadata:
name: ingress-nginx name: ingress-nginx
namespace: flux-system namespace: flux-system
@@ -40,6 +94,19 @@ spec:
--- ---
apiVersion: kustomize.toolkit.fluxcd.io/v1 apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization kind: Kustomization
metadata:
name: oauth-proxy
namespace: flux-system
spec:
interval: 10m0s
path: ./gitops/home-kubernetes/oauth-proxy
prune: true
sourceRef:
kind: GitRepository
name: flux-system
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata: metadata:
name: podinfo name: podinfo
namespace: flux-system namespace: flux-system
@@ -50,6 +117,32 @@ spec:
sourceRef: sourceRef:
kind: GitRepository kind: GitRepository
name: flux-system name: flux-system
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: plane
namespace: flux-system
spec:
interval: 10m0s
path: ./gitops/home-kubernetes/plane
prune: true
sourceRef:
kind: GitRepository
name: flux-system
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: experiments
namespace: flux-system
spec:
interval: 10m0s
path: ./gitops/home-kubernetes/experiments
prune: true
sourceRef:
kind: GitRepository
name: flux-system
# --- # ---
# apiVersion: kustomize.toolkit.fluxcd.io/v1 # apiVersion: kustomize.toolkit.fluxcd.io/v1
# kind: Kustomization # kind: Kustomization

View File

@@ -0,0 +1,16 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: flux-web
namespace: flux-system
spec:
interval: 30m
releaseName: flux-web
chartRef:
kind: OCIRepository
name: flux-operator
values:
fullnameOverride: flux-web
installCRDs: true
web:
serverOnly: true

View File

@@ -0,0 +1,13 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
name: flux-operator
namespace: flux-system
spec:
interval: 30m
url: oci://ghcr.io/controlplaneio-fluxcd/charts/flux-operator
layerSelector:
mediaType: "application/vnd.cncf.helm.chart.content.v1.tar+gzip"
operation: copy
ref:
semver: "0.x"

View File

@@ -0,0 +1,63 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: fuj-management
namespace: fuj
labels:
app: fuj-management
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: fuj-management
template:
metadata:
labels:
app: fuj-management
spec:
imagePullSecrets:
- name: gitea-registry
containers:
- name: fuj-management
image: gitea.home.hrajfrisbee.cz/kacerr/fuj-management:0.39-go
ports:
- containerPort: 8080
name: http
env:
- name: PORT
value: "8080"
- name: TZ
value: Europe/Prague
- name: CREDENTIALS_PATH
value: /secrets/credentials.json
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
readinessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 2
periodSeconds: 10
timeoutSeconds: 3
livenessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 5
periodSeconds: 30
timeoutSeconds: 5
volumeMounts:
- name: bot-credentials
mountPath: /secrets
readOnly: true
volumes:
- name: bot-credentials
secret:
secretName: fuj-management-bot-credentials

View File

@@ -0,0 +1,60 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: fujarna
namespace: fuj
labels:
app: fujarna
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: fujarna
template:
metadata:
labels:
app: fujarna
spec:
imagePullSecrets:
- name: gitea-registry
containers:
- name: fujarna
image: gitea.home.hrajfrisbee.cz/kacerr/fujarna:v0.0.2
ports:
- containerPort: 8080
name: http
env:
- name: PORT
value: "8080"
- name: TZ
value: Europe/Prague
volumeMounts:
- name: data
mountPath: /app/data
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
readinessProbe:
httpGet:
path: /api/tournaments
port: 8080
initialDelaySeconds: 2
periodSeconds: 10
timeoutSeconds: 3
livenessProbe:
httpGet:
path: /api/tournaments
port: 8080
initialDelaySeconds: 5
periodSeconds: 30
timeoutSeconds: 5
volumes:
- name: data
persistentVolumeClaim:
claimName: tournament-data

View File

@@ -0,0 +1,22 @@
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: gitea-registry
namespace: fuj
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: gitea-registry
creationPolicy: Owner
template:
type: kubernetes.io/dockerconfigjson
data:
.dockerconfigjson: "{{ .token }}"
data:
- secretKey: token
remoteRef:
key: k8s_home/gitea/container-registry
property: token

View File

@@ -0,0 +1,30 @@
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: fujarna-redirect
namespace: fuj
labels:
app: fujarna
app.kubernetes.io/name: fujarna-httproute
app.kubernetes.io/instance: fujarna
app.kubernetes.io/version: '6.0'
app.kubernetes.io/component: httproute
app.kubernetes.io/part-of: fujarna
spec:
parentRefs:
- name: cilium-gateway
namespace: kube-system
sectionName: http
hostnames:
- fujarna.lab.home.hrajfrisbee.cz
rules:
- matches:
- path:
type: PathPrefix
value: /
filters:
- type: RequestRedirect
requestRedirect:
scheme: https
statusCode: 301

View File

@@ -0,0 +1,29 @@
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: fujarna
namespace: fuj
labels:
app: fujarna
app.kubernetes.io/name: fujarna-httproute
app.kubernetes.io/instance: fujarna
app.kubernetes.io/version: '6.0'
app.kubernetes.io/component: httproute
app.kubernetes.io/part-of: fujarna
spec:
parentRefs:
- name: cilium-gateway
namespace: kube-system
sectionName: lab-home-hrajfrisbee-https-wildcard
hostnames:
- fujarna.lab.home.hrajfrisbee.cz
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: fujarna
namespace: fuj
port: 80

View File

@@ -0,0 +1,28 @@
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: fuj-management
namespace: fuj
labels:
app: fuj-management
app.kubernetes.io/name: fuj-management-httproute
app.kubernetes.io/instance: fuj-management
app.kubernetes.io/component: httproute
app.kubernetes.io/part-of: fuj-management
spec:
parentRefs:
- name: cilium-gateway
namespace: kube-system
sectionName: lab-home-hrajfrisbee-https-wildcard
hostnames:
- fuj-management.lab.home.hrajfrisbee.cz
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: fuj-management
namespace: fuj
port: 80

View File

@@ -0,0 +1,30 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: fujarna
namespace: fuj
annotations:
# WebSocket support - increase timeouts for live scoring connections
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-http-version: "1.1"
# Uncomment for cert-manager TLS:
# cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: nginx # change to "traefik" for k3s default
rules:
- host: hrajfrisbee.cz # change to your domain
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: fujarna
port:
number: 80
# Uncomment for TLS:
# tls:
# - hosts:
# - hrajfrisbee.cz
# secretName: fujarna-tls

View File

@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: fuj

View File

@@ -0,0 +1,12 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: tournament-data
namespace: fuj
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
# storageClassName: local-path # uncomment to override cluster default

View File

@@ -0,0 +1,8 @@
apiVersion: v1
data:
credentials.json: ewogICAgInR5cGUiOiAic2VydmljZV9hY2NvdW50IiwKICAgICJwcm9qZWN0X2lkIjogInBsYXRlZC1tZXNoLTQ4OTAxMC1zMSIsCiAgICAicHJpdmF0ZV9rZXlfaWQiOiAiYzQxM2VhYWUzMzk0NGFmZDljZjkyZjgzNGYzYmM1NDM5OGI1ZGY5YyIsCiAgICAicHJpdmF0ZV9rZXkiOiAiLS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tXG5NSUlFdmdJQkFEQU5CZ2txaGtpRzl3MEJBUUVGQUFTQ0JLZ3dnZ1NrQWdFQUFvSUJBUURuTzBJQ3p2NDNaMWFKXG5MYi9NSkFZbld2U2NWM2tlelBGSHpHNVd2WUE3SXBxM2ZkcUp6ZWYxSzcxeGJodGY5NFB6T2RNVGdrYXcxcmZYXG40QjhOMS9laWFZaWUyZkkwenA4emptejl0YnpDUjVwM0hiK3M0eHROR3hVUDJBWWQzVmRrbE5TRFlacGJ5Qm8rXG5qNWl1cy9BOXdYV1ZkQ2EvNnA1MmtYSU1Ld3o4b05rbmpMRTlzV21ZV1kvQmJZT1o0UExPSjNTWEdEMFJNQzlGXG4wbjBLUDJTRWxvaTFPc2wvV2s0Q21WazJFc0lNSjBiMHA1Wm1GZUZ6SlAzSFZUS0VtOExHam5vWjVwQmwzWGM0XG5GOThKSVFFQzZQallOcXQvSFlxdWRLb2hrelVSS2h5ekVJdHp3NXBrUnk3d1l1OGJHSGJOMzF0dWFpeDRMdFNjXG5zYU5oOHJhVEFnTUJBQUVDZ2dFQWFXZ2U4RTdSbmdueWJiZzRMV1BpbGtBbEw3dkVUK0VXd0NjVnlWNjdQTG5LXG5rNGpBZlg4cWxSMnFUekhsTXJzUElHb2txVWtDMW93YTVFS2JoV0VFMXJtSytQYmJMVzFmTDA1bXFzVVUxZTkwXG5INGFsSUxlcWMxeThIaXZZcGZhSFp5ZGROTUxpYmFKckxFZWpUMGhoWkpWeTkzMHFTT1EzOWs3WkVXanlrNW9WXG5WcUk2SE4yVGUrRTN0aklmNFcrN0lKN0c3RU9ZVG4zZHlrQnZCN1QwLzR2M0pUeGMzTk1IN0RZYzFyd1dicEdKXG5JcmNidndOa3VyVHhsdWg1VGtvSkZjd3ZXdEl4NE52MFFPNkk5TlpjMllPakZUMncwMUFrKzNDSTJLNmJOVE9qXG43WWFxYlRoNDk2cU1Md2NtRmFUZy9tbXEzMGZycEVsMWdQTWJWZFJqTVFLQmdRRDY3SDMvb0pGU1ZDWnFYQjFEXG5tdHZRblVMSWUvZkU5TXc4T2VrcUdMclQxbGlUTllhTks4YnRsOGFrbFlZY3FXU09zWk53ZkdyNmNhOGhUSEwyXG5pWXZNbkR4aVJPWkVkeElPUFJGY2l4SlNpdG5hb3kzSmtMMml0K2cxMUVGdUEyS3dGd29jc0RrRkQ1QTh0ZEFpXG5UOGhEeEc4dE5saFFrTUY0ekRMNTE2cHRLUUtCZ1FEcjZNZitLelBoNHM2Qm1GVzkrUzNtZEFZZkJETkJsZHJrXG56L1hrY2NteHNWeUpNNTErR2MzY3kzYmhIRDU4aGVLb3U1ZHdYanNqZkRCZlZKcjBpTHM0R3dGbjNlWlJYNmluXG5NVmlycmpyUWt5WnlSVDJkcHl5ODQ5MGN1WC9xQ01hbTVRYzY0UmpJN3JhRTM5ZDBRcnRRd0hWbnZCN1RHaEcvXG50WElXeThMQld3S0JnRGRtMi9NQyt0TVlyVnF5ZCt1alY0RmFGNUs0ZkY1S1JvWE5xNE9WN2pvemkxOEZaRzNyXG5Db01QRGRidEJLMXpZVFo0VXJ0MVhSRnE1R1lQd1JwYUNEbm5EUllOYkdJVmdERVFucksyS2hkN2ZOcGZTUTdHXG5ISGkrN2xCa1N3WUZUVmJmMzJXeUt3VEpDT20rb3ZQUWRjRXdyL05sbWw0L2Q5ZzZEYzlJSk9MeEFvR0JBTi9yXG5QaGwwN0hRV0VpbFViTjQ3TEdRMzhLOTBEeXJMa00vcXpOMGZYSEM3aHJmOE5CdERadkdTZHE1SlhzNVhUc01WXG4vREorWHVmWnN4aHBlQU52M1RUdC9UR1hvRXZRM29hRUFrUkN4alMvMTVoVFBKN3QxVHNkbGZ6ejNnUklmRFpoXG40YkR6ZzlFRk5GMS9Fa1NhS3E0Rlgrc3MxcDFOZTFkYVpJWmF0ZXROQW9HQkFLeStZTGRjQVh6dElaMTkvS0wwXG5sWTBPZi92azVJNUtKNlNiVUVheU9HTFJkWTJBdGk1R1QwQlpjVEV3QjYzWTNyWEVNQ3BsM25Ca1pVRklRczNEXG5wSE9DUDdDMXQ3STRSZlBGaGlSTzEvQnVsajZjUXNrUHFJWlpadGxOQjdROG1BVEttMnpXVThZTjRNbFhZSVBVXG5naGRrZHVQRWIyRC9ueDl3U3F2NzdUeWJcbi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS1cbiIsCiAgICAiY2xpZW50X2VtYWlsIjogImZ1ai1tYW5hZ2VtZW50LWJvdEBwbGF0ZWQtbWVzaC00ODkwMTAtczEuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLAogICAgImNsaWVudF9pZCI6ICIxMDM0ODY2MjM3NTQxOTMzODgxMzQiLAogICAgImF1dGhfdXJpIjogImh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbS9vL29hdXRoMi9hdXRoIiwKICAgICJ0b2tlbl91cmkiOiAiaHR0cHM6Ly9vYXV0aDIuZ29vZ2xlYXBpcy5jb20vdG9rZW4iLAogICAgImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6ICJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9vYXV0aDIvdjEvY2VydHMiLAogICAgImNsaWVudF94NTA5X2NlcnRfdXJsIjogImh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL3JvYm90L3YxL21ldGFkYXRhL3g1MDkvZnVqLW1hbmFnZW1lbnQtYm90JTQwcGxhdGVkLW1lc2gtNDg5MDEwLXMxLmlhbS5nc2VydmljZWFjY291bnQuY29tIiwKICAgICJ1bml2ZXJzZV9kb21haW4iOiAiZ29vZ2xlYXBpcy5jb20iCn0=
kind: Secret
metadata:
name: fuj-management-bot-credentials
namespace: fuj
type: Opaque

View File

@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: fuj-management
namespace: fuj
spec:
type: ClusterIP
selector:
app: fuj-management
ports:
- port: 80
targetPort: 8080
protocol: TCP
name: http

View File

@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: fujarna
namespace: fuj
spec:
type: ClusterIP
selector:
app: fujarna
ports:
- port: 80
targetPort: 8080
protocol: TCP
name: http

View File

@@ -0,0 +1,11 @@
apiVersion: v1
kind: Namespace
metadata:
name: ghost-on-kubernetes
labels:
app: ghost-on-kubernetes
app.kubernetes.io/name: ghost-on-kubernetes
app.kubernetes.io/instance: ghost-on-kubernetes
app.kubernetes.io/version: '6.0'
app.kubernetes.io/component: namespace
app.kubernetes.io/part-of: ghost-on-kubernetes

View File

@@ -0,0 +1,17 @@
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: ghost-config
namespace: ghost-on-kubernetes
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: ghost-config
data:
- secretKey: gmail-app-password
remoteRef:
key: k8s_home/ghost # Vault path (without 'data/' prefix)
property: gmail-app-password

View File

@@ -0,0 +1,42 @@
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: ghost-on-kubernetes-mysql-env
namespace: ghost-on-kubernetes
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: ghost-on-kubernetes-mysql-env # resulting K8s secret name
data:
- secretKey: MYSQL_DATABASE # key in K8s secret
remoteRef:
key: k8s_home/ghost # Vault path (without 'data/' prefix)
property: mysql-db-name # field within Vault secret
- secretKey: MYSQL_USER # key in K8s secret
remoteRef:
key: k8s_home/ghost
property: mysql-db-user
- secretKey: MYSQL_PASSWORD
remoteRef:
key: k8s_home/ghost
property: mysql-db-password
- secretKey: MYSQL_ROOT_PASSWORD
remoteRef:
key: k8s_home/ghost
property: mysql-db-root-password
- secretKey: MYSQL_HOST
remoteRef:
key: k8s_home/ghost
property: mysql-host
# type: Opaque
# stringData:
# MYSQL_DATABASE: mysql-db-name # Same as in config.production.json
# MYSQL_USER: mysql-db-user # Same as in config.production.json
# MYSQL_PASSWORD: mysql-db-password # Same as in config.production.json
# MYSQL_ROOT_PASSWORD: mysql-db-root-password # Same as in config.production.json
# MYSQL_HOST: '%' # Same as in config.production.json

View File

@@ -0,0 +1,21 @@
apiVersion: v1
kind: Secret
metadata:
name: ghost-on-kubernetes-mysql-env
namespace: ghost-on-kubernetes
labels:
app: ghost-on-kubernetes-mysql
app.kubernetes.io/name: ghost-on-kubernetes-mysql-env
app.kubernetes.io/instance: ghost-on-kubernetes
app.kubernetes.io/version: '6.0'
app.kubernetes.io/component: database-secret
app.kubernetes.io/part-of: ghost-on-kubernetes
type: Opaque
stringData:
MYSQL_DATABASE: mysql-db-name # Same as in config.production.json
MYSQL_USER: mysql-db-user # Same as in config.production.json
MYSQL_PASSWORD: mysql-db-password # Same as in config.production.json
MYSQL_ROOT_PASSWORD: mysql-db-root-password # Same as in config.production.json
MYSQL_HOST: '%' # Same as in config.production.json

View File

@@ -0,0 +1,18 @@
apiVersion: v1
kind: Secret
metadata:
name: tls-secret
namespace: ghost-on-kubernetes
labels:
app: ghost-on-kubernetes
app.kubernetes.io/name: tls-secret
app.kubernetes.io/instance: ghost-on-kubernetes
app.kubernetes.io/version: '6.0'
app.kubernetes.io/component: tls-secret
app.kubernetes.io/part-of: ghost-on-kubernetes
type: kubernetes.io/tls
stringData:
tls.crt: content-tls-crt-base64 # Optional, if you want to use your own TLS certificate
tls.key: content-tls-key-base64 # Optional, if you want to use your own TLS certificate

View File

@@ -0,0 +1,49 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: k8s-ghost-content
namespace: ghost-on-kubernetes
labels:
app: ghost-on-kubernetes
app.kubernetes.io/name: k8s-ghost-content
app.kubernetes.io/instance: ghost-on-kubernetes
app.kubernetes.io/version: '6.0'
app.kubernetes.io/component: storage
app.kubernetes.io/part-of: ghost-on-kubernetes
spec:
# Change this to your storageClassName, we suggest using a storageClassName that supports ReadWriteMany for production.
storageClassName: freenas-iscsi
volumeMode: Filesystem
# Change this to your accessModes. We suggest ReadWriteMany for production, ReadWriteOnce for development.
# With ReadWriteMany, you can have multiple replicas of Ghost, so you can achieve high availability.
# Note that ReadWriteMany is not supported by all storage providers and may require additional configuration.
# Ghost officialy doesn't support HA, they suggest using a CDN or caching. Info: https://ghost.org/docs/faq/clustering-sharding-multi-server/
accessModes:
- ReadWriteOnce # Change this to your accessModes if needed, we suggest ReadWriteMany so we can scale the deployment later.
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ghost-on-kubernetes-mysql-pvc
namespace: ghost-on-kubernetes
labels:
app: ghost-on-kubernetes-mysql
app.kubernetes.io/name: ghost-on-kubernetes-mysql-pvc
app.kubernetes.io/instance: ghost-on-kubernetes
app.kubernetes.io/version: '6.0'
app.kubernetes.io/component: database-storage
app.kubernetes.io/part-of: ghost-on-kubernetes
spec:
storageClassName: freenas-iscsi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi

View File

@@ -0,0 +1,49 @@
apiVersion: v1
kind: Service
metadata:
name: ghost-on-kubernetes-service
namespace: ghost-on-kubernetes
labels:
app: ghost-on-kubernetes
app.kubernetes.io/name: ghost-on-kubernetes-service
app.kubernetes.io/instance: ghost-on-kubernetes
app.kubernetes.io/version: '6.0'
app.kubernetes.io/component: service-frontend
app.kubernetes.io/part-of: ghost-on-kubernetes
spec:
ports:
- port: 2368
protocol: TCP
targetPort: ghk8s
name: ghk8s
type: ClusterIP
selector:
app: ghost-on-kubernetes
---
apiVersion: v1
kind: Service
metadata:
name: ghost-on-kubernetes-mysql-service
namespace: ghost-on-kubernetes
labels:
app: ghost-on-kubernetes-mysql
app.kubernetes.io/name: ghost-on-kubernetes-mysql-service
app.kubernetes.io/instance: ghost-on-kubernetes
app.kubernetes.io/version: '6.0'
app.kubernetes.io/component: service-database
app.kubernetes.io/part-of: ghost-on-kubernetes
spec:
ports:
- port: 3306
protocol: TCP
targetPort: mysqlgh
name: mysqlgh
type: ClusterIP
clusterIP: None
selector:
app: ghost-on-kubernetes-mysql

View File

@@ -0,0 +1,59 @@
apiVersion: v1
kind: Secret
metadata:
name: ghost-config-prod
namespace: ghost-on-kubernetes
labels:
app: ghost-on-kubernetes
app.kubernetes.io/name: ghost-config-prod
app.kubernetes.io/instance: ghost-on-kubernetes
app.kubernetes.io/version: '6.0'
app.kubernetes.io/component: ghost-config
app.kubernetes.io/part-of: ghost-on-kubernetes
type: Opaque
stringData:
config.production.json: |-
{
"url": "https://ghost.lab.home.hrajfrisbee.cz",
"admin": {
"url": "https://ghost.lab.home.hrajfrisbee.cz"
},
"server": {
"port": 2368,
"host": "0.0.0.0"
},
"mail": {
"transport": "SMTP",
"from": "user@server.com",
"options": {
"service": "Google",
"host": "smtp.gmail.com",
"port": 465,
"secure": true,
"auth": {
"user": "user@server.com",
"pass": "passsword"
}
}
},
"logging": {
"transports": [
"stdout"
]
},
"database": {
"client": "mysql",
"connection":
{
"host": "ghost-on-kubernetes-mysql-service",
"user": "mysql-db-user",
"password": "mysql-db-password",
"database": "mysql-db-name",
"port": "3306"
}
},
"process": "local",
"paths": {
"contentPath": "/home/nonroot/app/ghost/content"
}
}

View File

@@ -0,0 +1,134 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: ghost-on-kubernetes-mysql
namespace: ghost-on-kubernetes
labels:
app: ghost-on-kubernetes-mysql
app.kubernetes.io/name: ghost-on-kubernetes-mysql
app.kubernetes.io/instance: ghost-on-kubernetes
app.kubernetes.io/version: '6.0'
app.kubernetes.io/component: database
app.kubernetes.io/part-of: ghost-on-kubernetes
spec:
serviceName: ghost-on-kubernetes-mysql-service
replicas: 1
selector:
matchLabels:
app: ghost-on-kubernetes-mysql
template:
metadata:
labels:
app: ghost-on-kubernetes-mysql
spec:
initContainers:
- name: ghost-on-kubernetes-mysql-init
securityContext:
allowPrivilegeEscalation: false
privileged: false
readOnlyRootFilesystem: true
image: docker.io/busybox:stable-musl
imagePullPolicy: Always # You can change this value according to your needs
command:
- /bin/sh
- -c
- |
set -e
echo 'Changing ownership of mysql mount directory to 65534:65534'
chown -Rfv 65534:65534 /mnt/mysql || echo 'Error changing ownership of mysql mount directory to 65534:65534'
echo 'Changing ownership of tmp mount directory to 65534:65534'
chown -Rfv 65534:65534 /mnt/tmp || echo 'Error changing ownership of tmp mount directory to 65534:65534'
echo 'Changing ownership of socket mount directory to 65534:65534'
chown -Rfv 65534:65534 /mnt/var/run/mysqld || echo 'Error changing ownership of socket mount directory to 65534:65534'
volumeMounts:
- name: ghost-on-kubernetes-mysql-volume
mountPath: /mnt/mysql
subPath: mysql-empty-subdir
readOnly: false
- name: ghost-on-kubernetes-mysql-tmp
mountPath: /mnt/tmp
readOnly: false
- name: ghost-on-kubernetes-mysql-socket
mountPath: /mnt/var/run/mysqld
readOnly: false
# YOu can ajust the resources according to your needs
resources:
requests:
memory: 0Mi
cpu: 0m
limits:
memory: 1Gi
cpu: 900m
containers:
- name: ghost-on-kubernetes-mysql
securityContext:
allowPrivilegeEscalation: false
privileged: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 65534
image: docker.io/mysql:8.4
imagePullPolicy: Always # You can change this value according to your needs
envFrom:
- secretRef:
name: ghost-on-kubernetes-mysql-env
resources:
requests:
memory: 500Mi # You can change this value according to your needs
cpu: 300m # You can change this value according to your needs
limits:
memory: 1Gi # You can change this value according to your needs
cpu: 900m # You can change this value according to your needs
ports:
- containerPort: 3306
protocol: TCP
name: mysqlgh
volumeMounts:
- name: ghost-on-kubernetes-mysql-volume
mountPath: /var/lib/mysql
subPath: mysql-empty-subdir
readOnly: false
- name: ghost-on-kubernetes-mysql-tmp
mountPath: /tmp
readOnly: false
- name: ghost-on-kubernetes-mysql-socket
mountPath: /var/run/mysqld
readOnly: false
automountServiceAccountToken: false
# Optional: Uncomment the following to specify node selectors
# affinity:
# nodeAffinity:
# requiredDuringSchedulingIgnoredDuringExecution:
# nodeSelectorTerms:
# - matchExpressions:
# - key: node-role.kubernetes.io/worker
# operator: In
# values:
# - 'true'
securityContext:
seccompProfile:
type: RuntimeDefault
volumes:
- name: ghost-on-kubernetes-mysql-volume
persistentVolumeClaim:
claimName: ghost-on-kubernetes-mysql-pvc
- name: ghost-on-kubernetes-mysql-tmp
emptyDir:
sizeLimit: 128Mi
- name: ghost-on-kubernetes-mysql-socket
emptyDir:
sizeLimit: 128Mi

View File

@@ -0,0 +1,214 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: ghost-on-kubernetes
namespace: ghost-on-kubernetes
labels:
app: ghost-on-kubernetes
app.kubernetes.io/name: ghost-on-kubernetes
app.kubernetes.io/instance: ghost-on-kubernetes
app.kubernetes.io/version: '6.0'
app.kubernetes.io/component: ghost
app.kubernetes.io/part-of: ghost-on-kubernetes
spec:
# If you want HA for your Ghost instance, you can increase the number of replicas AFTER creation and you need to adjust the storage class. See 02-pvc.yaml for more information.
replicas: 1
selector:
matchLabels:
app: ghost-on-kubernetes
minReadySeconds: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 3
revisionHistoryLimit: 4
progressDeadlineSeconds: 600
template:
metadata:
namespace: ghost-on-kubernetes
labels:
app: ghost-on-kubernetes
spec:
automountServiceAccountToken: false # Disable automounting of service account token
volumes:
- name: k8s-ghost-content
persistentVolumeClaim:
claimName: k8s-ghost-content
- name: ghost-config-prod
secret:
secretName: ghost-config-prod
defaultMode: 420
- name: tmp
emptyDir:
sizeLimit: 64Mi
initContainers:
- name: permissions-fix
imagePullPolicy: Always
image: docker.io/busybox:stable-musl
env:
- name: GHOST_INSTALL
value: /home/nonroot/app/ghost
- name: GHOST_CONTENT
value: /home/nonroot/app/ghost/content
- name: NODE_ENV
value: production
securityContext:
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
resources:
limits:
cpu: 900m
memory: 1000Mi
requests:
cpu: 100m
memory: 128Mi
command:
- /bin/sh
- '-c'
- |
set -e
export DIRS='files logs apps themes data public settings images media'
echo 'Check if base dirs exists, if not, create them'
echo "Directories to check: $DIRS"
for dir in $DIRS; do
if [ ! -d $GHOST_CONTENT/$dir ]; then
echo "Creating $GHOST_CONTENT/$dir directory"
mkdir -pv $GHOST_CONTENT/$dir || echo "Error creating $GHOST_CONTENT/$dir directory"
fi
chown -Rfv 65532:65532 $GHOST_CONTENT/$dir && echo "chown ok on $dir" || echo "Error changing ownership of $GHOST_CONTENT/$dir directory"
done
exit 0
volumeMounts:
- name: k8s-ghost-content
mountPath: /home/nonroot/app/ghost/content
readOnly: false
containers:
- name: ghost-on-kubernetes
# For development, you can use the following image:
# image: ghcr.io/sredevopsorg/ghost-on-kubernetes:latest-dev
# image: ghcr.io/sredevopsorg/ghost-on-kubernetes:main
image: ghost:bookworm
imagePullPolicy: Always
ports:
- name: ghk8s
containerPort: 2368
protocol: TCP
# You should uncomment the following lines in production. Change the values according to your environment.
readinessProbe:
httpGet:
path: /ghost/api/v4/admin/site/
port: ghk8s
httpHeaders:
- name: X-Forwarded-Proto
value: https
- name: Host
value: ghost.lab.home.hrajfrisbee.cz
periodSeconds: 10
timeoutSeconds: 3
successThreshold: 1
failureThreshold: 3
initialDelaySeconds: 10
livenessProbe:
httpGet:
path: /ghost/api/v4/admin/site/
port: ghk8s
httpHeaders:
- name: X-Forwarded-Proto
value: https
- name: Host
value: ghost.lab.home.hrajfrisbee.cz
periodSeconds: 300
timeoutSeconds: 3
successThreshold: 1
failureThreshold: 1
initialDelaySeconds: 30
env:
- name: NODE_ENV
value: production
- name: url
value: "https://ghost.lab.home.hrajfrisbee.cz"
- name: database__client
value: "mysql"
- name: database__connection__host
value: "ghost-on-kubernetes-mysql-service"
- name: database__connection__port
value: "3306"
- name: database__connection__user
valueFrom:
secretKeyRef:
name: ghost-on-kubernetes-mysql-env
key: MYSQL_USER
- name: database__connection__password
valueFrom:
secretKeyRef:
name: ghost-on-kubernetes-mysql-env
key: MYSQL_PASSWORD
- name: database__connection__database
value: "ghost"
- name: mail__transport
value: "SMTP"
- name: mail__options__service
value: "Gmail"
- name: mail__options__auth__user
value: "kacerr.cz@gmail.com"
- name: mail__options__auth__pass
valueFrom:
secretKeyRef:
name: ghost-config
key: gmail-app-password
- name: mail__from
value: "'Kacerr's Blog' <kacerr.cz@gmail.com>"
resources:
limits:
cpu: 800m
memory: 800Mi
requests:
cpu: 100m
memory: 256Mi
volumeMounts:
- name: k8s-ghost-content
mountPath: /home/nonroot/app/ghost/content
readOnly: false
- name: ghost-config-prod
readOnly: true
mountPath: /home/nonroot/app/ghost/config.production.json
subPath: config.production.json
- name: tmp # This is the temporary volume mount to allow loading themes
mountPath: /tmp
readOnly: false
securityContext:
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
runAsNonRoot: true
runAsUser: 65532
restartPolicy: Always
terminationGracePeriodSeconds: 15
dnsPolicy: ClusterFirst
# Optional: Uncomment the following to specify node selectors
# affinity:
# nodeAffinity:
# requiredDuringSchedulingIgnoredDuringExecution:
# nodeSelectorTerms:
# - matchExpressions:
# - key: node-role.kubernetes.io/worker
# operator: In
# values:
# - 'true'
securityContext: {}

View File

@@ -0,0 +1,30 @@
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: ghost-on-kubernetes-redirect
namespace: ghost-on-kubernetes
labels:
app: ghost-on-kubernetes
app.kubernetes.io/name: ghost-on-kubernetes-httproute
app.kubernetes.io/instance: ghost-on-kubernetes
app.kubernetes.io/version: '6.0'
app.kubernetes.io/component: httproute
app.kubernetes.io/part-of: ghost-on-kubernetes
spec:
parentRefs:
- name: cilium-gateway
namespace: kube-system
sectionName: http
hostnames:
- ghost.lab.home.hrajfrisbee.cz
rules:
- matches:
- path:
type: PathPrefix
value: /
filters:
- type: RequestRedirect
requestRedirect:
scheme: https
statusCode: 301

View File

@@ -0,0 +1,29 @@
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: ghost-on-kubernetes
namespace: ghost-on-kubernetes
labels:
app: ghost-on-kubernetes
app.kubernetes.io/name: ghost-on-kubernetes-httproute
app.kubernetes.io/instance: ghost-on-kubernetes
app.kubernetes.io/version: '6.0'
app.kubernetes.io/component: httproute
app.kubernetes.io/part-of: ghost-on-kubernetes
spec:
parentRefs:
- name: cilium-gateway
namespace: kube-system
sectionName: lab-home-hrajfrisbee-https-wildcard
hostnames:
- ghost.lab.home.hrajfrisbee.cz
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: ghost-on-kubernetes-service
namespace: ghost-on-kubernetes
port: 2368

View File

@@ -0,0 +1,33 @@
# Optional: If you have a domain name, you can create an Ingress resource to expose your Ghost blog to the internet.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ghost-on-kubernetes-ingress
namespace: ghost-on-kubernetes
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
labels:
app: ghost-on-kubernetes
app.kubernetes.io/name: ghost-on-kubernetes-ingress
app.kubernetes.io/instance: ghost-on-kubernetes
app.kubernetes.io/version: '6.0'
app.kubernetes.io/component: ingress
app.kubernetes.io/part-of: ghost-on-kubernetes
spec:
ingressClassName: nginx
tls:
- hosts:
- ghost.lab.home.hrajfrisbee.cz
secretName: tls-secret
rules:
- host: ghost.lab.home.hrajfrisbee.cz
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: ghost-on-kubernetes-service
port:
name: ghk8s

View File

@@ -11,11 +11,13 @@ spec:
sourceRef: sourceRef:
kind: HelmRepository kind: HelmRepository
name: ingress-nginx name: ingress-nginx
version: 4.12.0 version: 4.14.1
values: values:
controller: controller:
admissionWebhooks: admissionWebhooks:
enabled: false enabled: false
patch: patch:
enabled: false enabled: false
config:
annotations-risk-level: "Critical"
interval: 5m0s interval: 5m0s

View File

@@ -0,0 +1,79 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: kube-prometheus-stack
namespace: monitoring
spec:
interval: 30m
chart:
spec:
chart: kube-prometheus-stack
sourceRef:
kind: HelmRepository
name: prometheus-community
namespace: flux-system
install:
createNamespace: true
crds: CreateReplace
remediation:
retries: 3
upgrade:
crds: CreateReplace
remediation:
retries: 3
values:
prometheus:
prometheusSpec:
retention: 60d
storageSpec:
volumeClaimTemplate:
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 20Gi
resources:
requests:
memory: 0.5Gi
cpu: 500m
limits:
memory: 4Gi
cpu: 2
serviceMonitorSelectorNilUsesHelmValues: false
podMonitorSelectorNilUsesHelmValues: false
ruleSelectorNilUsesHelmValues: false
alertmanager:
alertmanagerSpec:
storage:
volumeClaimTemplate:
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 3Gi
grafana:
persistence:
enabled: true
size: 10Gi
adminPassword: admin
ingress:
enabled: true
ingressClassName: nginx # adjust if using traefik/contour/etc
hosts:
- grafana.lab.home.hrajfrisbee.cz
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/auth-response-headers: X-Auth-Request-User,X-Auth-Request-Email,Authorization
nginx.ingress.kubernetes.io/auth-signin: https://oauth2-proxy.lab.home.hrajfrisbee.cz/oauth2/start?rd=$scheme://$host$escaped_request_uri
nginx.ingress.kubernetes.io/auth-url: https://oauth2-proxy.lab.home.hrajfrisbee.cz/oauth2/auth
tls:
- secretName: grafana-tls
hosts:
- grafana.lab.home.hrajfrisbee.cz
prometheusOperator:
admissionWebhooks:
certManager:
enabled: false

View File

@@ -0,0 +1,8 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: prometheus-community
namespace: flux-system
spec:
interval: 1h
url: https://prometheus-community.github.io/helm-charts

View File

@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: monitoring

View File

@@ -0,0 +1,34 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: coredns
namespace: kube-system
data:
Corefile: |
.:53 {
errors
health {
lameduck 5s
}
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
hosts {
# 192.168.0.30 vault.hrajfrisbee.cz
fallthrough
}
prometheus :9153
forward . /etc/resolv.conf {
max_concurrent 1000
}
cache 30 {
disable success cluster.local
disable denial cluster.local
}
loop
reload
loadbalance
}

View File

@@ -0,0 +1,19 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: mariadb-operator-crds
namespace: mariadb-operator
spec:
interval: 1h
chart:
spec:
chart: mariadb-operator-crds
version: "25.10.*"
sourceRef:
kind: HelmRepository
name: mariadb-operator
namespace: flux-system
install:
crds: Create
upgrade:
crds: CreateReplace

View File

@@ -0,0 +1,31 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: mariadb-operator
namespace: mariadb-operator
spec:
interval: 1h
dependsOn:
- name: mariadb-operator-crds
chart:
spec:
chart: mariadb-operator
version: "25.10.*"
sourceRef:
kind: HelmRepository
name: mariadb-operator
namespace: flux-system
values:
# uses built-in cert-controller for webhook TLS (no cert-manager dep)
webhook:
cert:
certManager:
enabled: false
# disable HA for operator itself (fine for testing)
ha:
enabled: false
# optional: enable metrics
metrics:
enabled: false
serviceMonitor:
enabled: false

View File

@@ -0,0 +1,8 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: mariadb-operator
namespace: flux-system
spec:
interval: 1h
url: https://helm.mariadb.com/mariadb-operator

Some files were not shown because too many files have changed in this diff Show More