Skip to content

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/settings fall back to index.html;
  • deploy from a Dockerfile with satusky.toml;
  • find and test the managed HTTPS URL.
  • 1ctl installed
  • an authenticated profile: 1ctl auth status
  • jq and curl for the final HTTP checks

Run the following commands in a terminal:

Terminal window
mkdir satusky-frontend
cd 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:alpine
COPY --chown=101:101 --chmod=644 nginx.conf /etc/nginx/conf.d/default.conf
COPY --chown=101:101 --chmod=644 index.html /usr/share/nginx/html/
EXPOSE 8080
CMD ["nginx", "-g", "daemon off;"]
DOCKERFILE

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

Choose a unique lowercase app name and create satusky.toml:

Terminal window
APP="my-frontend-$(date +%s)"
cat > satusky.toml <<TOML
[app]
name = "$APP"
port = 8080
[build]
dockerfile = "Dockerfile"
[checks]
health_path = "/"
TOML

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

Terminal window
1ctl deploy

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

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

Terminal window
1ctl logs --app "$APP" --tail 50
DEPLOYMENT_ID="$(1ctl -o json app get "$APP" | jq -r '.deployment_id')"
1ctl logs stream --deployment-id "$DEPLOYMENT_ID"

Press Ctrl+C to stop streaming.

Read the generated HTTPS URL from the deployment record:

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

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

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

Terminal window
1ctl app open "$APP"

For an existing Vite-based project, keep nginx.conf and satusky.toml, then use a multi-stage Dockerfile:

FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginxinc/nginx-unprivileged:alpine
COPY --chown=101:101 --chmod=644 nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build --chown=101:101 /app/dist/ /usr/share/nginx/html/
EXPOSE 8080
CMD ["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.

Change index.html or your application source, then run:

Terminal window
1ctl deploy
1ctl 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.

Terminal window
1ctl app delete "$APP" --yes

This removes the application workload and its managed route.

Confirm that the location / block contains:

try_files $uri $uri/ /index.html;

Confirm that all three port declarations use 8080:

  • port = 8080 in satusky.toml;
  • listen 8080; in nginx.conf;
  • EXPOSE 8080 in Dockerfile.

Then inspect the live state and logs:

Terminal window
1ctl app status "$APP"
1ctl logs --app "$APP" --tail 100

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.

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.