# Push Your First Image to the Container Registry & Deploy on Rapids

This tutorial walks the full *push-to-deploy* loop end to end: you build a tiny web app
into a container image, push it to your team's private registry at `cr.danubedata.ro`,
and run it serverless on **Rapids** — with a public HTTPS URL and scale-to-zero.

Everything stays inside DanubeData: pulls from the registry to Rapids travel over the
cluster LAN (free), and the pull credential is wired up for you automatically.

**Time:** ~15 minutes &nbsp;•&nbsp; **You need:** Docker, a DanubeData account, and a terminal.

This guide assumes your team slug is `acme` and your account email is `you@example.com` —
substitute your own. Your team slug is the `tenant_name` under **Team Settings → General**.

## What you'll build

A one-file HTTP service that returns JSON on `/` and `200 OK` on `/healthz`, listening on
port `8080`. We give you the code for **Node.js**, **Go**, and **Python** — pick the one you
like. The rest of the series treats it as just "the image", so the language stops mattering
after this page.

## Step 1: Write a tiny web app

Create a new empty folder and add the files for your language.

> **Two rules that matter on Rapids:** listen on the `PORT` environment variable (default
> `8080`), and don't try to bind a port below 1024 — containers run as a non-root user with
> Linux capabilities dropped, so privileged ports fail even as `root`. Port `8080` is the
> safe default.

### Node.js

`server.js`:

```js
const http = require('node:http');

const PORT = process.env.PORT || 8080;

const server = http.createServer((req, res) => {
  res.setHeader('Content-Type', 'application/json');

  if (req.url === '/healthz') {
    res.writeHead(200);
    return res.end(JSON.stringify({ status: 'ok' }));
  }

  res.writeHead(200);
  res.end(JSON.stringify({
    message: 'Hello from Rapids 👋',
    revision: process.env.K_REVISION || 'local',
  }));
});

server.listen(PORT, () => console.log(`listening on :${PORT}`));
```

`Dockerfile`:

```dockerfile
FROM node:20-alpine
WORKDIR /app
COPY server.js .
EXPOSE 8080
USER node
CMD ["node", "server.js"]
```

### Go

`main.go`:

```go
package main

import (
	"encoding/json"
	"log"
	"net/http"
	"os"
)

func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}

	http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
	})

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]string{
			"message":  "Hello from Rapids 👋",
			"revision": os.Getenv("K_REVISION"),
		})
	})

	log.Printf("listening on :%s", port)
	log.Fatal(http.ListenAndServe(":"+port, nil))
}
```

`go.mod`:

```
module hello

go 1.23
```

`Dockerfile` (multi-stage → a tiny distroless image, a few MB):

```dockerfile
FROM golang:1.23-alpine AS build
WORKDIR /src
COPY go.mod main.go ./
RUN CGO_ENABLED=0 go build -o /app/server .

FROM gcr.io/distroless/static-debian12
COPY --from=build /app/server /server
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["/server"]
```

### Python

`main.py`:

```python
import os
from fastapi import FastAPI

app = FastAPI()

@app.get("/healthz")
def healthz():
    return {"status": "ok"}

@app.get("/")
def root():
    return {"message": "Hello from Rapids 👋", "revision": os.getenv("K_REVISION", "local")}
```

`requirements.txt`:

```
fastapi==0.115.*
uvicorn[standard]==0.32.*
```

`Dockerfile`:

```dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY main.py .
EXPOSE 8080
RUN useradd -m app
USER app
CMD ["sh", "-c", "uvicorn main:app --host 0.0.0.0 --port ${PORT:-8080}"]
```

## Step 2: Build the image

Tag the image under your team slug from the very first build — the first path segment
**must** be your slug, or the registry rejects the push.

```bash
docker build -t cr.danubedata.ro/acme/hello:1.0.0 .
```

Optionally run it locally to confirm it works before you push:

```bash
docker run --rm -p 8080:8080 cr.danubedata.ro/acme/hello:1.0.0
# in another terminal:
curl localhost:8080
# {"message":"Hello from Rapids 👋","revision":"local"}
```

> **Apple Silicon / ARM machines:** the cluster runs `linux/amd64`. If you build on an M-series
> Mac, add `--platform linux/amd64` to `docker build` (or use `docker buildx`), otherwise the
> pod fails with `exec format error`.

## Step 3: Create a registry access key

1. Go to **Container Registry → Access Keys → New access key**.
2. Give it a **Name** (e.g. `laptop`) and pick **Push + Pull** as the scope.
3. Click **Create** and copy the token — it's shown **once**. Tokens start with `cr_`.

Keep it in an environment variable for the next step:

```bash
export DD_REGISTRY_TOKEN="cr_xxxxxxxxxxxxxxxxxxxx"
```

## Step 4: Log in and push

Use your account email as the Docker username (it's not secret — Docker just needs
*something* there) and the `cr_...` token as the password.

```bash
echo "$DD_REGISTRY_TOKEN" | docker login cr.danubedata.ro -u you@example.com --password-stdin

docker push cr.danubedata.ro/acme/hello:1.0.0
```

When the push finishes, the tag appears under **Container Registry → Repositories → hello**.

## Step 5: Deploy on Rapids

### Using the dashboard

1. Go to **Rapids → Create Container** and choose **Docker image** as the deployment type.
2. **Name:** `hello`
3. **Image:** `cr.danubedata.ro/acme/hello:1.0.0`
4. **Registry credential:** pick **DanubeData Container Registry** from the dropdown. It's
   pre-seeded for every team and authenticates against your own registry — you don't create
   or rotate anything for first-party pulls.
5. **Port:** `8080`
6. **Resource profile:** **Free** is enough for this tutorial — it includes the monthly free
   tier. (See the [pricing page](https://danubedata.ro/pricing) for the paid profiles and
   pay-per-use rates.)
7. Leave **Min replicas** at `0` for scale-to-zero and click **Create Container**.

Rapids generates a Knative Service, deploys it via GitOps, and provisions a TLS certificate.

### Or using the CLI

If you have the [DanubeData CLI](https://docs.danubedata.ro/cli-overview) installed and have
run `danube login`:

```bash
danube rapids create \
  --name hello \
  --type docker_image \
  --image cr.danubedata.ro/acme/hello \
  --tag 1.0.0 \
  --port 8080 \
  --profile free
```

You don't pass a registry credential on the CLI — the platform recognises `cr.danubedata.ro`
images as first-party and wires the pull secret automatically.

## Step 6: Test the live URL

Once the status shows **Running**, your container is reachable at:

```
https://hello-acme.danubedata.run
```

```bash
curl https://hello-acme.danubedata.run
# {"message":"Hello from Rapids 👋","revision":"hello-00001"}
```

The `revision` value now comes from Knative instead of `local` — proof you're hitting the
deployed container. Leave it idle for a few minutes and it scales to zero; the next request
cold-starts a fresh instance in milliseconds.

## Recap

You built an image, pushed it to your private registry, and deployed it serverless — all
within DanubeData, with the registry → Rapids pull staying on the cluster LAN.

## What's next

- **[Automate it: CI/CD Build → Push → Deploy](https://docs.danubedata.ro/tutorial-cr-rapids-cicd)** —
  ship a new revision on every `git push`.
- **[Production-Ready: Domains, Secrets & Autoscaling](https://docs.danubedata.ro/tutorial-cr-rapids-production)** —
  add your own domain, inject secrets, and tune scaling.
- **[Container Registry reference](https://docs.danubedata.ro/container-registry)** — tag
  conventions, plans & limits, and the full command set.

---

**Questions?** Contact support at support@danubedata.ro
