Deploy a Frontend
This guide deploys a small static single-page application (SPA). The same runtime works for React, Vue, Svelte, and other frameworks that produce static files.
You will learn how to:
- serve static files as a non-root container;
- make client-side routes such as
/dashboard/settingsfall back toindex.html; - deploy from a Dockerfile with
satusky.toml; - find and test the managed HTTPS URL.
Prerequisites
Section titled “Prerequisites”1ctlinstalled- an authenticated profile:
1ctl auth status jqandcurlfor the final HTTP checks
1. Create the example
Section titled “1. Create the example”Run the following commands in a terminal:
mkdir satusky-frontendcd satusky-frontend
cat > index.html <<'HTML'<!doctype html><html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>SatuSky frontend</title> </head> <body> <main> <h1>Frontend deployed with SatuSky</h1> <p id="route"></p> </main> <script> document.querySelector('#route').textContent = `Client route: ${location.pathname}`; </script> </body></html>HTML
cat > nginx.conf <<'NGINX'server { listen 8080; server_name _;
root /usr/share/nginx/html; index index.html;
location / { try_files $uri $uri/ /index.html; }
location /assets/ { expires 1y; add_header Cache-Control "public, immutable"; }
gzip on; gzip_types text/plain text/css application/javascript application/json;}NGINX
cat > Dockerfile <<'DOCKERFILE'FROM nginxinc/nginx-unprivileged:alpineCOPY --chown=101:101 --chmod=644 nginx.conf /etc/nginx/conf.d/default.confCOPY --chown=101:101 --chmod=644 index.html /usr/share/nginx/html/EXPOSE 8080CMD ["nginx", "-g", "daemon off;"]DOCKERFILEThe unprivileged nginx image listens on port 8080, so the container does not need root privileges. try_files returns index.html for paths that are not real files; the browser-side router can then handle the URL.
2. Configure SatuSky
Section titled “2. Configure SatuSky”Choose a unique lowercase app name and create satusky.toml:
APP="my-frontend-$(date +%s)"
cat > satusky.toml <<TOML[app] name = "$APP" port = 8080
[build] dockerfile = "Dockerfile"
[checks] health_path = "/"TOMLThe three sections have distinct responsibilities:
[app]defines the deployed application and the port on which it listens.[build]selects the Dockerfile used by the cloud builder.[checks]selects the HTTP endpoint used to confirm the application responds.
The port in satusky.toml, nginx.conf, and Dockerfile must agree.
3. Deploy
Section titled “3. Deploy”1ctl deployThe command packages the current directory, builds linux/amd64 and linux/arm64 images in the cloud, and submits the deployment. You do not need Docker installed locally.
Check the accepted deployment and its live status separately:
1ctl app get "$APP"1ctl app status "$APP"app get shows the stored deployment record and generated URL. app status
checks the current workload and public route. During initial reconciliation it
may report that the workload, route, or DNS is still pending; wait until the
DNS condition is verified before treating the hostname as public-ready.
If the container does not become healthy, inspect its logs:
1ctl logs --app "$APP" --tail 50DEPLOYMENT_ID="$(1ctl -o json app get "$APP" | jq -r '.deployment_id')"1ctl logs stream --deployment-id "$DEPLOYMENT_ID"Press Ctrl+C to stop streaming.
4. Test the public route
Section titled “4. Test the public route”Read the generated HTTPS URL from the deployment record:
URL=$(1ctl -o json app get "$APP" | jq -r '.domain')printf 'URL: %s\n' "$URL"Wait for the public route to be attached and the DNS condition to be
verified, then test both the home page and a client-side route. pending,
nxdomain, wrong_target, and error do not prove the hostname resolves to
its reserved target.
curl --fail --show-error --retry 24 --retry-delay 5 --retry-all-errors "$URL/" | grep -F 'Frontend deployed with SatuSky'curl --fail --show-error --retry 24 --retry-delay 5 --retry-all-errors "$URL/dashboard/settings" | grep -F 'Frontend deployed with SatuSky'Both requests must contain:
Frontend deployed with SatuSkyThe second response proves that nginx returned index.html for a deep link instead of a 404. The JavaScript application then reads /dashboard/settings from location.pathname.
If either command still fails because the hostname does not resolve after the
bounded retry window, public verification has failed even if the Deployment and
Service are healthy. Run 1ctl app status "$APP" and retry later. A local
port-forward can diagnose the container, but it is not a substitute for a
working public route.
You can also open the route in your default browser:
1ctl app open "$APP"Use a React, Vue, or Svelte build
Section titled “Use a React, Vue, or Svelte build”For an existing Vite-based project, keep nginx.conf and satusky.toml, then use a multi-stage Dockerfile:
FROM node:22-alpine AS buildWORKDIR /appCOPY package*.json ./RUN npm ciCOPY . .RUN npm run build
FROM nginxinc/nginx-unprivileged:alpineCOPY --chown=101:101 --chmod=644 nginx.conf /etc/nginx/conf.d/default.confCOPY --from=build --chown=101:101 /app/dist/ /usr/share/nginx/html/EXPOSE 8080CMD ["nginx", "-g", "daemon off;"]Adjust /app/dist/ if your framework writes to a different output directory. Keep browser-safe values such as a public API base URL in your frontend build configuration. Never bake credentials or private API keys into a frontend bundle; every downloaded asset is visible to users.
Redeploy a change
Section titled “Redeploy a change”Change index.html or your application source, then run:
1ctl deploy1ctl app releases "$APP"The managed URL remains associated with the application. A successful image deployment creates a new release that can be selected for rollback; see the deployment rollout and rollback guide for the full release workflow.
Delete the example
Section titled “Delete the example”1ctl app delete "$APP" --yesThis removes the application workload and its managed route.
Troubleshooting
Section titled “Troubleshooting”Deep links return 404
Section titled “Deep links return 404”Confirm that the location / block contains:
try_files $uri $uri/ /index.html;The route returns 502
Section titled “The route returns 502”Confirm that all three port declarations use 8080:
port = 8080insatusky.toml;listen 8080;innginx.conf;EXPOSE 8080inDockerfile.
Then inspect the live state and logs:
1ctl app status "$APP"1ctl logs --app "$APP" --tail 100The page shows an old release
Section titled “The page shows an old release”Hashed JavaScript and CSS assets can be cached for a long time, but index.html should not be cached aggressively. Hard-refresh the page and ensure your nginx configuration does not add an immutable cache header to / or index.html.
The hostname does not resolve yet
Section titled “The hostname does not resolve yet”Run 1ctl app status "$APP". Wait for DNS condition: verified before retrying
the curl commands. If the route remains unattached after the workload is
healthy, collect 1ctl app status and 1ctl logs output for support.
Next steps
Section titled “Next steps”- Deploy a Backend to add an API.
- Environment Configuration to separate public runtime configuration from secrets.
- Custom Domains to attach your own hostname.