From 41987cdd1a8b98a514501734288a0697f80a52f4 Mon Sep 17 00:00:00 2001 From: FYWinds Date: Thu, 22 Oct 2026 16:42:04 -0700 Subject: [PATCH] feat: deploy folder with compose example, drop helm chart The chart only served one cluster and its README carried that setup's details. deploy/ now holds a single-instance compose stack from the published image (migration before server, persistent volumes, loopback port), .env.example, and a README covering reverse proxy, upgrades, backups and multi-replica requirements. BREAKING CHANGE: deploy/chart and the root docker-compose.yaml are removed; use deploy/docker-compose.yaml or your own manifests. --- README.md | 82 +------------ app/core/log.py | 2 +- app/core/telemetry.py | 4 +- deploy/.env.example | 34 ++++++ deploy/README.md | 142 ++++++++++++++++++++++ deploy/chart/Chart.yaml | 6 - deploy/chart/templates/configmap.yaml | 10 -- deploy/chart/templates/deployment.yaml | 102 ---------------- deploy/chart/templates/migration-job.yaml | 46 ------- deploy/chart/templates/postgres.yaml | 47 ------- deploy/chart/templates/redis.yaml | 23 ---- deploy/chart/templates/service.yaml | 17 --- deploy/chart/values.yaml | 80 ------------ deploy/docker-compose.yaml | 88 ++++++++++++++ docker-compose.yaml | 60 --------- 15 files changed, 269 insertions(+), 474 deletions(-) create mode 100644 deploy/.env.example create mode 100644 deploy/README.md delete mode 100644 deploy/chart/Chart.yaml delete mode 100644 deploy/chart/templates/configmap.yaml delete mode 100644 deploy/chart/templates/deployment.yaml delete mode 100644 deploy/chart/templates/migration-job.yaml delete mode 100644 deploy/chart/templates/postgres.yaml delete mode 100644 deploy/chart/templates/redis.yaml delete mode 100644 deploy/chart/templates/service.yaml delete mode 100644 deploy/chart/values.yaml create mode 100644 deploy/docker-compose.yaml delete mode 100644 docker-compose.yaml diff --git a/README.md b/README.md index 8a9b7cc..5b8ca5b 100644 --- a/README.md +++ b/README.md @@ -81,87 +81,9 @@ Everything above works natively on Windows. Differences: Other standard `OTEL_*` variables (`OTEL_TRACES_SAMPLER`, `OTEL_METRIC_EXPORT_INTERVAL`, …) are honored. The default sampler is `parentbased_always_on`: requests follow the caller's sampling decision, scheduler jobs are always traced. -## Deploying to Kubernetes +## Deployment -WynnSourceServer uses [Kustomize](https://kustomize.io/) overlays for deployment and [ArgoCD](https://argo-cd.readthedocs.io/) with [Image Updater](https://argocd-image-updater.readthedocs.io/) for GitOps. - -### Directory Structure - -``` -deploy/ - base/ # Shared manifests - deployment.yaml # App deployment with health probes - service.yaml # ClusterIP service on port 8000 - postgres.yaml # CNPG PostgreSQL cluster - redis.yaml # Redis instance (via Redis Operator) - migration-job.yaml # Alembic migration (ArgoCD sync hook) - kustomization.yaml - overlays/ - dev/ # Dev environment (namespace: wynnsource-dev) - kustomization.yaml - prod/ # Prod environment (namespace: wynnsource, replicas: 2) - kustomization.yaml -``` - -### Cluster Prerequisites - -The following operators must be installed in the cluster: - -- [CloudNativePG](https://cloudnative-pg.io/) — PostgreSQL operator -- [Redis Operator](https://github.com/OT-CONTAINER-KIT/redis-operator) — Redis operator -- [ArgoCD Image Updater](https://argocd-image-updater.readthedocs.io/) — Automatic image updates - -### Step 1: Create Secrets - -Create the application secrets in the target namespace. Using [Sealed Secrets](https://sealed-secrets.netlify.app/) is recommended. - -```bash -kubectl -n create secret generic wynnsource-secrets \ - --from-literal=WCS_ADMIN_TOKEN='' -``` - -### Step 2: Create ConfigMap - -```bash -kubectl -n create configmap wynnsource-config \ - --from-literal=LEVEL='INFO' \ - --from-literal=BETA_ALLOWED_VERSIONS='0.2.6, 0.2.7' -``` - -### Step 3: Create ArgoCD Application - -```yaml -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: wynnsource - namespace: argocd - annotations: - argocd-image-updater.argoproj.io/image-list: app=ghcr.io/wynnsource/wynnsource-server - argocd-image-updater.argoproj.io/app.update-strategy: newest-build - argocd-image-updater.argoproj.io/app.allow-tags: "regexp:^dev-" - argocd-image-updater.argoproj.io/app.ignore-tags: "latest,dev-latest" -spec: - project: default - source: - repoURL: https://github.com/WynnSource/WynnSourceServer.git - targetRevision: dev # or master for production - path: deploy/overlays/dev # or deploy/overlays/prod - destination: - server: https://kubernetes.default.svc - namespace: wynnsource-dev # or wynnsource - syncPolicy: - automated: - prune: true - selfHeal: true - syncOptions: - - CreateNamespace=true - - ServerSideApply=true -``` - -### Step 4: Configure Ingress - -Add an IngressRoute (or Ingress) pointing to the `wynnsource-server` service on port 8000. Make sure the `X-Real-IP` header is forwarded from the ingress controller for proper client IP logging and rate limiting. +See [`deploy/`](deploy/README.md): a single-instance Docker Compose setup using the published image, and notes for running several replicas (for example on Kubernetes). ## Using the Schema (Protobuf) diff --git a/app/core/log.py b/app/core/log.py index 00d0ad1..fcdf263 100644 --- a/app/core/log.py +++ b/app/core/log.py @@ -14,7 +14,7 @@ def _add_trace_context(record: "loguru.Record") -> None: - """Attach the active span, so Grafana can jump from a trace to its log lines.""" + """Attach the active span, so log viewers can jump from a trace to its log lines.""" ctx = trace.get_current_span().get_span_context() if ctx.is_valid: trace_id, span_id = trace.format_trace_id(ctx.trace_id), trace.format_span_id(ctx.span_id) diff --git a/app/core/telemetry.py b/app/core/telemetry.py index 71975e3..21c54ea 100644 --- a/app/core/telemetry.py +++ b/app/core/telemetry.py @@ -76,7 +76,7 @@ def setup_telemetry(app: FastAPI) -> bool: if not TELEMETRY_CONFIG.enabled: return False - # Stable HTTP semantic conventions (http.route, url.path, ...), matching Traefik's spans. + # Stable HTTP semantic conventions (http.route, url.path, ...), matching current proxies' spans. os.environ.setdefault("OTEL_SEMCONV_STABILITY_OPT_IN", "http") resource = _resource() @@ -88,7 +88,7 @@ def setup_telemetry(app: FastAPI) -> bool: _meter_provider = MeterProvider( resource=resource, metric_readers=[PeriodicExportingMetricReader(OTLPMetricExporter())], - # HTTP RED metrics come from Tempo span metrics and Traefik. + # HTTP RED metrics are expected from span metrics in the tracing backend or from the proxy. views=[View(instrument_name="http.server.*", aggregation=DropAggregation())], ) metrics.set_meter_provider(_meter_provider) diff --git a/deploy/.env.example b/deploy/.env.example new file mode 100644 index 0000000..36bbd71 --- /dev/null +++ b/deploy/.env.example @@ -0,0 +1,34 @@ +# Copy to .env next to docker-compose.yaml and fill in the required values. +# .env is git-ignored. Every variable is documented in the root README. + +# --- Required ------------------------------------------------------------- +POSTGRES_PASSWORD=change-me +WCS_ADMIN_TOKEN=change-me + +# --- Image and listener --------------------------------------------------- +# Release tag (e.g. 0.1.8) or latest. Pin a version in production. +WCS_VERSION=latest +# Host address and port the server is published on. Keep 127.0.0.1 when a +# reverse proxy on the same host terminates TLS. +WCS_BIND=127.0.0.1 +WCS_PORT=8000 + +# --- Database -------------------------------------------------------------- +POSTGRES_DB=wcs_db +POSTGRES_USER=wynnsource + +# --- Server --------------------------------------------------------------- +LEVEL=INFO +LOG_JSON=false +MAX_IP_RECORDS=10 +# Comma-separated mod versions allowed to submit while in beta. +BETA_ALLOWED_VERSIONS= + +# --- Optional: error reporting (Sentry) ----------------------------------- +# WCS_SENTRY_DSN= +# WCS_SENTRY_ENVIRONMENT=production +# WCS_SENTRY_PROFILE_SESSION_SAMPLE_RATE=0 + +# --- Optional: OpenTelemetry traces and metrics (OTLP/HTTP) ----------------- +# OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318 +# OTEL_RESOURCE_ATTRIBUTES=deployment.environment.name=production diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..6d4bb13 --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,142 @@ +# Deploying WynnSourceServer + +Two starting points: + +- **[Single instance with Docker Compose](#single-instance-with-docker-compose)**: one host, everything in this folder. +- **[Larger deployments](#larger-deployments)**: what to keep in mind when running several replicas, for example on Kubernetes. + +All configuration is environment variables, documented in the [root README](../README.md#environment-variables). + +## Single instance with Docker Compose + +`docker-compose.yaml` runs four services from the published image `ghcr.io/wynnsource/wynnsource-server`: + +| Service | Role | +|---|---| +| `server` | The API, on port 8000 inside the container | +| `migration` | Runs `alembic upgrade head` once, then exits; `server` starts only after it succeeds | +| `db` | PostgreSQL 18, data in the `db-data` volume | +| `redis` | Redis (cache, rate limits, scheduler job locks), data in the `redis-data` volume | + +### Start + +```bash +cd deploy +cp .env.example .env +# Set at least POSTGRES_PASSWORD and WCS_ADMIN_TOKEN, and pin WCS_VERSION to a release. +docker compose up -d +docker compose ps # server should become healthy +curl http://127.0.0.1:8000/readyz +``` + +To run your own checkout instead of the published image, uncomment `build: ..` in `docker-compose.yaml` and use `docker compose up -d --build`. + +### Put a reverse proxy in front + +The server is published on `127.0.0.1:8000` only. Expose it through a reverse proxy on the same host that: + +- terminates TLS; +- sets **`X-Real-IP`** to the client address. The server uses it for rate limiting and per-user IP records, and **rejects token-authenticated requests that arrive without it** (except from `127.0.0.1`); +- forwards `Host` and `X-Forwarded-Proto`. + +Example for nginx: + +```nginx +location / { + proxy_pass http://127.0.0.1:8000; + 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; +} +``` + +### Upgrade + +```bash +# set WCS_VERSION in .env to the new release +docker compose pull +docker compose up -d # migration runs first, then the server is recreated +``` + +Releases are tagged `X.Y.Z`; `latest` follows the newest release. `dev-` and `dev-latest` are pre-release builds from the `dev` branch. + +### Back up + +The database is the only state worth keeping. Redis holds cache and locks that rebuild themselves. + +```bash +docker compose exec -T db sh -c 'pg_dump -U "$POSTGRES_USER" -Fc "$POSTGRES_DB"' > wcs_db-$(date +%F).dump +# restore into an empty database: +docker compose exec -T db sh -c 'pg_restore -U "$POSTGRES_USER" -d "$POSTGRES_DB" --clean --if-exists' < wcs_db-YYYY-MM-DD.dump +``` + +Keep copies off the host, and try a restore once before you need it. + +### Stop + +`docker compose down` keeps the volumes; `docker compose down -v` deletes the database. + +## Larger deployments + +The server is stateless, so it scales horizontally. State lives in PostgreSQL; Redis is shared by all replicas. + +```mermaid +flowchart LR + client([Clients]) --> proxy[TLS reverse proxy / ingress
sets X-Real-IP] + proxy --> s1[server replica] + proxy --> s2[server replica] + s1 & s2 --> pg[(PostgreSQL)] + s1 & s2 --> redis[(Redis)] + mig[migration job
alembic upgrade head] --> pg + s1 & s2 -. OTLP/HTTP .-> otel[OpenTelemetry collector] +``` + +### Rollout order + +1. PostgreSQL and Redis are available. +2. Run the migration **once** per release (`uv run --no-dev alembic upgrade head` in the release image), as a one-shot job, not from every replica. +3. Roll the server replicas to the same image. + +### Requirements for several replicas + +- **Distinct identity per replica.** Scheduled jobs take a lock in Redis held under `K8S_POD_NAME` (defaults to the hostname). Telemetry needs a distinct `service.instance.id` per replica, e.g. `OTEL_RESOURCE_ATTRIBUTES=service.instance.id=`, so replicas never share a metric series. +- **Probes.** Liveness on `GET /healthz`, readiness on `GET /readyz` (port 8000). Give startup up to a minute before liveness checks begin. +- **Client IP.** The proxy or ingress in front must set `X-Real-IP` (see above). Uvicorn trusts forwarded headers only from `127.0.0.1`, `::1` and `10.0.0.0/8`. +- **Shutdown.** The container runs uvicorn as PID 1 and handles `SIGTERM` (finishes requests, flushes telemetry); give it a termination grace period of a few seconds. +- **Resources.** About 100m CPU / 256 MiB per replica at rest is a reasonable request; limit memory at around 1 GiB. + +### Configuration and secrets + +- Keep `WCS_ADMIN_TOKEN`, database credentials and `WCS_SENTRY_DSN` in a secret store and inject them as environment variables. Never commit them. +- Non-secret settings (`LEVEL`, `LOG_JSON`, `MAX_IP_RECORDS`, `BETA_ALLOWED_VERSIONS`) can live in plain configuration. Restart or roll the replicas when they change. + +### Database + +- Use a managed PostgreSQL or a PostgreSQL operator rather than a hand-run container. +- Enable point-in-time recovery (continuous WAL archiving plus base backups), store backups off the cluster, and test restores. +- Size `max_connections` for all replicas plus the migration job. + +### Observability + +- `LOG_JSON=true` writes one JSON object per line, with `trace_id`/`span_id`, for log pipelines. +- `OTEL_EXPORTER_OTLP_ENDPOINT` enables OpenTelemetry traces and metrics over OTLP/HTTP. +- `WCS_SENTRY_DSN` enables error reporting; `WCS_SENTRY_PROFILE_SESSION_SAMPLE_RATE` adds continuous profiling. + +### Image updates + +Pin a release tag and update it deliberately, or let an image-update tool follow the `X.Y.Z` tags (production) or `dev-` tags (staging). Avoid deploying `latest`, since a restart could then change the version. + +### Kubernetes objects + +A typical Kubernetes setup uses these objects. Write them in whatever tooling you use (plain manifests, Kustomize, Helm): + +| Object | Purpose | +|---|---| +| `Deployment` | Server replicas with the probes and env above | +| `Service` | ClusterIP on port 8000 | +| `ConfigMap` / `Secret` | Settings and credentials, consumed with `envFrom` | +| `Job` | Migration, run before each rollout (e.g. as a sync or pre-upgrade hook) | +| PostgreSQL | Managed database or an operator-managed cluster | +| Redis | A small instance; persistence is optional | +| `Ingress` (or equivalent) | TLS and the `X-Real-IP` header | diff --git a/deploy/chart/Chart.yaml b/deploy/chart/Chart.yaml deleted file mode 100644 index 40f4592..0000000 --- a/deploy/chart/Chart.yaml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: v2 -name: wynnsource-server -description: WynnSource server with its PostgreSQL and Redis dependencies -type: application -version: 0.1.0 -appVersion: "0.1.5" diff --git a/deploy/chart/templates/configmap.yaml b/deploy/chart/templates/configmap.yaml deleted file mode 100644 index 0bc3fba..0000000 --- a/deploy/chart/templates/configmap.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: wynnsource-config - annotations: - argocd.argoproj.io/sync-wave: "0" -data: -{{- range $key, $value := .Values.config }} - {{ $key }}: {{ $value | quote }} -{{- end }} diff --git a/deploy/chart/templates/deployment.yaml b/deploy/chart/templates/deployment.yaml deleted file mode 100644 index a7fab24..0000000 --- a/deploy/chart/templates/deployment.yaml +++ /dev/null @@ -1,102 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: wynnsource-server - annotations: - argocd.argoproj.io/sync-wave: "2" -spec: - # Old ReplicaSets kept for rollback; the k8s default of 10 is noise. - revisionHistoryLimit: {{ .Values.revisionHistoryLimit }} - replicas: {{ .Values.replicaCount }} - selector: - # Immutable on the pre-chart deployment; never change this. - matchLabels: - app: wynnsource-server - strategy: - type: RollingUpdate - template: - metadata: - labels: - app: wynnsource-server - annotations: - checksum/config: {{ .Values.config | toYaml | sha256sum }} - spec: - securityContext: - runAsNonRoot: true - containers: - - name: server - image: "{{ .Values.image.repository }}:{{ required "image.tag is required" .Values.image.tag }}" - ports: - - containerPort: 8000 - name: http - envFrom: - - configMapRef: - name: wynnsource-config - - secretRef: - name: {{ .Values.existingSecret }} - env: - # Downward API first: later entries reference them via $(...). - # K8S_POD_NAME also identifies the holder of scheduler job locks. - - name: K8S_POD_NAME - valueFrom: - fieldRef: - fieldPath: metadata.name - - name: K8S_NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - {{- with .Values.telemetry }} - {{- if .otlpEndpoint }} - - name: OTEL_EXPORTER_OTLP_ENDPOINT - value: {{ .otlpEndpoint | quote }} - - name: OTEL_RESOURCE_ATTRIBUTES - value: "service.instance.id=$(K8S_POD_NAME),k8s.pod.name=$(K8S_POD_NAME),k8s.namespace.name=$(K8S_NAMESPACE){{ with .environment }},deployment.environment.name={{ . }}{{ end }}" - {{- end }} - {{- end }} - - name: POSTGRES_HOST - valueFrom: - secretKeyRef: - name: wynnsource-pg-app - key: host - - name: POSTGRES_PORT - valueFrom: - secretKeyRef: - name: wynnsource-pg-app - key: port - - name: POSTGRES_USER - valueFrom: - secretKeyRef: - name: wynnsource-pg-app - key: user - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: wynnsource-pg-app - key: password - - name: POSTGRES_DB - valueFrom: - secretKeyRef: - name: wynnsource-pg-app - key: dbname - - name: REDIS_HOST - value: wynnsource-redis - - name: REDIS_PORT - value: "6379" - {{- with .Values.extraEnv }} - {{- toYaml . | nindent 12 }} - {{- end }} - livenessProbe: - httpGet: { path: /healthz, port: 8000 } - initialDelaySeconds: 10 - periodSeconds: 15 - readinessProbe: - httpGet: { path: /readyz, port: 8000 } - initialDelaySeconds: 5 - periodSeconds: 10 - startupProbe: - httpGet: { path: /healthz, port: 8000 } - initialDelaySeconds: 3 - periodSeconds: 5 - failureThreshold: 10 - resources: - {{- toYaml .Values.resources | nindent 12 }} diff --git a/deploy/chart/templates/migration-job.yaml b/deploy/chart/templates/migration-job.yaml deleted file mode 100644 index 3a64532..0000000 --- a/deploy/chart/templates/migration-job.yaml +++ /dev/null @@ -1,46 +0,0 @@ -apiVersion: batch/v1 -kind: Job -metadata: - name: wynnsource-migration - annotations: - argocd.argoproj.io/hook: Sync - argocd.argoproj.io/hook-delete-policy: BeforeHookCreation - argocd.argoproj.io/sync-wave: "1" -spec: - backoffLimit: 3 - template: - spec: - containers: - - name: migration - image: "{{ .Values.image.repository }}:{{ required "image.tag is required" .Values.image.tag }}" - command: ["uv", "run", "--no-dev", "alembic", "upgrade", "head"] - envFrom: - - secretRef: - name: {{ .Values.existingSecret }} - env: - - name: POSTGRES_HOST - valueFrom: - secretKeyRef: - name: wynnsource-pg-app - key: host - - name: POSTGRES_PORT - valueFrom: - secretKeyRef: - name: wynnsource-pg-app - key: port - - name: POSTGRES_USER - valueFrom: - secretKeyRef: - name: wynnsource-pg-app - key: user - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: wynnsource-pg-app - key: password - - name: POSTGRES_DB - valueFrom: - secretKeyRef: - name: wynnsource-pg-app - key: dbname - restartPolicy: OnFailure diff --git a/deploy/chart/templates/postgres.yaml b/deploy/chart/templates/postgres.yaml deleted file mode 100644 index dcafd0a..0000000 --- a/deploy/chart/templates/postgres.yaml +++ /dev/null @@ -1,47 +0,0 @@ -{{- if .Values.postgres.enabled }} -apiVersion: postgresql.cnpg.io/v1 -kind: Cluster -metadata: - name: wynnsource-pg - annotations: - argocd.argoproj.io/sync-wave: "0" - # Data outlives the app: ArgoCD must neither prune this nor cascade-delete - # it when the Application is removed. - argocd.argoproj.io/sync-options: Prune=false,Delete=false -spec: - instances: 1 - resources: - {{- toYaml .Values.postgres.resources | nindent 4 }} - storage: - size: {{ .Values.postgres.storageSize }} - {{- with .Values.postgres.storageClass }} - storageClass: {{ . }} - {{- end }} - monitoring: - enablePodMonitor: {{ .Values.postgres.podMonitor }} - postgresql: - parameters: - max_connections: {{ .Values.postgres.maxConnections | quote }} - bootstrap: - initdb: - database: {{ .Values.postgres.database }} - owner: {{ .Values.postgres.owner }} - {{- if .Values.postgres.backup.enabled }} - backup: - volumeSnapshot: - className: {{ required "postgres.backup.volumeSnapshotClassName is required when backup is enabled" .Values.postgres.backup.volumeSnapshotClassName }} - {{- end }} -{{- if .Values.postgres.backup.enabled }} ---- -apiVersion: postgresql.cnpg.io/v1 -kind: ScheduledBackup -metadata: - name: wynnsource-pg-daily -spec: - schedule: {{ .Values.postgres.backup.schedule | quote }} - backupOwnerReference: self - cluster: - name: wynnsource-pg - method: volumeSnapshot -{{- end }} -{{- end }} diff --git a/deploy/chart/templates/redis.yaml b/deploy/chart/templates/redis.yaml deleted file mode 100644 index c41a8ef..0000000 --- a/deploy/chart/templates/redis.yaml +++ /dev/null @@ -1,23 +0,0 @@ -{{- if .Values.redis.enabled }} -apiVersion: redis.redis.opstreelabs.in/v1beta2 -kind: Redis -metadata: - name: wynnsource-redis - annotations: - argocd.argoproj.io/sync-wave: "0" - # Cache state is cheap but the CR owns a PVC; keep it out of prune and - # cascade deletion, consistent with the postgres cluster. - argocd.argoproj.io/sync-options: Prune=false,Delete=false -spec: - kubernetesConfig: - image: {{ .Values.redis.image }} - resources: - {{- toYaml .Values.redis.resources | nindent 6 }} - storage: - volumeClaimTemplate: - spec: - accessModes: ["ReadWriteOnce"] - resources: - requests: - storage: {{ .Values.redis.storageSize }} -{{- end }} diff --git a/deploy/chart/templates/service.yaml b/deploy/chart/templates/service.yaml deleted file mode 100644 index 7468ab9..0000000 --- a/deploy/chart/templates/service.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: wynnsource-server - annotations: - argocd.argoproj.io/sync-wave: "2" - labels: - app: wynnsource-server -spec: - type: ClusterIP - selector: - app: wynnsource-server - ports: - - port: {{ .Values.service.port }} - targetPort: http - protocol: TCP - name: http diff --git a/deploy/chart/values.yaml b/deploy/chart/values.yaml deleted file mode 100644 index ba227c1..0000000 --- a/deploy/chart/values.yaml +++ /dev/null @@ -1,80 +0,0 @@ -# Safe defaults only. Anything cluster-specific (domains, storage classes, -# snapshot classes, image tags) is supplied by the deploying repository. - -image: - repository: ghcr.io/wynnsource/wynnsource-server - # Required. Set by the deploying repo; kept current by argocd-image-updater. - tag: "" - -replicaCount: 1 - -# Old ReplicaSets retained for rollback (Deployment default is 10). -revisionHistoryLimit: 3 - -# Rendered into the wynnsource-config ConfigMap, consumed via envFrom. -# A change here rolls the deployment (checksum annotation). -config: - LEVEL: INFO - MAX_IP_RECORDS: "10" - # One JSON object per log line (trace_id/span_id included) for the log pipeline. - LOG_JSON: "true" - -# Extra environment variables appended to the server container -# (e.g. sentry environment / profiling rate / version suffix). -extraEnv: [] - -# OpenTelemetry traces and metrics, pushed over OTLP/HTTP. Off while -# otlpEndpoint is empty. Each pod reports service.instance.id = its pod name, -# so replicas never share a metric series. -telemetry: - # e.g. http://otel-collector.monitoring.svc:4318 - otlpEndpoint: "" - # deployment.environment.name resource attribute (e.g. prod, dev). - environment: "" -# Secret consumed via envFrom; managed outside the chart (SealedSecret etc.). -existingSecret: wynnsource-secrets - -resources: - requests: - cpu: 100m - memory: 256Mi - limits: - cpu: 1000m - memory: 1024Mi - -service: - port: 8000 - -postgres: - enabled: true - database: wcs_db - owner: wynnsource - storageSize: 5Gi - # Empty = cluster default StorageClass. - storageClass: "" - maxConnections: "100" - # Prometheus PodMonitor for postgres metrics (cluster scrapes it when - # unlabeled PodMonitor discovery is enabled). - podMonitor: false - resources: - requests: - cpu: 50m - memory: 256Mi - backup: - enabled: false - # VolumeSnapshotClass name provided by the cluster. - volumeSnapshotClassName: "" - # Six-field cron; CNPG counts seconds first. - schedule: "0 15 3 * * *" - -redis: - enabled: true - image: redis:7-alpine - storageSize: 1Gi - resources: - requests: - cpu: 50m - memory: 64Mi - limits: - cpu: 200m - memory: 128Mi diff --git a/deploy/docker-compose.yaml b/deploy/docker-compose.yaml new file mode 100644 index 0000000..771a35b --- /dev/null +++ b/deploy/docker-compose.yaml @@ -0,0 +1,88 @@ +# Single-instance deployment of the published image: server, one-shot +# migration, PostgreSQL and Redis on one host. See README.md in this folder. +# +# cp .env.example .env # then set POSTGRES_PASSWORD and WCS_ADMIN_TOKEN +# docker compose up -d +name: wynnsource + +x-postgres-env: &postgres-env + POSTGRES_HOST: db + POSTGRES_PORT: "5432" + POSTGRES_DB: ${POSTGRES_DB:-wcs_db} + POSTGRES_USER: ${POSTGRES_USER:-wynnsource} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env} + +x-server-image: &server-image + image: ghcr.io/wynnsource/wynnsource-server:${WCS_VERSION:-latest} + # Published images are linux/amd64 only; ARM hosts run them under emulation. + platform: linux/amd64 + # To run your own checkout instead of the published image: + # build: .. + +services: + server: + <<: *server-image + env_file: .env + environment: + <<: *postgres-env + REDIS_HOST: redis + REDIS_PORT: "6379" + # Loopback only: put a TLS reverse proxy in front (see README.md). + ports: + - "${WCS_BIND:-127.0.0.1}:${WCS_PORT:-8000}:8000" + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/readyz', timeout=3)"] + interval: 15s + timeout: 5s + start_period: 20s + retries: 3 + depends_on: + migration: + condition: service_completed_successfully + redis: + condition: service_healthy + restart: unless-stopped + + # Applies database migrations, then exits. The server starts only after it + # succeeds, so every `docker compose up` after a version bump migrates first. + migration: + <<: *server-image + command: ["uv", "run", "--no-dev", "alembic", "upgrade", "head"] + env_file: .env + environment: + <<: *postgres-env + depends_on: + db: + condition: service_healthy + restart: "no" + + db: + image: postgres:18-alpine + environment: + POSTGRES_DB: ${POSTGRES_DB:-wcs_db} + POSTGRES_USER: ${POSTGRES_USER:-wynnsource} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env} + # PostgreSQL 18 images keep data under /var/lib/postgresql/18/docker. + volumes: + - db-data:/var/lib/postgresql + healthcheck: + test: ["CMD-SHELL", "pg_isready -U \"$$POSTGRES_USER\" -d \"$$POSTGRES_DB\""] + interval: 5s + timeout: 5s + retries: 10 + restart: unless-stopped + + redis: + image: redis:7-alpine + volumes: + - redis-data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 10 + restart: unless-stopped + +volumes: + db-data: + redis-data: diff --git a/docker-compose.yaml b/docker-compose.yaml deleted file mode 100644 index 45f2508..0000000 --- a/docker-compose.yaml +++ /dev/null @@ -1,60 +0,0 @@ -services: - app: - build: . - ports: - - "8000:8000" - environment: - - POSTGRES_HOST=db - - REDIS_HOST=redis - - POSTGRES_USER=user - - POSTGRES_PASSWORD=password - - POSTGRES_DB=wcs_db - - WCS_ADMIN_TOKEN=example_admin_token - depends_on: - migration: - condition: service_completed_successfully - redis: - condition: service_started - restart: unless-stopped - networks: - - wcs_internal - - migration: - build: . - command: uv run --no-dev alembic upgrade head - environment: - - POSTGRES_HOST=db - - REDIS_HOST=redis - - POSTGRES_USER=user - - POSTGRES_PASSWORD=password - - POSTGRES_DB=wcs_db - depends_on: - db: - condition: service_healthy - networks: - - wcs_internal - - db: - image: postgres:15-alpine - environment: - POSTGRES_DB: wcs_db - POSTGRES_USER: user - POSTGRES_PASSWORD: password - healthcheck: - test: ["CMD-SHELL", "pg_isready -U user -d wcs_db"] - interval: 5s - timeout: 5s - retries: 5 - restart: unless-stopped - networks: - - wcs_internal - - redis: - image: redis:7-alpine - restart: unless-stopped - networks: - - wcs_internal - -networks: - wcs_internal: - driver: bridge \ No newline at end of file