Microservices
Each service is its own deployable unit with its own satusky.toml. They are deployed separately, scaled separately, and updated independently. The only coupling between user-service and order-service in this guide is a URL: order-service reads the address of user-service from an environment variable you inject after the first deploy.
Project layout
Section titled “Project layout”services/ user-service/ satusky.toml order-service/ satusky.tomlEach service carries its own satusky.toml. No shared config file — just two independent app definitions.
satusky.toml format
Section titled “satusky.toml format”All configuration lives in a single [app] section. There are no separate [build], [resources], or [network] sections.
services/user-service/satusky.toml
[app] name = "user-service" port = 8081 cpu = "0.25" memory = "128Mi"services/order-service/satusky.toml
[app] name = "order-service" port = 8082 cpu = "0.25" memory = "128Mi"Step 1: Deploy user-service first
Section titled “Step 1: Deploy user-service first”Always deploy the dependency before the dependent. order-service needs a URL to call — you cannot inject that URL until user-service is live and has a domain assigned.
cd services/user-service1ctl deploy --image nginx:alpine --machine compute-main-01 --waitYou will see output like this:
💡 Using pre-built image: nginx:alpineStep 2/5: Creating/updating deployment user-service ✓Step 3/5: Configuring services user-service ✓Step 4/5: Setting up environment and storage user-service ✓💡 No existing public route found for deployment 4b57f3c8-c51e-48c0-b0fb-f6fcebccf42d, will create new one: No public route found💡 Generated new domain: excitedowl-twzwpdg.satusky.comStep 5/5: Configuring public routing and dependencies user-service ✓✅ 🚀 Deployment for user-service is successful! Your app is live at: https://excitedowl-twzwpdg.satusky.comDeployment ID: 4b57f3c8-c51e-48c0-b0fb-f6fcebccf42d💡 Waiting for deployment to become healthy...💡 Deployment status: NotReady (0 pct)✅ Deployment is healthy — pods RunningA few things to notice:
- Step 1/5 is skipped when you pass
--image— cloud build is bypassed entirely. - The domain is random — something like
excitedowl-twzwpdg.satusky.com. It is not derived from the app name. --waitblocks until pods reach Running state. Without it, the command exits immediately after the deploy request is accepted, and pods may not be ready yet.
What gets created in Kubernetes:
kubectl -n <your-namespace> get deployment user-service# NAME READY UP-TO-DATE AVAILABLE AGE# user-service 1/1 1 1 11s
kubectl -n <your-namespace> get service user-service# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE# user-service ClusterIP 10.107.210.25 <none> 8081/TCP 11s
kubectl -n <your-namespace> get httproute user-service# NAME HOSTNAMES AGE# user-service excitedowl-twzwpdg.satusky.com 11s
kubectl -n <your-namespace> get pods -l app=user-service# NAME READY STATUS RESTARTS AGE# user-service-6fc4c7db-wnvq9 1/1 Running 0 11sStep 2: Capture the user-service URL
Section titled “Step 2: Capture the user-service URL”1ctl -o json deploy get returns the full deployment spec including the domain field. Note that -o json is a global flag — it must go before the subcommand:
# Correct1ctl -o json deploy get
# Wrong — will not work1ctl deploy get -o jsonCapture the domain into a shell variable:
cd services/user-serviceUSER_SERVICE_URL=$(1ctl -o json deploy get | jq -r '.domain')echo $USER_SERVICE_URL# https://excitedowl-twzwpdg.satusky.comThe domain field in deploy get is the full https:// URL. This is what you pass to order-service.
Important: domain does not appear in 1ctl -o json deploy list. The list response only includes deployment_id, app_label, status, replicas, and other summary fields. Always use deploy get to retrieve the domain.
Step 3: Deploy order-service
Section titled “Step 3: Deploy order-service”You must deploy order-service before you can set its environment variables or secrets. If you try to run env create or secret create before the deployment exists, you will get:
❌ app "order-service" not found in namespace <your-namespace>Run '1ctl deploy' first or pass --deployment-idDeploy order-service first:
cd services/order-service1ctl deploy --image nginx:alpine --machine compute-main-01 --wait💡 Using pre-built image: nginx:alpineStep 2/5: Creating/updating deployment order-service ✓Step 3/5: Configuring services order-service ✓Step 4/5: Setting up environment and storage order-service ✓💡 No existing public route found for deployment 05ab6f6d-c179-48bf-8cb8-8c44fc48e7b6, will create new one: No public route found💡 Generated new domain: quietwolf-ubt9ik9.satusky.comStep 5/5: Configuring public routing and dependencies order-service ✓✅ 🚀 Deployment for order-service is successful! Your app is live at: https://quietwolf-ubt9ik9.satusky.comDeployment ID: 05ab6f6d-c179-48bf-8cb8-8c44fc48e7b6💡 Waiting for deployment to become healthy...💡 Deployment status: NotReady (0 pct)✅ Deployment is healthy — pods RunningWhat gets created in Kubernetes:
kubectl -n <your-namespace> get deployment order-service# NAME READY UP-TO-DATE AVAILABLE AGE# order-service 1/1 1 1 20s
kubectl -n <your-namespace> get pods -l app=order-service# NAME READY STATUS RESTARTS AGE# order-service-84cdff5c7c-sj4wt 1/1 Running 0 9sStep 4: Wire the URL into order-service
Section titled “Step 4: Wire the URL into order-service”Now that order-service exists, inject the user-service URL as an environment variable:
cd services/order-service1ctl env create --env USER_SERVICE_URL="$USER_SERVICE_URL"✅ Environment order-service created successfullyorder-service will read USER_SERVICE_URL at startup. No hardcoded addresses in source code.
Step 5: Set secrets per service
Section titled “Step 5: Set secrets per service”Secrets are scoped to a deployment by app name. Setting JWT_SECRET on user-service has zero effect on order-service’s secret store.
# user-service needs a JWT secretcd services/user-service1ctl secret create --kv JWT_SECRET=supersecret-jwt-value✅ Secret user-service created successfully# order-service needs a database connection stringcd services/order-service✅ Secret order-service created successfullyHow secrets are stored in Kubernetes: Key names are lowercased and underscores converted to dashes. JWT_SECRET becomes jwt-secret in the K8s Secret object; your application reads it via the original name as injected by the platform.
You can verify what was stored:
kubectl -n <your-namespace> get secret user-service-secrets -o jsonpath='{.data}' \ | python3 -c "import sys,json,base64; d=json.load(sys.stdin); [print(k,'=',base64.b64decode(v).decode()) for k,v in d.items()]"# jwt-secret = supersecret-jwt-value
kubectl -n <your-namespace> get secret order-service-secrets -o jsonpath='{.data}' \ | python3 -c "import sys,json,base64; d=json.load(sys.stdin); [print(k,'=',base64.b64decode(v).decode()) for k,v in d.items()]"# database-url = postgres://user:[email protected]:5432/ordersStep 6: Verify both services are running
Section titled “Step 6: Verify both services are running”1ctl deploy listDEPLOYMENT ID HOSTNAMES TYPE STATUS CREATED──────────────────────────────────── ──────────────────────────────────── ────────── ───────── ──────────4b57f3c8-c51e-48c0-b0fb-f6fcebccf42d c7d2a022-07bf-41f3-b51c-5ebb27365fc4 production completed 2 minutes ago05ab6f6d-c179-48bf-8cb8-8c44fc48e7b6 c7d2a022-07bf-41f3-b51c-5ebb27365fc4 production completed just nowUse the JSON output to filter programmatically. Note that domain is not in the list response — it only appears in deploy get:
1ctl -o json deploy list | python3 -c "import sys, jsonfor d in json.load(sys.stdin): print(d['app_label'], d['status'])"# user-service completed# order-service completedStep 7: Check release history
Section titled “Step 7: Check release history”cd services/user-service1ctl deploy releasesVERSION IMAGE STATUS DEPLOYED─────── ──────────── ────── ────────1 nginx:alpine active 2 minutes agocd services/order-service1ctl deploy releasesVERSION IMAGE STATUS DEPLOYED─────── ──────────── ────── ────────1 nginx:alpine active just nowColumn names: VERSION, IMAGE, STATUS, DEPLOYED. The current release shows active; superseded releases show superseded (not “replaced”).
Step 8: Redeploy user-service (simulate a code change)
Section titled “Step 8: Redeploy user-service (simulate a code change)”Ship a new version of user-service without touching order-service. order-service keeps running throughout.
cd services/user-service1ctl deploy --image nginx:alpine --machine compute-main-01💡 Using pre-built image: nginx:alpineStep 2/5: Creating/updating deployment user-service ✓Step 3/5: Configuring services user-service ✓Step 4/5: Setting up environment and storage user-service ✓Step 5/5: Configuring public routing and dependencies user-service ✓✅ 🚀 Deployment for user-service is successful! Your app is live at: https://excitedowl-twzwpdg.satusky.comDeployment ID: 4b57f3c8-c51e-48c0-b0fb-f6fcebccf42dThe domain does not change across redeploys. Notice there is no 💡 Generated new domain: line this time — the existing public route is reused. order-service’s USER_SERVICE_URL remains valid; no update is needed.
Check the release history to confirm the new release:
1ctl deploy releasesVERSION IMAGE STATUS DEPLOYED─────── ──────────── ────────── ────────────2 nginx:alpine active just now1 nginx:alpine superseded 2 minutes agoCheck pods to confirm the rolling update completed:
kubectl -n <your-namespace> get pods -l app=user-service# NAME READY STATUS RESTARTS AGE# user-service-676599fcf6-wj8ks 1/1 Running 0 4sStep 9: Update the service URL after destroy + recreate
Section titled “Step 9: Update the service URL after destroy + recreate”A redeploy keeps the same domain. But if user-service was destroyed and recreated from scratch, it gets a new random domain. In that case, update order-service’s env var and restart its pods.
You can run deploy get for any service from any directory by passing the full path to its config:
cd services/order-serviceNEW_URL=$(1ctl -o json deploy get --config /path/to/services/user-service/satusky.toml | jq -r '.domain')
# Update order-service's env1ctl env create --env USER_SERVICE_URL="$NEW_URL"✅ Environment order-service created successfully# Restart pods to pick up the new value1ctl deploy restart💡 Initiating rolling restart for deployment 05ab6f6d-c179-48bf-8cb8-8c44fc48e7b6...✅ Rolling restart initiated. Pods are being replaced one by one.💡 Use '1ctl deploy status --deployment-id 05ab6f6d-c179-48bf-8cb8-8c44fc48e7b6' to monitor progress.deploy restart does a rolling restart — it replaces pods one by one without downtime. The third line gives you the exact command to monitor progress.
Step 10: Clean up
Section titled “Step 10: Clean up”Destroy both services when you are done. The -y flag skips the confirmation prompt.
cd services/user-service1ctl deploy destroy -y💡 Destroying deployment 4b57f3c8-c51e-48c0-b0fb-f6fcebccf42d...✅ Deployment 4b57f3c8-c51e-48c0-b0fb-f6fcebccf42d destroyed successfullycd services/order-service1ctl deploy destroy -y💡 Destroying deployment 05ab6f6d-c179-48bf-8cb8-8c44fc48e7b6...✅ Deployment 05ab6f6d-c179-48bf-8cb8-8c44fc48e7b6 destroyed successfullydeploy destroy removes every associated Kubernetes resource: Deployment, Service, public route, ConfigMap, and Secret. Verify everything is gone:
kubectl -n <your-namespace> get deployment user-service# Error from server (NotFound): deployments.apps "user-service" not found
kubectl -n <your-namespace> get deployment order-service# Error from server (NotFound): deployments.apps "order-service" not found
kubectl -n <your-namespace> get service user-service# Error from server (NotFound): services "user-service" not found
kubectl -n <your-namespace> get service order-service# Error from server (NotFound): services "order-service" not found
kubectl -n <your-namespace> get httproute user-service# Error from server (NotFound): httproutes.gateway.networking.k8s.io "user-service" not found
kubectl -n <your-namespace> get httproute order-service# Error from server (NotFound): httproutes.gateway.networking.k8s.io "order-service" not found
kubectl -n <your-namespace> get secret user-service-secrets# Error from server (NotFound): secrets "user-service-secrets" not found
kubectl -n <your-namespace> get secret order-service-secrets# Error from server (NotFound): secrets "order-service-secrets" not foundKey facts to remember
Section titled “Key facts to remember”Deploy the dependency first — always use --wait.
order-service cannot have its env or secrets configured until it is deployed. If you call env create or secret create before deploying, you get an error. Deploy both services first, then configure their connections.
env create and secret create require the deployment to exist first.
The error is explicit: ❌ app "order-service" not found — Run '1ctl deploy' first. The correct order is: deploy → env create → secret create → restart.
The domain is stable across redeploys.
When you redeploy the same app (same satusky.toml, same app name), the existing public route is reused and the domain stays identical. The 💡 Generated new domain: message only appears on the very first deploy. order-service’s USER_SERVICE_URL does not need updating after a redeploy.
The domain changes only after destroy + recreate. Destroying a deployment deletes the public route. The next deploy generates a new random domain. That is the only time you need to update dependent services.
domain is in deploy get, not deploy list.
1ctl -o json deploy list returns summary fields only — no domain. Use 1ctl -o json deploy get to retrieve the domain for a specific deployment.
-o json is a global flag — it goes before the subcommand.
# Correct1ctl -o json deploy list1ctl -o json deploy get
# Wrong1ctl deploy list -o json1ctl deploy get -o jsonUse --config to manage a service from a different directory.
# From the order-service directory, get user-service's domain1ctl -o json deploy get --config /path/to/user-service/satusky.toml | jq -r '.domain'Secrets are scoped per deployment. You cannot accidentally share a secret between services by using the same key name. Each service’s secret store is independent.