Deploy a new self-hosted service to the Kubernetes cluster from a GitHub repository.
Use when: (1) User provides a GitHub URL or project name and wants to deploy it,
(2) User says "deploy [service]" or "set up [service]",
(3) User wants to add a new service to the cluster.
Automated workflow: Docker image → Terraform module → Deploy.
Handles database setup, ingress, DNS configuration.
설치
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
Deploy a new self-hosted service to the Kubernetes cluster from a GitHub repository.
Use when: (1) User provides a GitHub URL or project name and wants to deploy it,
(2) User says "deploy [service]" or "set up [service]",
(3) User wants to add a new service to the cluster.
Automated workflow: Docker image → Terraform module → Deploy.
Handles database setup, ingress, DNS configuration.
author
Claude Code
version
1.0.0
date
"2025-01-01T00:00:00.000Z"
Setup Project Skill
Purpose: Deploy a new self-hosted service to the Kubernetes cluster from a GitHub repository.
When to use: User provides a GitHub URL or project name and wants to deploy it to the cluster.
Last resort: Build from Dockerfile (avoid if possible)
Classify Dockerfile State (drives whether we contribute a PR back upstream later):
State
When
Action on deploy success
image-used
An official/community image worked (priority 1-5).
No upstream PR. Default case.
used-as-is
Upstream ships a Dockerfile; it built and ran fine.
No upstream PR.
fixed-broken-upstream
Upstream Dockerfile exists but fails to build / run; we patched it.
Open a fix-dockerfile PR after stability gate.
written-from-scratch
Upstream has no Dockerfile at all; we authored one.
Open an add-dockerfile PR after stability gate.
Record the chosen state and supporting metadata in modules/kubernetes/<service>/.contribution-state.json. When we author or fix a Dockerfile, also write modules/kubernetes/<service>/files/Dockerfile, .dockerignore, and BUILD.md (from templates/Dockerfile.README.md) — these travel with the upstream PR.
Use shared Redis: redis.redis.svc.cluster.local:6379
No password required
IMPORTANT: Never create databases yourself - always ask user for credentials to use.
3. NFS Storage Setup (if service needs persistent data)
IMPORTANT: NFS directories must exist and be exported on the NFS server BEFORE deploying the service. If the directory doesn't exist, the pod will fail to mount the volume and get stuck in ContainerCreating.
The NFS export must be configured in TrueNAS so Kubernetes nodes can mount it
Create the export via TrueNAS WebUI or API, allowing access from the Kubernetes network (10.0.20.0/24)
Verify the export is accessible:
# From a k8s node or the dev VM
showmount -e 10.0.10.15 | grep <service>
Verify the mount works before proceeding:
# Quick test from a k8s node
ssh root@10.0.20.100 'mount -t nfs 10.0.10.15:/mnt/main/<service> /tmp/test-mount && ls /tmp/test-mount && umount /tmp/test-mount'
Only proceed to Terraform module creation after confirming the NFS export is accessible.
4. Terraform Module Creation
Create module directory:
mkdir -p modules/kubernetes/<service-name>/
Create modules/kubernetes/<service-name>/main.tf:
variable "tls_secret_name" {}
variable "tier" { type = string }
variable "postgresql_password" {} # Only if needed
# Add other variables as needed (smtp_password, api_keys, etc.)
resource "kubernetes_namespace" "<service>" {
metadata {
name = "<service>"
}
}
module "tls_secret" {
source = "../setup_tls_secret"
namespace = kubernetes_namespace.<service>.metadata[0].name
tls_secret_name = var.tls_secret_name
}
# If database migrations needed, add init_container
resource "kubernetes_deployment" "<service>" {
metadata {
name = "<service>"
namespace = kubernetes_namespace.<service>.metadata[0].name
labels = {
app = "<service>"
tier = var.tier
}
}
spec {
replicas = 1
selector {
match_labels = {
app = "<service>"
}
}
template {
metadata {
labels = {
app = "<service>"
}
}
spec {
# Init container for migrations (if needed)
# init_container { ... }
container {
name = "<service>"
image = "<docker-image>:<tag>"
port {
container_port = <port>
}
# Environment variables
env {
name = "DATABASE_URL"
value = "postgresql://<service>:${var.postgresql_password}@postgresql.dbaas.svc.cluster.local:5432/<service>"
}
# Add other env vars as needed
# Volume mounts for persistent data
volume_mount {
name = "data"
mount_path = "<mount-path>"
sub_path = "<optional-subpath>"
}
resources {
requests = {
memory = "256Mi"
cpu = "100m"
}
limits = {
memory = "2Gi"
cpu = "1"
}
}
# Health checks (if endpoints exist)
liveness_probe {
http_get {
path = "/health" # or /healthz, /, etc.
port = <port>
}
initial_delay_seconds = 60
period_seconds = 30
}
}
# NFS volume for persistence
volume {
name = "data"
nfs {
server = "10.0.10.15"
path = "/mnt/main/<service>"
}
}
}
}
}
}
resource "kubernetes_service" "<service>" {
metadata {
name = "<service>"
namespace = kubernetes_namespace.<service>.metadata[0].name
labels = {
app = "<service>"
}
}
spec {
selector = {
app = "<service>"
}
port {
name = "http"
port = 80
target_port = <container-port>
}
}
}
module "ingress" {
source = "../ingress_factory"
namespace = kubernetes_namespace.<service>.metadata[0].name
name = "<service>"
tls_secret_name = var.tls_secret_name
# Add extra_annotations if needed (proxy-body-size, timeouts, etc.)
}
5. Update Main Terraform Files
Add to modules/kubernetes/main.tf:
Add variable declarations at top:
variable "<service>_postgresql_password" { type = string }
Add to appropriate DEFCON level (ask user which level, default to 5):
env {
name = "MAILER_HOST"
value = "mailserver.viktorbarzin.me" # Public hostname for TLS
}
env {
name = "MAILER_PORT"
value = "587"
}
env {
name = "MAILER_USER"
value = "info@viktorbarzin.me"
}
env {
name = "MAILER_PASSWORD"
value = var.mailserver_accounts["info@viktorbarzin.me"] # Pass from module
}
What the script does (all via GitHub REST — gh CLI is sandbox-blocked):
Reads .contribution-state.json; skips unless state is written-from-scratch or fixed-broken-upstream and no contribution_pr_url is already recorded.
Upstream sanity checks: repo exists, public, not archived; default branch discoverable; for written-from-scratch, verifies a Dockerfile didn't land upstream while we were deploying; bails cleanly if an open PR from our fork already exists.
POST /repos/<owner>/<name>/forks — idempotent; waits up to 30s for the fork to be ready at ViktorBarzin/<name>.
POST /repos/ViktorBarzin/<name>/merge-upstream — keeps fork current with upstream default branch.
Creates branch add-dockerfile (or fix-dockerfile), timestamp-suffixed if that branch already exists with unrelated commits.
Commits Dockerfile, .dockerignore, BUILD.md via Contents API. Each commit message carries Signed-off-by: for DCO-enforcing repos.
Opens PR against upstream with body rendered from templates/PR_BODY.md.
Writes contribution_pr_url back into .contribution-state.json and echoes the URL.
GitHub 5xx → 3× exponential backoff, then hard fail with a clear message — safe to re-run the script.
After the PR opens: the URL is in .contribution-state.json. Share it with the user. No automated follow-up on merge/reject — that's a manual check for now.
Common Patterns
Init Container for Migrations
init_container {
name = "migration"
image = "<same-image>"
command = ["sh", "-c", "<migration-command>"]
# Same env vars and volumes as main container
}
Dynamic Environment Variables
locals {
common_env = [
{ name = "VAR1", value = "value1" },
{ name = "VAR2", value = "value2" },
]
}
dynamic "env" {
for_each = local.common_env
content {
name = env.value.name
value = env.value.value
}
}
External URL Configuration
Many apps need their public URL configured:
env {
name = "APP_URL" # or PUBLIC_URL, EXTERNAL_URL, etc.
value = "https://<service>.viktorbarzin.me"
}
env {
name = "HTTPS" # or ENABLE_HTTPS, etc.
value = "true"
}
Checklist
Find official Docker image or docker-compose
Identify dependencies (DB, Redis, etc.)
Ask user for database credentials (never create yourself)
Create NFS directory and export on TrueNAS (if persistent storage needed)
Verify NFS mount is accessible from k8s nodes
Create modules/kubernetes/<service>/main.tf
Classify dockerfile_state and write .contribution-state.json
If writing/fixing Dockerfile: satisfy the quality bar (multi-stage, non-root, .dockerignore, BUILD.md)