Skip to main content

Snout Containers

A Docker image of yours, running for as long as your project does, at https://<ref>.api.snoutdata.com/containers/v1/<name>/. For the one kind of code a request cannot hold: a process that keeps a connection of its own open, such as a WhatsApp client, an MQTT or Kafka subscription, an IMAP idle, a game loop or a market data feed.

Most backends do not need one. A Socket.IO server is usually realtime handlers, an API endpoint or a webhook is a function, and a scheduled job is either of those on a schedule. A container is for what is left.

Containers run on the Plus plan (one, 256 MB) and the Pro plan (three, 512 MB each), with half a CPU core each. A free project cannot run one, because a container runs all the time.

Deploy one​

snoutdata containers deploy whatsapp ghcr.io/acme/whatsapp-bridge:1.4
snoutdata containers list
snoutdata containers logs whatsapp --follow
snoutdata containers restart whatsapp
snoutdata containers delete whatsapp

deploy asks your registry what the tag points at now and pins that digest. What runs is that exact image until you deploy again, whatever happens to the tag meanwhile; pushing a new :latest changes nothing until the next deploy. It then waits for the container to start and prints its URL, or why it did not start.

The image must include linux/arm64, which is what SnoutData runs. Most official images do. If yours is built for amd64 only, deploy says so; build both with (or deploy with --dockerfile, below, which builds for arm64):

docker buildx build --platform linux/amd64,linux/arm64 -t ghcr.io/acme/whatsapp-bridge:1.4 --push .

A private registry​

Give the user name with --registry-user and the token on stdin, so it is in neither your shell history nor ps:

echo "$GHCR_TOKEN" | snoutdata containers deploy whatsapp ghcr.io/acme/whatsapp-bridge:1.4 \
--registry-user acme --stdin

Any registry works: Docker Hub, GitHub (GHCR), GitLab, or your own. The token is stored sealed, is used only to pull your image, and is never shown again. A redeploy of the same container reuses it; --clear-registry forgets it.

From a Dockerfile, with no registry​

No registry of your own? Point deploy at a folder with a Dockerfile in it (or at the Dockerfile):

snoutdata containers deploy whatsapp --dockerfile ./bridge

It builds the folder for linux/arm64 with the Docker on your machine, pushes the image straight into your project's own repository in SnoutData's registry, and deploys it. The login it pushes with is made for that one repository, ends within the hour, and is logged out of Docker as soon as the push is done. Nothing is built on our side: the build is your Docker's, with your cache and your build secrets.

Where it runs from​

The first time a container starts, its image is copied into SnoutData's registry, and every start after that (a restart, a crash, a move to another machine) runs the copy. So once a container has started, your registry being down, your token expiring or Docker Hub's rate limit never stops it coming back. A registry token is used at deploy and for that first copy, and not after.

An image may be at most 1 GB, compressed (its linux/arm64 image, which is all that is copied). A slimmer base (alpine, distroless, a -slim tag) usually fixes one that is larger.

What a container gets​

PORTThe port to listen on. 8080 unless you deploy with --port.
DATABASE_URLYour project's database, as its owner, over TLS.
SNOUTDATA_URL, SNOUTDATA_ANON_KEY, SNOUTDATA_SERVICE_ROLE_KEYYour project's API address and its two keys.
Your secretsEverything set with snoutdata secrets set, the same environment your functions get.

A secret named DATABASE_URL or PORT takes the place of ours. Setting or changing a secret restarts your containers with the new value.

It is stateless​

Anything written inside a container is lost when it restarts, is redeployed, or moves to another machine. The container's disk is for scratch: caches, temporary files. What matters goes in your project's database (or Storage for files), which is why DATABASE_URL is already set.

For a WhatsApp client like Baileys, that means its session (the auth state it would otherwise keep in a folder) goes in a table, so a restart reconnects without a new QR scan. Most such libraries take a custom store for exactly this.

Reaching it​

https://<ref>.api.snoutdata.com/containers/v1/<name>/ reaches your container's port, with the prefix removed: /containers/v1/whatsapp/webhook arrives as /webhook. Websockets work.

No API key is needed there. A container is your own server, and the callers it exists for (a webhook sender, your own front end) cannot add one, so it authenticates its own requests the way any server you run does. A key that is sent is passed through unchanged.

Reaching your users and your database​

A container talks to the rest of your project the way any client does:

  • Your users: realtime.send() from SQL, or a broadcast over HTTP, reaches every Realtime and Socket.IO client in the room.
  • Your database: DATABASE_URL, with your row-level security policies where you use a role other than the owner. A table it watches with LISTEN is how your handlers and functions hand it work.

Run one copy of anything that holds a session of its own. SnoutData runs exactly one instance of each container, and stops the old one before starting the new one on a deploy; if your process can be started twice by accident, take pg_try_advisory_lock at startup so the second waits.

When it stops​

A container that exits is started again, waiting a little longer each time (5 seconds, doubling up to 5 minutes). snoutdata containers list shows restarting with the exit code and the last lines it printed, and failed once it has failed five times in a row. Running for a minute clears the count. logs shows everything it printed to stdout and stderr in the last seven days.

Limits​

PlusPro
Containers13
Memory each256 MB512 MB
CPU eachhalf a corehalf a core
Sent each month, all containers together5 GB60 GB
Image size, compressed1 GB1 GB

What a container is good at is holding connections: an idle websocket costs memory (about 30 KB each for a small Go server) and no CPU, and a Plus container can hold a few thousand. The front door lets a Plus project hold 200 websockets at once and a Pro project 400. A busy one costs CPU: gVisor serves every network read and write itself, so half a core echoes about 3,000 small messages a second. A loop that fans messages out to many users belongs in realtime handlers, where it runs in your database.

Each container runs in its own gVisor sandbox and user namespace, on a network of its project's own: it reaches the public internet and your project's other containers, and nothing private: not the machine, not the cloud's metadata service, not another project's containers. A container over its memory is stopped and restarted. On a downgrade, the containers past the new plan's number are stopped and kept, and start again on an upgrade.

What a container may send​

The public internet is open to a container, so what it sends is limited by how much and how fast, the same on every plan:

New connections20 a second, with bursts of 200
Connections it holds open1,000
Sending, on connections it opened2 MB a second, with bursts of 8 MB
Port 25Closed. Send mail through a provider's submission port (587 or 465)

Past a limit, a new connection is refused at once (a TCP reset, so your client fails fast rather than hanging), and bytes over the rate are dropped, which TCP sees as a slower link. Connections TO your container (your users, webhooks and websockets through its address) are not counted against any of this. A container that is refused again and again, slowed down for long stretches, or sends more than 4 GB in an hour opens an alert: you are emailed which, and so are we.

Each month's allowance​

What a project's containers send in a calendar month (UTC) counts against its plan's allowance: 5 GB on Plus, 60 GB on Pro, for all its containers together. Everything a container sends counts, replies to your users included; what reaches it does not. At 80% you are emailed. At the allowance the project's containers are stopped until the 1st, when they start again on their own, and you are emailed then too: snoutdata containers list and the Containers tab say so, and show what has been sent this month. The connections, messages and webhooks these containers are for use a small fraction of it.

Open source, and self-hosted​

What runs your containers is snout-containers, Apache-2.0: you can read exactly how a container is isolated and limited. The self-hosted stack runs it as an opt-in profile, once the machine has gVisor and the podman engine its README describes.

Also read​