Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,6 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json

# turbo
.turbo
.wrangler
.wrangler
.tmp
helm/envsync/charts
121 changes: 121 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
SHELL := /bin/bash

RELEASE ?= envsync
NAMESPACE ?= envsync
KIND_CLUSTER_NAME ?= envsync
CHART_DIR ?= helm/envsync
VALUES_KIND ?= $(CHART_DIR)/values-kind.yaml
GENERATED_VALUES ?= .tmp/values-kind.generated.yaml
HELM ?= $(if $(wildcard $(CURDIR)/.tmp/bin/helm),$(CURDIR)/.tmp/bin/helm,$(shell command -v helm 2>/dev/null))

.DEFAULT_GOAL := help

.PHONY: help
help:
@printf "EnvSync deployment helpers\n\n"
@printf "Targets:\n"
@printf " %-20s %s\n" "prereqs-check" "Verify required local tooling"
@printf " %-20s %s\n" "kind-create" "Create the local Kind cluster"
@printf " %-20s %s\n" "kind-delete" "Delete the local Kind cluster"
@printf " %-20s %s\n" "helm-deps" "Build Helm chart dependencies"
@printf " %-20s %s\n" "helm-lint" "Lint the Helm chart"
@printf " %-20s %s\n" "helm-template" "Render the Helm chart locally"
@printf " %-20s %s\n" "helm-install-kind" "Install or upgrade the chart into Kind"
@printf " %-20s %s\n" "helm-uninstall" "Uninstall the release"
@printf " %-20s %s\n" "status" "Show workload status in the namespace"
@printf " %-20s %s\n" "port-forward-api" "Port-forward the API service to localhost:4000"

.PHONY: prereqs-check
prereqs-check:
@missing=0; \
for tool in kind kubectl openssl bun; do \
if ! command -v $$tool >/dev/null 2>&1; then \
echo "Missing required tool: $$tool"; \
missing=1; \
fi; \
done; \
if [ -z "$(HELM)" ] || [ ! -x "$(HELM)" ]; then \
echo "Missing required tool: helm"; \
echo "Install Helm from https://helm.sh/docs/intro/install/"; \
missing=1; \
fi; \
if [ $$missing -ne 0 ]; then \
exit 1; \
fi

.PHONY: kind-create
kind-create: prereqs-check
@if kind get clusters | grep -qx "$(KIND_CLUSTER_NAME)"; then \
echo "Kind cluster '$(KIND_CLUSTER_NAME)' already exists"; \
else \
kind create cluster --name "$(KIND_CLUSTER_NAME)" --config kind-config.yaml; \
fi

.PHONY: kind-delete
kind-delete:
@kind delete cluster --name "$(KIND_CLUSTER_NAME)"

.PHONY: helm-deps
helm-deps: prereqs-check
@"$(HELM)" dependency build "$(CHART_DIR)"

.PHONY: helm-lint
helm-lint: helm-deps
@"$(HELM)" lint "$(CHART_DIR)"

.PHONY: generate-kind-values
generate-kind-values:
@mkdir -p .tmp
@cat > "$(GENERATED_VALUES)" <<EOF
zitadel:
masterkey: "$(shell openssl rand -base64 24 | tr -d '\n' | cut -c1-32)"
admin:
password: "$(shell openssl rand -base64 18 | tr -d '\n' | cut -c1-18)"
minikms:
rootKey: "$(shell openssl rand -hex 16)"
rustfs:
accessKey: "rustfsadmin"
secretKey: "$(shell openssl rand -base64 24 | tr -d '\n' | cut -c1-32)"
postgresql:
auth:
postgresPassword: "$(shell openssl rand -base64 24 | tr -d '\n' | cut -c1-32)"
password: "$(shell openssl rand -base64 24 | tr -d '\n' | cut -c1-32)"
replicationPassword: "$(shell openssl rand -base64 24 | tr -d '\n' | cut -c1-32)"
database:
roles:
zitadel:
password: "$(shell openssl rand -base64 24 | tr -d '\n' | cut -c1-32)"
openfga:
password: "$(shell openssl rand -base64 24 | tr -d '\n' | cut -c1-32)"
minikms:
password: "$(shell openssl rand -base64 24 | tr -d '\n' | cut -c1-32)"
EOF

.PHONY: helm-template
helm-template: helm-deps generate-kind-values
@"$(HELM)" template "$(RELEASE)" "$(CHART_DIR)" \
--namespace "$(NAMESPACE)" \
-f "$(VALUES_KIND)" \
-f "$(GENERATED_VALUES)"

