# How to Run Redpanda (Kafka API) in a Cloud Sandbox

> Run the official Redpanda image in a Sandboxer microVM with two listeners, one for containers and one advertised as localhost:19092, then reach it from your laptop with sbx forward. Kafka is TCP, so it doesn't go through a hosted HTTPS URL; Redpanda Console does. rpk produced 100,000 messages in about 0.2 seconds.

Source: https://sandboxer.com/guides/run-redpanda-kafka-in-a-sandbox. Updated 2026-10-27.

**Short answer:** Run the official Redpanda image in a Sandboxer microVM with two listeners, one for containers and one advertised as localhost:19092, then reach it from your laptop with sbx forward. Kafka is TCP, so it doesn't go through a hosted HTTPS URL; Redpanda Console does. rpk produced 100,000 messages in about 0.2 seconds.

## What we ran

Everything below ran on production on October 27, 2026, in one `default` sandbox (4 vCPU, 8 GB). [Redpanda](https://docs.redpanda.com/current/get-started/quick-start/) is a single-binary broker that speaks the Kafka protocol, so Kafka clients and tools work against it unchanged. The `latest` image was Redpanda v26.2.6. The laptop client was Python `confluent-kafka` 2.16.0. Timings are single runs or three back-to-back runs; yours will differ.

| Step | Result |
| - | - |
| `sbx new` until ready | 3.2 s |
| Pull Redpanda, start it, wait until `rpk cluster health` is healthy | 8 s |
| Produce 100,000 short messages with `rpk` inside the sandbox | 187 to 222 ms |
| Consume the same 100,001 messages with `rpk` | 174 ms |
| Produce one message from the laptop through `sbx forward` and wait for the ack | 125 ms median, 20 messages |
| Redpanda Console on a private hosted URL | 401 without the token, 200 with it |
| Kafka client pointed at a hosted URL | `Broker transport failure` |
| `sbx stop --wait`, then `sbx resume` | 2.2 s and 1.4 s, memory restored, messages intact |
| Memory in use after the tests | Redpanda 584 MiB, Console 44 MiB |

## Which route reaches which port

Kafka clients speak Kafka's binary protocol over TCP. A [hosted URL](/docs/hosting) is an HTTPS proxy that checks a token on each request, so it suits web UIs and HTTP APIs, not the broker.

| What | Port in the VM | How you reach it from outside |
| - | - | - |
| Kafka API (producers, consumers, admin) | 19092 on `127.0.0.1` | `sbx forward` or `ssh -L`, both over your API key |
| Kafka API for other containers | 9092 on the Docker network | Only other containers on `kafkanet` |
| Redpanda Console (web UI) | 8080 on `127.0.0.1` | `sbx host --localhost`, private by default |

We checked the second-to-last row of the results table directly: we hosted 19092, pointed a Kafka admin client at `…-19092.box.sandboxer.com:443` over TLS, and it failed with `Failed to get metadata: Local: Broker transport failure`. A plain `curl` at that URL got `401` from the proxy. We then hid the route.

## 1. Start Redpanda with two listeners

Docker is preinstalled in every sandbox. The command follows Redpanda's single-broker quickstart. Image pulls take longer than the default 30-second `sbx exec` timeout, so pass `--timeout`:

```bash
sbx new --name redpanda --type default --no-env
id=sb_pgzo691h33    # the id that sbx new printed

sbx exec $id --timeout 600 "docker network create kafkanet && \
  docker run -d --name redpanda --network kafkanet --restart unless-stopped \
    -p 127.0.0.1:19092:19092 -v redpanda-data:/var/lib/redpanda/data \
    docker.redpanda.com/redpandadata/redpanda:latest \
    redpanda start --mode dev-container --smp 1 \
      --kafka-addr internal://0.0.0.0:9092,external://0.0.0.0:19092 \
      --advertise-kafka-addr internal://redpanda:9092,external://localhost:19092 && \
  until docker exec redpanda rpk cluster health | grep -q 'Healthy:.*true'; do sleep 1; done && \
  docker exec redpanda rpk cluster health"
```

```text
CLUSTER HEALTH OVERVIEW
=======================
Healthy:                          true
Unhealthy reasons:                []
Controller ID:                    0
All nodes:                        [0]
…
```

The listener flags are the part that trips people up. A Kafka client only uses the address you give it to fetch metadata. After that it connects to whatever address the broker *advertises*. So each listener advertises the name its clients can resolve:

- `internal://redpanda:9092` is for containers on the `kafkanet` network, such as Redpanda Console in step 4.
- `external://localhost:19092` is for clients that reach port 19092 as `localhost`. That covers anything in the VM and your laptop through a port forward that keeps the same port number.

Three other flags matter:

- `-p 127.0.0.1:19092:19092` keeps the broker inside the VM. Dev-container mode has no authentication, so nothing outside the VM should reach it except through your forward.
- `-v redpanda-data:/var/lib/redpanda/data` keeps the log segments in a named volume on the sandbox disk.
- `--restart unless-stopped` makes Docker start Redpanda again if the sandbox boots from disk instead of restoring memory.

`--mode dev-container` and `--smp 1` are Redpanda's settings for a small development broker. They are not a production layout. See Redpanda's [production deployment docs](https://docs.redpanda.com/current/deploy/) for that.

## 2. Produce and consume with rpk

The image ships with [`rpk`](https://docs.redpanda.com/current/reference/rpk/), Redpanda's CLI. Create a topic with three partitions, then send and read one message:

```bash
sbx exec $id "docker exec redpanda rpk topic create orders -p 3 && \
  echo hello-from-rpk | docker exec -i redpanda rpk topic produce orders -k first && \
  docker exec redpanda rpk topic consume orders -n 1"
```

```text
TOPIC   STATUS
orders  OK
Produced to partition 1 at offset 0 with timestamp 1793095742303.
{
  "topic": "orders",
  "key": "first",
  "value": "hello-from-rpk",
  "timestamp": 1793095742303,
  "partition": 1,
  "offset": 0
}
```

`rpk topic produce` sends one message per input line, so a file of 100,000 lines is a quick load test:

```bash
sbx exec $id --timeout 300 "seq 1 100000 | sed 's/^/order-/' > msgs.txt && \
  s=\$(date +%s%N); docker exec -i redpanda rpk topic produce orders < msgs.txt > /dev/null; \
  echo produce ms=\$(( (\$(date +%s%N)-s)/1000000 )); \
  s=\$(date +%s%N); docker exec redpanda rpk topic consume orders -n 100001 -f '%v\n' | tail -1; \
  echo consume ms=\$(( (\$(date +%s%N)-s)/1000000 )); \
  docker exec redpanda rpk topic describe orders -p"
```

```text
produce ms=222
order-93172
consume ms=174
PARTITION  LEADER  EPOCH  REPLICAS  LOG-START-OFFSET  HIGH-WATERMARK
0          0       1      [0]       0                 38790
1          0       1      [0]       0                 38861
2          0       1      [0]       0                 22350
```

Three more produce runs into a fresh topic took 187, 210 and 210 ms. The messages are a few bytes each, so this measures per-message overhead, not disk bandwidth. The last message consumed is `order-93172`, not `order-100000`: Kafka orders messages within a partition, not across a topic.

## 3. Produce and consume from your laptop

Open a forward in one terminal. It runs in the foreground until you press Ctrl-C:

```bash
sbx forward $id --remote 19092
```

```text
{"event":"forwarding","id":"sb_pgzo691h33","reverse":false,"local":19092,"remote":19092,"bind":"127.0.0.1"}
```

Keep the local port at 19092. The broker advertises `localhost:19092`, so after the first metadata request your client connects to that address. If you forward to another local port, the client still dials 19092 after the first request (we kept 19092 and did not test other ports).

In a second terminal, any Kafka client works with `bootstrap.servers=127.0.0.1:19092`. We used [`confluent-kafka`](https://docs.confluent.io/kafka-clients/python/current/overview.html) for Python (`pip install confluent-kafka`). This script, `laptop.py`, sends 20 messages one at a time, waits for each ack, then reads the whole topic back:

```python
import time, uuid
from confluent_kafka import Producer, Consumer

conf = {"bootstrap.servers": "127.0.0.1:19092"}
p = Producer(conf)
lat = []
for i in range(20):
    t0 = time.perf_counter()
    p.produce("orders", key=f"k{i}", value=f"from-laptop-{i}")
    p.flush()
    lat.append((time.perf_counter() - t0) * 1000)
lat.sort()
print(f"produce+ack median {lat[10]:.0f} ms, min {lat[0]:.0f} ms over {len(lat)} messages")

c = Consumer({**conf, "group.id": f"laptop-{uuid.uuid4().hex[:6]}", "auto.offset.reset": "earliest"})
c.subscribe(["orders"])
n, last = 0, None
deadline = time.time() + 20
while time.time() < deadline:
    m = c.poll(1.0)
    if m is None:
        if n: break
        continue
    if m.error(): print(m.error()); continue
    n += 1; last = m
print(f"consumed {n} messages, last: partition={last.partition()} offset={last.offset()} value={last.value().decode()}")
c.close()
```

```text
produce+ack median 125 ms, min 125 ms over 20 messages
consumed 100021 messages, last: partition=2 offset=22356 value=from-laptop-17
```

The 125 ms is the round trip between our client and the sandbox, plus the broker's ack. It depends on where you are. A producer that batches, which is the default when you don't `flush()` after every message, pays it once per batch. For load tests, run the producer in the sandbox as in step 2.

[`ssh -L 19092:127.0.0.1:19092 $id.sbx`](/guides/ssh-into-a-cloud-sandbox-from-vs-code-or-cursor) is the same tunnel with plain OpenSSH; we tested `sbx forward` only for Kafka. CLI builds from 2026-10-27 (`7e848c3`) on free local port 19092 when you stop `sbx forward` with `kill` from a script, as well as with Ctrl-C. With an older CLI, check the port afterwards (`lsof -i :19092`), because its `ssh` child could keep listening.

## 4. Redpanda Console on a private URL

[Redpanda Console](https://docs.redpanda.com/current/console/) is a web UI for topics, messages and consumer groups. It reaches the broker on the internal listener and serves HTTP on 8080, so it fits a hosted URL:

```bash
sbx exec $id --timeout 600 "docker run -d --name console --network kafkanet --restart unless-stopped \
  -p 127.0.0.1:8080:8080 -e KAFKA_BROKERS=redpanda:9092 docker.redpanda.com/redpandadata/console:latest"
sbx host $id 8080 --localhost --title "Redpanda Console"
```

`-p 127.0.0.1:8080:8080` keeps Console off the VM's public IPv6 address whatever the firewall does. (In our October 27, 2026 check, `ufw` did block a Docker-published port there, but Docker can bypass `ufw` on other setups.) `--localhost` tells the hosted URL to relay to that loopback port. (Our test run published `-p 8080:8080` and hosted it without `--localhost`; the results below come from that run.)

`sbx host` prints a private URL like `https://zesty-spry-nectar-8080.box.sandboxer.com?_token=…`. Console has no login in this setup, so the token is the only lock. Treat the URL like a password. We checked it with `curl`:

```bash
url='https://zesty-spry-nectar-8080.box.sandboxer.com?_token=…'   # the full URL sbx host printed
base=${url%%\?*}
curl -s -o /dev/null -w '%{http_code}\n' "$base/"                  # no token
curl -s -c jar -b jar -L -o /dev/null -w '%{http_code}\n' "$url"   # token
curl -s -b jar "$base/api/topics" | python3 -c 'import sys, json
for t in json.load(sys.stdin)["topics"]: print(t["topicName"], t["partitionCount"], t["isInternal"])'
```

```text
401
200
__consumer_offsets 3 True
orders 3 False
```

The `401` comes from Sandboxer: without the token, the request never reaches the sandbox. With it, the page title is `Redpanda Console` and the topic list includes `orders` and the consumer group offsets from step 3. Don't use `sbx host --public` for Console unless you add Console's own authentication; anyone with the link could browse your messages.

## 5. What survives a stop and resume

We wrote a marker message, stopped the sandbox, resumed it and read the marker back:

```bash
sbx exec $id "echo written-before-stop | docker exec -i redpanda rpk topic produce orders -k marker"
sbx stop $id --wait
sbx resume $id
sbx exec $id "docker ps --format '{{.Names}} {{.Status}}'; \
  docker exec redpanda rpk topic consume orders -n 100022 -f '%k %v\n' | grep marker"
```

```text
Produced to partition 1 at offset 38865 with timestamp 1793095839469.
console Up About a minute
redpanda Up About a minute
marker written-before-stop
```

Our first stop and resume took 2.2 and 1.4 seconds. The resume reported `memoryRestored: true`, so neither container restarted. The Console URL kept working with the same token. A new `sbx forward` after the resume ran `laptop.py` again with the same 125 ms median; [the docs](/docs/ssh#forward-ports) say a running forward reconnects by itself when the sandbox moves.

A resume boots from disk instead when no machine with the same CPU type is free, or when you move to a bigger size. Then processes are gone, and `--restart unless-stopped` starts both containers again from the `redpanda-data` volume. We did not force that case here; the [Docker Compose guide](/guides/run-docker-compose-in-a-sandbox-with-a-url-per-service) shows the pattern. Moving to a smaller size isn't supported yet; create a new sandbox and copy the topics over with a Kafka mirroring tool.

## 6. Clean up

```bash
sbx delete $id --yes
```

A `default` sandbox costs $0.036/hour while it runs and nothing while it is stopped.

## FAQ

### Can a Kafka client connect to a hosted HTTPS URL?

No. A hosted URL is an HTTPS proxy that checks a token on each request, and Kafka clients speak their own binary protocol. We tried it and got `Broker transport failure`. Use `sbx forward` or an SSH tunnel for the broker, and a hosted URL for HTTP tools like Redpanda Console.

### Why does my client connect, then fail with "connection refused" or "unknown host redpanda"?

Usually the client reached the bootstrap address, then dialed the advertised address from the metadata. We didn't reproduce this error; it follows from how Kafka clients use advertised listeners. Make sure the listener you reach advertises an address your client can resolve: `localhost:19092` for the forward, `redpanda:9092` for containers on the Docker network. Forward to the same local port the broker advertises.

### Is this real Kafka?

It is Redpanda, which implements the Kafka API, so Kafka clients and `rpk` work against it. We tested `rpk` and Python `confluent-kafka`. If you need Apache Kafka itself, run the official `apache/kafka` image with the same listener and port-forward pattern; we did not test it here.

### How much memory does it need?

After the tests, Redpanda used 584 MiB and Console 44 MiB in a `default` sandbox with 8 GB. Dev-container mode with `--smp 1` uses one core. If a container runs out of memory, it exits with code 137 and the sandbox stays up.

### Can other sandboxes reach my broker?

No. Sandboxes can't reach each other's private addresses, and the broker port is bound to `127.0.0.1` in the VM. See [what can reach a sandbox](/guides/what-can-reach-a-sandbox-and-what-it-can-reach).

## Related

- [Run RabbitMQ in a sandbox](/guides/run-rabbitmq-in-a-sandbox)
- [What can reach a sandbox, and what it can reach](/guides/what-can-reach-a-sandbox-and-what-it-can-reach)
- [SSH into a cloud sandbox from VS Code or Cursor](/guides/ssh-into-a-cloud-sandbox-from-vs-code-or-cursor)
- [Hosting](/docs/hosting)
