# Scale the whole chart down without uninstalling it: scaleDown: true renders # every Deployment/StatefulSet with replicas: 0 (and suspends CronJobs) and # takes precedence over any per-workload replicas value. PVCs, Secrets and # Services stay in place; scaleDown: false brings the workloads back up. scaleDown: false # Routing table of the router: which client-facing hostname is proxied to which # backend Minecraft server. Required and must not be empty - a router without # routes is not operable, and there is no sensible default. # Every entry needs "domain" and "internalUrl"; the optional "port" overrides # server.backendPort for that single backend: # mappings: # - domain: mc.example.com # internalUrl: survival-service.minecraft.svc.cluster.local # - domain: creative.example.com # internalUrl: creative-service.minecraft.svc.cluster.local # port: 25566 # Since chart 2.0.0 a change here rolls the router pod: the deployment carries # a checksum/config annotation over the rendered ConfigMap. That restart is # required for the change to take effect at all - the config is mounted with # subPath and mc-router only reads it at startup - and costs a few seconds of # proxy downtime, during which clients cannot connect. mappings: null server: # Replica count for this workload; 0 is allowed and scales it down. # The chart-global scaleDown flag takes precedence. replicas: 1 # nodePort of the router Service. 25565 is the Minecraft default port and is # deliberately outside the Kubernetes default node-port range (30000-32767) - # the cluster runs an extended --service-node-port-range. Do NOT "fix" this # into the default range: the Minecraft clients connect to this exact port. outwardPort: 25565 # Port the router listens on for Minecraft client connections (mc-router # --port); used as containerPort and as the Service's cluster-internal port. port: 25565 # Default port of the backend Minecraft servers, used for every mapping that # does not carry its own "port". backendPort: 25565 # Port of the mc-router HTTP API (--api-binding). Not exposed by the Service; # only the probes talk to it. apiPort: 8080 resources: # Limits are optional: memory and ephemeral-storage limits default to # limitFactor x the request, an explicitly set limit always wins, and a # CPU limit is only set when configured explicitly under "limits". limitFactor: 3 requests: cpu: 100m memory: 50Mi ephemeral-storage: 50Mi # The probes query the mc-router API. Even in 1.46.4 there is still no # /health or /healthz endpoint - the API server serves GET/POST /routes, # POST /defaultRoute, DELETE /routes/{addr}, /vars and /metrics and nothing # else - so GET /routes stays the health signal. It answers with the live # routing table, and the API server is only started after the routes config # has been read and the Minecraft listener is up, so a 200 here means the # router is actually serving clients. # The "Accept: application/json" header was mandatory in 1.25.1 (the API # router matched on it and answered 404 without it); mc-router 1.32.2 dropped # that matcher, so from there on the header is optional. It is kept because # it is the correct request for a JSON endpoint and the response is identical # with and without it - removing it would be churn without any gain. livenessProbe: httpGet: path: /routes port: api-port httpHeaders: - name: Accept value: application/json periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 3 readinessProbe: httpGet: path: /routes port: api-port httpHeaders: - name: Accept value: application/json periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 3 # The startup probe gates liveness/readiness. mc-router is a single static Go # binary that reads one config file and serves the API in well under a # second; budget = periodSeconds * failureThreshold = 30s. startupProbe: httpGet: path: /routes port: api-port httpHeaders: - name: Accept value: application/json periodSeconds: 2 timeoutSeconds: 5 failureThreshold: 15 securityContext: pod: runAsNonRoot: true # Up to 1.45.x the itzg/mc-router image was built FROM scratch and # declared no user, so it would have run as root. Since 1.46.0 it is # based on cgr.dev/chainguard/static and declares USER 65532. The chart # keeps pinning its own unprivileged uid/gid regardless, so the runtime # identity does not silently change with the base image; the statically # linked binary needs no particular uid/gid. runAsUser: 10000 runAsGroup: 10000 # Keeps the mounted routing-config ConfigMap readable for that gid. fsGroup: 10000 fsGroupChangePolicy: Always # RuntimeDefault applies the container runtime's default seccomp filter, # which blocks the syscalls a normal workload never makes. It sits in the # pod context on purpose: from here it covers every container in the pod # including init containers, while a container-level seccompProfile would # have to be repeated for each one (and silently missed on the next one # added). A container that genuinely needs a different filter can still # override it under securityContext.container, which wins over this value. seccompProfile: type: RuntimeDefault container: allowPrivilegeEscalation: false # No deviation needed: the image ships nothing but the static binary and # a CA bundle, mc-router writes nothing, and the only mount is the # read-only routing config. readOnlyRootFilesystem: true runAsNonRoot: true capabilities: drop: - ALL add: [] # Extra environment variables for the router container, appended to the ones # the chart always sets (ROUTES_CONFIG). This is also how every mc-router # option that the chart does not model explicitly is set: mc-router parses # its flags with go-flagsfiller, so every "--some-flag" is equally settable # as "SOME_FLAG" (that is exactly how ROUTES_CONFIG maps to --routes-config). # Useful examples in 1.46.4: LOG_LEVEL (debug/info/warn/error), # BACKEND_DIAL_TIMEOUT, CONNECTION_RATE_LIMIT. # Values are rendered through tpl and support the value, secretKeyRef and # configMapKeyRef forms: # - name: SOME_ENV # value: "{{ .Release.Name }}" # - name: FROM_SECRET # valueFrom: # secretKeyRef: # name: "some-secret" # key: "someKey" # - name: FROM_CONFIGMAP # valueFrom: # configMapKeyRef: # name: "some-configmap" # key: "someKey" env: [] # --------------------------------------------------------------------------- # NetworkPolicy (Gitea #5). On by default. The reference implementation with # the full reasoning is template-chart; the convention is in README, "Chart # style guidelines", and the operator's view on this chart's wiki page. # --------------------------------------------------------------------------- networkPolicy: # WHAT THIS POLICY ALLOWS: inbound the proxy port (server.port, OPEN - it is # a NodePort and the senders are players on the internet) and the API port # (the probe path, open inside the cluster). Outbound: DNS plus the backend # ports DERIVED from the routing table - server.backendPort and the "port" # override of every mapping that sets one. # # So this is one of the few egress caps here that follows the configuration # instead of guessing at it: add a backend on a non-standard port and the # policy moves with it. # # WHAT IT DOES NOT DO: the rule permits those PORTS to any destination, not # those SERVICES. mappings name hosts (internalUrl), and a NetworkPolicy # matches CIDRs and selectors but never hostnames - so anything else # listening on a backend port is reachable through this rule too. # # And the proxy port carries the POD port, not server.outwardPort: a # NetworkPolicy is evaluated after kube-proxy has translated the NodePort, so # the NodePort number would match nothing while looking strict. enabled: true # Namespaces whose pods may reach this workload, in addition to the release's # own namespace, which is always allowed. Empty means: own namespace only. # # Matched on the "kubernetes.io/metadata.name" label Kubernetes sets on every # namespace itself, so no labelling work is needed on a namespace this chart # does not own - and no typo protection either: a name that does not exist # matches nothing and is silently ineffective. Note the granularity: an entry # opens the port to EVERY pod in that namespace. allowedNamespaces: [] # Source CIDRs allowed to reach the API port (server.apiPort), which is the port all three probes address. # # EMPTY MEANS OPEN, NOT BLOCKED. With no entry that rule is rendered WITHOUT a # "from" selector, so the port is reachable from anywhere the CNI routes to - # inside the cluster, since it carries no NodePort. # # That is the deliberate choice between two bad defaults. The kubelet's probes # originate from the IP of the WORKER NODE running the pod, an address this # chart cannot know and must not guess: a CIDR that excludes the real node # blocks every probe, the pod never becomes Ready, the rollout times out, and # the symptom reads as a broken application rather than as a policy. # # Narrow it by listing the worker node IPs (e.g. ["10.0.1.11/32"]) - and mind # the companion trap: once this list is non-empty the rule IS restricted, so # everything else that needs the port has to be in it. The chart adds the own # namespace and allowedNamespaces to the same rule for exactly that reason. probeSources: [] egress: # Additional outbound TCP ports. DNS (UDP and TCP 53) is always allowed and # has no switch - a knob whose only other setting breaks every name lookup # in the pod is a trap, not a feature. # # PORTS, NOT DESTINATIONS, and that is a limit of the object rather than # laziness: a NetworkPolicy matches CIDRs and selectors and never hostnames, # so "only to the database host" cannot be written at all. Each entry # permits its port to ANY destination. What the cap still buys is real: a # compromised pod cannot open arbitrary outbound connections. # # Normally empty: the backend ports already come from mappings. Add a port # only for something outside the routing table - a status API, for # instance. extraPorts: []