.PHONY: helm-install-kind
helm-install-kind: kind-create helm-deps generate-kind-values
@kubectl create namespace "$(NAMESPACE)" --dry-run=client -o yaml | kubectl apply -f -
@"$(HELM)" upgrade --install "$(RELEASE)" "$(CHART_DIR)" \
--namespace "$(NAMESPACE)" \
-f "$(VALUES_KIND)" \
-f "$(GENERATED_VALUES)" \
--wait \
--timeout 20m

.PHONY: helm-uninstall
helm-uninstall:
@"$(HELM)" uninstall "$(RELEASE)" --namespace "$(NAMESPACE)"

.PHONY: status
status:
@kubectl get all,pvc,ingress,configmap,secret -n "$(NAMESPACE)"

.PHONY: port-forward-api
port-forward-api:
@kubectl port-forward -n "$(NAMESPACE)" svc/$(RELEASE)-api 4000:4000
9 changes: 6 additions & 3 deletions docker-compose.prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
services:
envsync_api:
image: ghcr.io/envsync-cloud/envsync-api:latest
user: "0"
env_file:
- .env
ports:
Expand All @@ -17,7 +18,7 @@ services:
S3_ENDPOINT: http://rustfs:9000
S3_BUCKET_URL: http://rustfs:9000
OPENFGA_API_URL: http://openfga:8090
ZITADEL_URL: http://zitadel:8080
ZITADEL_URL: ${ZITADEL_URL:-http://auth.127.0.0.1.sslip.io:8080}
MINIKMS_GRPC_ADDR: minikms:50051
OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4318
depends_on:
Expand Down Expand Up @@ -46,7 +47,7 @@ services:
OPENFGA_API_URL: http://openfga:8090
DATABASE_HOST: postgres
S3_ENDPOINT: http://rustfs:9000
ZITADEL_URL: http://zitadel:8080
ZITADEL_URL: ${ZITADEL_URL:-http://auth.127.0.0.1.sslip.io:8080}
MINIKMS_GRPC_ADDR: minikms:50051
volumes:
- zitadel_data:/zitadel-data:ro
Expand Down Expand Up @@ -177,7 +178,9 @@ services:
zitadel_db:
condition: service_healthy
networks:
- envsync_network
envsync_network:
aliases:
- auth.127.0.0.1.sslip.io

openfga_db:
image: postgres:17
Expand Down
189 changes: 189 additions & 0 deletions docs/self-hosting-bringup-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
# EnvSync Self-Hosting Bring-Up Report

## Goal

Get EnvSync running locally in a self-hosting-oriented setup without changing application source code.

## What Was Added

### Deployment scaffolding

- Added a Helm-first deployment scaffold under `helm/envsync/`.
- Added a repo-root `Makefile` to wrap Kind and Helm workflows.
- Added `kind-config.yaml` for local cluster creation.
- Added a minimal `k8s/README.md` and replaced the old `k8s/Makefile` with a stub so the repo no longer pretends the advanced operator-based path is already implemented.

### Compose/runtime improvements

- Updated `docker-compose.prod.yaml` so local self-hosting can actually bootstrap and run.
- Added `.tmp` and `helm/envsync/charts` to `.gitignore` to keep local tool installs and vendored chart artifacts out of git.

## What Was Verified Live

The following stack was brought up successfully via `docker compose -f docker-compose.prod.yaml`:

- `envsync_api`
- `postgres`
- `redis`
- `rustfs`
- `zitadel_db`
- `zitadel`
- `openfga_db`
- `openfga_migrate`
- `openfga`
- `minikms_db`
- `minikms`

Bootstrap was run successfully with `envsync_init`, and the generated values were written back into the local `.env`.

Verified working endpoints:

- `http://localhost:4000/health`
- `http://localhost:4000/version`
- `http://localhost:4000/api/access/web`
- `http://auth.127.0.0.1.sslip.io:8080`

## Fixes Applied

### 1. OpenFGA bootstrap failure from unsafe password characters

Problem:

- `openfga_migrate` builds a Postgres URI directly from `OPENFGA_DB_PASSWORD`.
- Random passwords containing `/` or `+` broke URI parsing.

Fix:

- Regenerated local compose secrets with URL-safe values.
- Recreated the fresh local volumes so the databases were initialized with the corrected passwords.

### 2. Zitadel first-instance setup failure

Problem:

- The generated admin password did not satisfy Zitadel’s password complexity policy.

Fix:

- Replaced the local admin password with a compliant value and recreated the fresh local Zitadel data.

### 3. Zitadel internal/external hostname mismatch

Problem:

- Containers were calling Zitadel as `http://zitadel:8080`.
- Zitadel was configured for a different external domain, so discovery returned `404` during bootstrap.

Fix:

- Switched the shared local Zitadel URL to `http://auth.127.0.0.1.sslip.io:8080`.
- Added a Docker network alias on the `zitadel` service for `auth.127.0.0.1.sslip.io`.
- Updated `envsync_api` and `envsync_init` to consume `ZITADEL_URL` from env instead of hardcoding `http://zitadel:8080`.

### 4. `.localhost` was not usable inside containers

Problem:

- `auth.localhost` looked attractive for local development.
- Inside Bun containers it resolves to loopback by design, bypassing Docker service resolution.

Fix:

- Switched to `auth.127.0.0.1.sslip.io`.
- On the host it resolves to `127.0.0.1`.
- Inside Docker it resolves through the Compose network alias to the Zitadel service.

### 5. API crash on `/logs`

Problem:

- The `envsync_api` image runs as the `envsync` user.
- The logger resolves its log directory to `/logs` and tries to create it at container root.
- The process crashed with `EACCES: permission denied, mkdir '/logs'`.

Fix:

- Added a local-only compose override: `user: "0"` for `envsync_api`.
- This avoids changing application code while allowing the current image to start cleanly.

### 6. `prod-init.ts` blocks on HyperDX even when HyperDX is not part of the started stack

Problem:

- The init script always executes the HyperDX wait step.
- That makes bootstrap fail or stall even when HyperDX is not intended to run.

Workaround used:

- Started a temporary lightweight stub container on the Compose network with alias `hdx`.
- Let the init script finish.
- Removed the stub afterward.

Status:

- This is still an application/runtime bug worth fixing later in `prod-init.ts`.

## Infrastructure Issues Encountered

### 1. Kind did not work on this host

Observed failure:

- Docker runtime error around `/dev/mapper/control` permissions while creating the Kind cluster.

Impact:

- Helm manifests were linted and rendered successfully, but a live Kind validation was blocked by host runtime constraints.

### 2. k3d fallback also failed on this host

Observed failure:

- k3s exited with a cgroup-v2 cpuset error.

Impact:

- Local Kubernetes validation could not be completed on this machine even though the deployment scaffolding is in place.

## Current Local Runtime Shape

The working local self-hosting flow is:

1. Start core services with `docker compose -f docker-compose.prod.yaml up -d`.
2. Run bootstrap once with `docker compose -f docker-compose.prod.yaml run --rm envsync_init`.
3. Persist the generated Zitadel and OpenFGA values into local `.env`.
4. Start `envsync_api`.
5. Validate with `/health`, `/version`, and `/api/access/web`.

## Files Changed

Primary tracked files changed for this work:

- `.gitignore`
- `docker-compose.prod.yaml`
- `Makefile`
- `kind-config.yaml`
- `helm/envsync/**`
- `k8s/README.md`
- `k8s/Makefile`

This document:

- `docs/self-hosting-bringup-report.md`

## Known Follow-Ups

- Make HyperDX optional in `prod-init.ts` instead of requiring a runtime stub.
- Fix the logger/image mismatch so `envsync_api` does not need `user: "0"` in local compose.
- Validate the Helm chart on a host where Kind or another local Kubernetes runtime works.
- Decide whether the advanced operator-based `k8s/` path should be implemented now or kept deferred behind the Helm path.

## Important Note

No application source code was intentionally modified as part of this bring-up.

There was already an unrelated local change in:

- `apps/envsync-landing/src/utils/env.ts`

That file was left alone.
8 changes: 8 additions & 0 deletions helm/envsync/.helmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.DS_Store
.git/
.gitignore
.idea/
.vscode/
tmp/
*.swp
*.bak
9 changes: 9 additions & 0 deletions helm/envsync/Chart.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
dependencies:
- name: postgresql
repository: oci://registry-1.docker.io/bitnamicharts
version: 18.5.6
- name: redis
repository: oci://registry-1.docker.io/bitnamicharts
version: 24.1.2
digest: sha256:0fb8bbb06f14a4872de3526db3be4532904560583e2db2859dfb207259b452fb
generated: "2026-03-24T12:51:22.883188186+05:30"
16 changes: 16 additions & 0 deletions helm/envsync/Chart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
apiVersion: v2
name: envsync
description: Helm chart for self-hosted EnvSync control plane services
type: application
version: 0.1.0
appVersion: "0.4.0"
kubeVersion: ">=1.28.0-0"
dependencies:
- name: postgresql
version: 18.5.6
repository: oci://registry-1.docker.io/bitnamicharts
condition: postgresql.enabled
- name: redis
version: 24.1.2
repository: oci://registry-1.docker.io/bitnamicharts
condition: redis.enabled
Loading