Skip to content

Your first release

Deploy to Kubernetes without knowing Kubernetes.

Describe your application with Docker Compose, run it on your machine, and push. The platform deploys it to staging. When you are happy with it, promote that release to production.

This walkthrough uses a small web application. You will configure its two environments, publish your first release, and open it in your browser.

Compose template preview

This walkthrough describes the target Compose workflow. Integration into the published project template is still in progress; that template currently uses the earlier deploy/ flow.

You prepare and test the application; a push releases it to staging; you choose when production updates

Before you start

You need Git, GitHub CLI (gh), Docker with Compose, Make, SOPS, and age on your machine. Sign in to GitHub with gh auth login.

Your platform team provides:

  • A GitHub organization and project template. The platform's GitHub Apps and CI runners must have access to your new repository.
  • An image repository and credentials. This guide uses Docker Hub. CI needs permission to publish; the platform also needs permission to read private images for promotion and deployment.
  • Two application hostnames and dashboard URLs. One hostname for staging, one for production, plus access to the release dashboard, Kargo.

The examples use blackstorm-dev/my-app, Docker Hub namespace <your-namespace>, and hostnames under example.com. Replace them with your team's values. You do not need to create a cluster or install Kubernetes on your laptop.

1. Create your application repository

Create your own repository from the template, then initialize it:

gh repo create blackstorm-dev/my-app --private \
  --template blackstorm-dev/project-template --clone
cd my-app
make init

make init sets up secret encryption and the checks that keep credentials out of your commits. It also gives your repository's CI the key it needs to read encrypted files. Keep the generated age.key out of Git and back it up securely.

For this walkthrough, keep the sample application and its Dockerfile. The application listens on port 8080, displays MESSAGE at /, and returns ok at /healthz.

These are the files you will work with:

my-app/
├── app.py
├── Dockerfile
├── docker/
│   ├── compose.yaml              # services shared by all environments
│   ├── compose.dev.yaml          # local build and access from your laptop
│   ├── compose.staging.yaml      # staging settings and hostname
│   └── compose.production.yaml   # production settings and hostname
├── secrets/
│   └── dockerhub.env             # encrypted credentials for publishing
└── Makefile

The template supplies CI and the commands around Compose. You describe the application; the platform generates its Kubernetes manifests.

2. Run it on your machine

The base Compose describes the service. WEB_IMAGE lets CI supply a release image; locally it falls back to my-app:dev.

docker/compose.yaml
name: my-app
services:
  web:
    image: ${WEB_IMAGE:-my-app:dev}
    environment:
      PORT: "8080"

The development file adds a build from your Dockerfile and publishes the container's port on localhost:8000:

docker/compose.dev.yaml
services:
  web:
    build:
      context: ..
      dockerfile: Dockerfile
    ports:
      - "127.0.0.1:8000:8080"
    environment:
      MESSAGE: Hello from development
make up
curl --fail http://127.0.0.1:8000/
curl --fail http://127.0.0.1:8000/healthz

make up combines the base and development files, builds the image, and starts the application with Docker Compose. The two requests should return Hello from development and ok.

3. Give staging and production their settings

Each environment combines the base plus its own file. The development file is not included in a deployment.

docker/compose.staging.yaml
name: my-app-staging
services:
  web:
    image: ${WEB_IMAGE:?Set WEB_IMAGE to the release image}
    environment:
      MESSAGE: Hello from staging
    ports:
      - "8080"
    x-ingress:
      routes:
        - hostname: my-app-staging.example.com
          port: 8080
docker/compose.production.yaml
name: my-app-production
services:
  web:
    image: ${WEB_IMAGE:?Set WEB_IMAGE to the release image}
    environment:
      MESSAGE: Hello from production
    ports:
      - "8080"
    x-ingress:
      routes:
        - hostname: my-app.example.com
          port: 8080

Replace the hostnames with those assigned to your application. x-ingress tells the platform which hostname reaches which container port. The platform handles the gateway and HTTPS; the sample still listens for HTTP on port 8080 inside the cluster.

You do not update WEB_IMAGE by hand for each release. CI supplies the immutable image reference when preparing the deployment. Promotion keeps that image and uses the target environment's settings: production displays its own message, even though it runs the same application image.

4. Give CI permission to publish

Set the Docker Hub namespace: the account or organization that owns the image repository, without /my-app.

gh variable set DOCKERHUB_NAMESPACE --body '<your-namespace>'
make secrets FILE=secrets/dockerhub.env

The second command opens your editor. Enter the credentials you received:

secrets/dockerhub.env — while editing
DOCKERHUB_USERNAME=your-dockerhub-user
DOCKERHUB_TOKEN=your-dockerhub-token

Save and close. SOPS encrypts the file; commit that encrypted version. CI uses the key configured by make init to read it. The private image repository must already exist as <your-namespace>/my-app.

These credentials publish the image. Confirm that the platform's read access is also ready; a successful push to Docker Hub does not prove the cluster can download a private image.

The sample needs no application secrets. When your application does, declare encrypted files using Compose secrets: see Use a secret in your application.

5. Push and follow the release to staging

Enable discovery once, then publish your changes:

gh repo edit --add-topic blackstorm-deploy
git diff
# Review the changed files, including the encrypted credentials.
git add docker/ .sops.yaml secrets/dockerhub.env
git diff --cached
git commit -m "Configure my first release"
git push origin main

The topic makes your repository discoverable by the platform.

There are two places to follow your release:

Open What happens What confirms this step
Your repository → Actions CI checks the application, publishes its image, and prepares each environment The release workflow succeeds
Your project in Kargo The platform picks up that release and deploys it to staging The staging promotion succeeds

A green CI run means the release is ready to deploy. Wait for staging's promotion and deployment to finish, then open your staging hostname:

curl --fail https://my-app-staging.example.com/
curl --fail https://my-app-staging.example.com/healthz

You should see Hello from staging and ok. Production has not been promoted yet.

For a platform running locally, use the assigned .localhost hostname and port 8443 instead of the example URLs. If its certificate is not trusted on your machine, use curl -k for that local check only.

6. Promote that release to production

Once you have checked the application in staging:

  1. Open your project in Kargo.
  2. On the production stage, choose Promote. Start from production, not staging.
  3. Select the release that successfully deployed to staging, then confirm Promote.
  4. Wait for the production promotion to succeed, then check your application.

Open your production hostname:

curl --fail https://my-app.example.com/
curl --fail https://my-app.example.com/healthz

You should see Hello from production and ok. Production now runs the same image you tested in staging. Promotion did not rebuild it.

Your next change

Change the code or Compose configuration, review the diff, and push to main. CI prepares another release and staging updates automatically. Production stays on its current release until you promote another one.

To return to an earlier release, select it from production → Promote in Kargo. Rollback restores that release's code and configuration; it does not undo database changes or restore uploaded files.

If the release gets stuck

What you see Where to look next
No workflow, or a job stays queued Check the repository's Actions settings and its access to the platform runners
CI cannot decrypt or publish Check SOPS_AGE_KEY, secrets/dockerhub.env, the namespace variable, and the image repository permissions
CI succeeds but the project never appears Check the blackstorm-deploy topic and the GitOps GitHub App's access to this repository
Staging's promotion fails Open that promotion in Kargo and read the failing step
A promotion reports a deployment failure Follow its link to Argo CD for deployment details, or share the failed promotion with your platform team
The app is healthy but its URL does not work Check the hostname and container port in x-ingress against the URL you are opening

Add application secrets, scheduled jobs, resource requests, and observability as your application needs them.