Skip to content

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.

services/
user-service/
satusky.toml
order-service/
satusky.toml

Each service carries its own satusky.toml. No shared config file — just two independent app definitions.


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"

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.

Terminal window
cd services/user-service
1ctl deploy --image nginx:alpine --machine compute-main-01 --wait

You will see output like this:

💡 Using pre-built image: nginx:alpine
Step 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.com
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.com
Deployment ID: 4b57f3c8-c51e-48c0-b0fb-f6fcebccf42d
💡 Waiting for deployment to become healthy...
💡 Deployment status: NotReady (0 pct)
✅ Deployment is healthy — pods Running

A 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.
  • --wait blocks 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:

Terminal window
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 11s

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:

Terminal window
# Correct
1ctl -o json deploy get
# Wrong — will not work
1ctl deploy get -o json

Capture the domain into a shell variable:

Terminal window
cd services/user-service
USER_SERVICE_URL=$(1ctl -o json deploy get | jq -r '.domain')
echo $USER_SERVICE_URL
# https://excitedowl-twzwpdg.satusky.com

The 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.


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-id

Deploy order-service first:

Terminal window
cd services/order-service
1ctl deploy --image nginx:alpine --machine compute-main-01 --wait
💡 Using pre-built image: nginx:alpine
Step 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.com
Step 5/5: Configuring public routing and dependencies order-service ✓
✅ 🚀 Deployment for order-service is successful! Your app is live at: https://quietwolf-ubt9ik9.satusky.com
Deployment ID: 05ab6f6d-c179-48bf-8cb8-8c44fc48e7b6
💡 Waiting for deployment to become healthy...
💡 Deployment status: NotReady (0 pct)
✅ Deployment is healthy — pods Running

What gets created in Kubernetes:

Terminal window
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 9s

Now that order-service exists, inject the user-service URL as an environment variable:

Terminal window
cd services/order-service
1ctl env create --env USER_SERVICE_URL="$USER_SERVICE_URL"
✅ Environment order-service created successfully

order-service will read USER_SERVICE_URL at startup. No hardcoded addresses in source code.


Secrets are scoped to a deployment by app name. Setting JWT_SECRET on user-service has zero effect on order-service’s secret store.

Terminal window
# user-service needs a JWT secret
cd services/user-service
1ctl secret create --kv JWT_SECRET=supersecret-jwt-value
✅ Secret user-service created successfully
Terminal window
# order-service needs a database connection string
cd services/order-service
1ctl secret create --kv DATABASE_URL=postgres://user:[email protected]:5432/orders
✅ Secret order-service created successfully

How 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:

Terminal window
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/orders

Terminal window
1ctl deploy list
DEPLOYMENT ID HOSTNAMES TYPE STATUS CREATED
──────────────────────────────────── ──────────────────────────────────── ────────── ───────── ──────────
4b57f3c8-c51e-48c0-b0fb-f6fcebccf42d c7d2a022-07bf-41f3-b51c-5ebb27365fc4 production completed 2 minutes ago
05ab6f6d-c179-48bf-8cb8-8c44fc48e7b6 c7d2a022-07bf-41f3-b51c-5ebb27365fc4 production completed just now

Use the JSON output to filter programmatically. Note that domain is not in the list response — it only appears in deploy get:

Terminal window
1ctl -o json deploy list | python3 -c "
import sys, json
for d in json.load(sys.stdin):
print(d['app_label'], d['status'])
"
# user-service completed
# order-service completed

Terminal window
cd services/user-service
1ctl deploy releases
VERSION IMAGE STATUS DEPLOYED
─────── ──────────── ────── ────────
1 nginx:alpine active 2 minutes ago
Terminal window
cd services/order-service
1ctl deploy releases
VERSION IMAGE STATUS DEPLOYED
─────── ──────────── ────── ────────
1 nginx:alpine active just now

Column 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.

Terminal window
cd services/user-service
1ctl deploy --image nginx:alpine --machine compute-main-01
💡 Using pre-built image: nginx:alpine
Step 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.com
Deployment ID: 4b57f3c8-c51e-48c0-b0fb-f6fcebccf42d

The 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:

Terminal window
1ctl deploy releases
VERSION IMAGE STATUS DEPLOYED
─────── ──────────── ────────── ────────────
2 nginx:alpine active just now
1 nginx:alpine superseded 2 minutes ago

Check pods to confirm the rolling update completed:

Terminal window
kubectl -n <your-namespace> get pods -l app=user-service
# NAME READY STATUS RESTARTS AGE
# user-service-676599fcf6-wj8ks 1/1 Running 0 4s

Step 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:

Terminal window
cd services/order-service
NEW_URL=$(1ctl -o json deploy get --config /path/to/services/user-service/satusky.toml | jq -r '.domain')
# Update order-service's env
1ctl env create --env USER_SERVICE_URL="$NEW_URL"
✅ Environment order-service created successfully
Terminal window
# Restart pods to pick up the new value
1ctl 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.


Destroy both services when you are done. The -y flag skips the confirmation prompt.

Terminal window
cd services/user-service
1ctl deploy destroy -y
💡 Destroying deployment 4b57f3c8-c51e-48c0-b0fb-f6fcebccf42d...
✅ Deployment 4b57f3c8-c51e-48c0-b0fb-f6fcebccf42d destroyed successfully
Terminal window
cd services/order-service
1ctl deploy destroy -y
💡 Destroying deployment 05ab6f6d-c179-48bf-8cb8-8c44fc48e7b6...
✅ Deployment 05ab6f6d-c179-48bf-8cb8-8c44fc48e7b6 destroyed successfully

deploy destroy removes every associated Kubernetes resource: Deployment, Service, public route, ConfigMap, and Secret. Verify everything is gone:

Terminal window
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 found

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.

Terminal window
# Correct
1ctl -o json deploy list
1ctl -o json deploy get
# Wrong
1ctl deploy list -o json
1ctl deploy get -o json

Use --config to manage a service from a different directory.

Terminal window
# From the order-service directory, get user-service's domain
1ctl -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.