Railshot
AI deployment & operations platform
Deploy a locally built web app to prepared cloud and on-premises environments, then run it.
An AI deployment and operations platform that takes a web app built locally to prepared cloud or on-premises environments and keeps running it.
Operations and customer app clusters are separate, and the public path is kept apart from deployment control.
In the operations K3s, the dashboard, product API and build controller are placed on platform-role nodes and runner Jobs on a build-role node, while Argo CD handles deployment. Customer apps run in a designated namespace of a separate single-node K3s + Cilium cluster for each registered target.
Build → publish image
The product API requests a GitHub Actions run, and the runner on the build node takes and checks the job. Only a bundle that passed the same checks is published to GHCR by a GitHub-hosted release job.
product API · cd.js → bridge.py
railshot-systemrun · apply: workflow_dispatchloop job · self-hosted runnerrailshot-deploy.ymlThe product API requests a GitHub Actions run with tenant, app, source commit and target. The API never calls a runner Job directly.
build controller · CronJob
railshot-systemrun · apply: replenish runner Jobrunner Job · hostNetwork · Docker socketrailshot-buildThe build controller CronJob (every minute, on a platform node) creates runner Jobs in the railshot-build namespace. Its Role allows only Job get, list and create plus get, update and patch on the one runner-registration Secret; it cannot create namespaces or other Secrets.
runner Job · hostNetwork · Docker socket
railshot-buildrun · apply: take the jobloop job · self-hosted runnerrailshot-deploy.ymlThe runner on the build-role node (exact hostname, NoSchedule toleration) takes the loop job by its self-hosted labels and runs it. Customer source runs in the existing restricted Docker build and check containers.
runner Job · hostNetwork · Docker socket
railshot-builddata: tested bundlerelease job · GitHub-hostedrailshot-deploy.ymlThe loop job leaves the bundle that passed the same checks, and the release job takes only that bundle.
release job · GitHub-hosted
railshot-deploy.ymldata: publish by digestGHCRA GitHub-hosted release job (ubuntu-24.04) publishes the tested bundle’s image to GHCR without rebuilding it. Image publishing never happens inside the customer app cluster.
Config Git → Argo → customer namespace
Config Git carries the desired resources and GHCR the image bytes. Argo CD applies only to the registered customer namespace.
product API · cd.js → bridge.py
railshot-systemdata: workload commit · pushconfig Git repositorycd.js runs bridge.py, which commits and pushes workload.json only when the publication record and the registered target agree, then reads the remote revision back. Git carries the desired resources; GHCR carries the image bytes.
config Git repositorydata: revisionArgo CD · Application
argocdThe Application reads only its restricted config repository, path and revision.
product API · cd.js → bridge.py
railshot-systemrun · apply: Application syncArgo CD · ApplicationargocdThe named Application is set by server-side apply, and a sync of one exact revision is requested without prune. The product API’s railshot-product ServiceAccount holds both the operator-installed registration bootstrap (create for Secrets, AppProjects and Applications in argocd; get, update and escalate on named Roles) and get and patch grants appended by exact name for each validated target. Kubernetes create cannot be limited by name, so the product’s input validation also keeps the names in scope.
Argo CD · Application
argocdrun · apply: apply to namespace<app namespace>Argo CD applies to the designated namespace in the customer cluster with the registered cluster Secret’s credential. The AppProject allows only namespaced kinds and no cluster-scoped resources.
Deployment · Pod · digest image
<app namespace>data: pull by digestGHCRThe published digest image is pulled with a pull Secret referenced by name in the same namespace. Secret values never enter the artifact or config Git.
Public request path
App requests go provider L7 entry → node NodePort → Service → app Pod. Cilium’s policy enforcement is a separate control, not a hop that carries the request. The way into the operations dashboard is a separate path.
App userdata: HTTPS 443Provider L7 entry
App users reach the provider’s L7 entry by the registered hostname. Each provider implements its own entry.
Provider L7 entrydata: NodePortNodePort Service · Local
<app namespace>The entry forwards to the node’s private address and NodePort. The NodePort is the one allocated at target registration.
NodePort Service · Local
<app namespace>data: to the PodDeployment · Pod · digest image<app namespace>The Service sends the request to the app Pod (Deployment) on the same node. With externalTrafficPolicy Local, the source address reaches the NetworkPolicy’s registered-CIDR check unchanged.
Cilium · Helm · VXLAN
kube-systemrun · apply: enforce policyNetworkPolicy · listed CIDRs<app namespace>Cilium enforces the NetworkPolicy: ingress only from the registered CIDRs to the app port; no egress by default, except an approved database address. Cloud firewalls and host rules still matter.
Railshot userdata: dashboard · APIdashboard · nginx
railshot-systemUsers come through the operations entry to the nginx dashboard, and nginx fronts the product API. The dashboard Service is a NodePort only when an approved node port is given; the API stays ClusterIP.
Verify · observe
The deployment verdict rests only on Argo status and the public URL check. Pod diagnostics, current logs and operating metrics are separate signals that never change it.
product API · cd.js → bridge.py
railshot-systemobserve: revision · health · imagesArgo CD · ApplicationargocdDeployed requires the Application’s sync revision, Synced and Healthy, operation Succeeded, and the image references recorded in the Argo Application status equal to the reviewed Deployment’s digest images. It is not a direct Pod imageID gate, and a green Argo status alone does not establish the match.
product API · cd.js → bridge.py
railshot-systemobserve: public HTTPS checkProvider L7 entryThe registered public URL is checked without redirects. Succeeded is recorded only with both the Argo verdict and this check.
product API · cd.js → bridge.py
railshot-systemobserve: Pod diagnostic · not the verdictDeployment · Pod · digest image<app namespace>After the Argo verdict and the public check, the Deployment, ReplicaSets and Pods are read separately with the registered credential, and imageID and readiness are appended as a ready, progressing or unavailable diagnostic. A failed or differing diagnostic never changes the deployed or succeeded verdict.
product API · cd.js → bridge.py
railshot-systemobserve: log tail · 100 lines per PodDeployment · Pod · digest image<app namespace>Only for the currently deployed operation, with the trusted config and the registered credential. For each current Pod (at most three), the running image digest must equal the expected digest; then up to 100 redacted lines per Pod are read within a byte limit. An older attempt answers superseded; without configuration, not_configured. This is not log aggregation.
Observer VMobserve: scrape · NodePortsrailshot-observability
Prometheus on the observer VM scrapes node-exporter and kube-state-metrics through two firewall-restricted NodePorts. Missing or stale values are unknown, not zero.
Observer VMobserve: Blackbox probeProvider L7 entry
probe_success=1 means a 2xx at that probe location. It proves neither every user’s access nor the deployed version, and it never promotes published to deployed.
Operator install · register
Cluster installation, the registration bootstrap and target inputs, Secrets and the credential renewal setup are operator inputs, never part of the tenant app payload.
Operator bootstraprun · apply: render platform · Secretrailshot-system
The operator renders the platform declaration with render-platform.py and supplies the private operations config and Secrets. The remote MCP is included only when its image is supplied, the personal gateway (WireGuard) only with --personal.
Operator bootstraprun · apply: registration bootstrapAppProject · cluster Secret
argocdThe operator installs the registration bootstrap Role and an empty registrations Role in argocd and prepares the target inputs. The registration helpers the product API runs (environment.py, applications.py) then write the railshot-<target> Secret with a scoped credential (clusterResources false, allowed namespaces listed) and append the validated exact names to the registrations Role. Per-app registration binds an app-<id> namespace to an AppProject of the same name.
Operator bootstraprun · apply: install K3s · CiliumCilium · Helm · VXLAN
kube-systeminstall-k3s.sh installs a single-node K3s server (flannel, network policy, traefik and servicelb off; kube-proxy kept), and Cilium is installed as a Helm release (kubeProxyReplacement false, VXLAN, Hubble off).
renewal CronJob · every 2 h
argocdrun · apply: TokenRequest · 6 hrailshot-argocd SA · token<app namespace>The railshot-credentials CronJob requests a fresh six-hour token for the customer SA every two hours. The customer-side Role allows only token create for its own SA. Six hours is one token’s TTL, not when operator access ends.
Every connection at the same weight. Pick a flow to highlight only its path, with numbered steps.
- Solid · the direction requests, bundles, images and declarations flow
- Dashed · run requests, apply, install, credentials
- Long dash-dot · reading status, logs, probes, metrics; separate from the verdict
- Heavy black line · the selected flow; numbers give the order
- Box · a namespace, its name in the header (not a node)
- Inner chip · a component in that namespace
- Dashed box · outside the clusters
- railshot-system · product
- argocd · deployment authority
- railshot-build · host Docker authority
- GitHub · customer cluster
The October 2 placement decision in docs/architecture (two role nodes) and the public README diagram are history; this map follows the declarations and code at 645e28a. Arrows show cause and direction, not proof that every provider path is live now.
- Namespaces and nodes
- A namespace is a naming scope: neither hardware nor, by itself, a security boundary. The platform and build roles are scheduling placement by nodeSelector and taint; this map does not fix the operations cluster’s node count. Argo CD’s node placement is also a separate check.
- Build-node authority
- Runner Jobs run with hostNetwork, the host Docker socket, NET_ADMIN and root. A namespace, taint or RBAC does not remove host Docker authority, so this is neither VM-administrator isolation nor a sandbox for arbitrary code.
- Customer namespace
- Each registered target uses a non-default namespace and AppProject (for example tenant-demo, or app-<id> per app). Only one service’s Deployment (ServiceAccount token automount off), NodePort Service and NetworkPolicy, plus an optional PVC or migration Job, are created; Secrets are only referenced by name (the pull Secret and an approved DATABASE_URL), never created.
- AWS entry
- Route 53 Alias → shared ALB HTTPS 443 → customer node private address and NodePort. Many apps reuse one node, each with its own namespace, Application and NodePort.
- GCP entry
- Global EXTERNAL_MANAGED HTTPS LB → zonal GCE_VM_IP_PORT NEG → the existing VM’s private NodePort. Certificates come from Certificate Manager with DNS authorization. The module owns neither the VM nor the VPC, and one VM remains a single point of failure.
- OpenStack entry
- Amphora TLS listener 443 with Host and path L7 policies → HTTP NodePort. The default address is a private VIP; Octavia, K3s, the database and DNS are outside the module. Public reachability and on-site Amphora readiness need separate acceptance, and no on-site apply has been verified with this module yet.
- Across providers
- The three entries are separate implementations, with no automatic failover or replication between providers. The customer app cluster is an example of a registered target, not a claim that the same deployment runs on every cloud at once.
- Operator options
- The personal gateway (WireGuard) joins the platform only when the operator enables --personal; it is not a required path for customer clouds. In the supported direct mode the observer VM may share the operations control VM.