DEV Community

Vincent Tran
Vincent Tran

Posted on Originally published at 0xgosu.dev on

Cloud in a Bottle: A Personal Cloud That Works Like an App Platform

Self-hosting has a strange product problem. The people most motivated to own their software and data are often asked to become part-time infrastructure engineers first.

Installing an application can mean choosing a reverse proxy, arranging DNS, obtaining certificates, translating a Docker Compose file, deciding where state lives, setting up backups, and maintaining yet another login. None of those jobs is impossible. Together, they turn “run this useful tool” into a weekend project with a permanent pager attached.

Cloud in a Bottle is an open-source attempt to change that bargain. Its goal is not merely to put a friendly dashboard in front of containers. It treats one personally controlled server as an application platform: one identity boundary, one deployment contract, one routing layer, explicit permissions between apps, and storage whose durability is visible to both users and developers.

The useful question is not whether it makes servers disappear. It cannot. The question is whether it moves routine infrastructure decisions into a small, inspectable platform without taking ownership away from the person running it.

The cloud experience is the feature

Cloud applications won because they removed friction. They open from any device, update without installers, share URLs naturally, and hide the machine doing the work. Centralization was not the only possible way to get those benefits, but it became the dominant business model because a vendor-operated service could pay the infrastructure bill and control the whole experience.

Traditional self-hosting reverses the ownership model but often loses the product experience. Each application arrives with a different installation method and a different idea of configuration, networking, accounts, updates, and backups. The operator becomes the integration layer.

Cloud in a Bottle tries to make the server itself that integration layer. It runs on an Ubuntu machine you control: spare hardware at home, a virtual private server, or a managed machine provisioned by Imbue and handed over with your SSH access. The same open-source stack runs in each case. Applications come from Git repositories and run as OCI containers, while the platform provides the shared machinery around them.

That distinction matters. A container dashboard answers, “How can I start this workload?” An app platform must also answer:

  • How does a human reach it securely?
  • Which identity does the app see?
  • What data may it access?
  • How does it call another app without receiving all of that app’s files?
  • Which state is backed up, and which state is disposable?
  • What happens when the app asks for a dangerous capability?

The project is interesting because those answers form a coherent contract instead of a pile of setup recipes.

One service controls the machine

The architecture is intentionally small. According to the platform overview, one Python service called the router faces the rest of the system. It serves the dashboard and API, builds and starts app containers, chooses the destination for incoming requests, checks authentication, and supervises the supporting network processes.

Caddy terminates TLS. CoreDNS can serve the instance’s delegated DNS zone. A narrow system agent performs operations that the unprivileged service user cannot perform itself. Each installed app gets one rootless Podman container.

“A
The public route narrows through one policy point before it reaches a loopback-only app container.

Consider a visit to notes.example.com. A wildcard DNS record resolves the app subdomain to the server. Caddy accepts the HTTPS connection using a certificate covering the zone and its wildcard. It forwards the request over loopback to the router on port 8080. The router reads the hostname, maps notes to an installed app, checks the owner’s session unless that path is public, and proxies the request to the app’s HTTP port.

The routing documentation makes two choices that keep this manageable. First, one wildcard record covers new app names, so installing an app does not require another visit to a DNS control panel. Second, one wildcard certificate covers the zone and app subdomains. In the normal public setup, DNS-01 validation lets the instance create the temporary challenge record itself.

Caddy is used for the part it handles well: TLS termination and the HTTP-to-HTTPS redirect. Bottle’s router manages the wildcard certificate workflow and generates Caddy’s configuration, while application identity and routing policy remain in the router.

This is not a distributed control plane. That is a feature at this scale. One personal server does not need a miniature Kubernetes architecture. It needs a short path from hostname to process and an operator who can still understand the failure modes.

A repository becomes an installable app

An application is packaged with a Dockerfile and a cloudinabottle.toml file at the repository root. The basic contract is deliberately small:

[app]
name = "reading-list"
version = "0.1.0"
description = "Save and annotate articles"

[runtime.container]
image = "Dockerfile"
port = 8080

[routing]
public_paths = ["/webhook"]

[resources]
memory_mb = 256
cpu_cores = 0.25

[data]
sqlite = ["main"]
app_temp_data = true

Enter fullscreen mode Exit fullscreen mode

The router clones the repository, reads the manifest, builds the image with rootless Podman, and starts it. The app listens on 0.0.0.0 inside the container; the platform publishes its HTTP port only on the host loopback interface and exposes it through the router. The app-author guide also defines injected environment variables for the app name, stable app identity, service token, router URL, zone domain, and mounted data paths.

This format does two jobs. It automates deployment, but it also makes privilege visible. A reviewer can see that an app wants 256 MB of memory, one SQLite database, scratch space, and a public webhook. More sensitive requests—extra Linux capabilities, host devices, direct ports, or access to every app’s data—appear in the same manifest.

The platform rejects capabilities and device paths outside rootless-safe allowlists. That does not make every allowed request harmless. It does make the permission surface concrete before installation, which is far better than discovering a privileged mount deep inside an opaque Compose stack.

Existing software does not have to adopt every platform feature. If it already runs in a container and speaks HTTP, a thin manifest may be enough. Native integration is optional: apps can gradually use the owner identity, declared storage, or cross-app services when those features improve the experience.

Rootless containers reduce the default blast radius

Cloud in a Bottle assumes that a personal server will run code the owner did not write. Its security model starts by placing every app in a rootless Podman container under an unprivileged host account.

In rootless mode, container root maps into an unprivileged user namespace rather than to real host root. Podman’s own rootless-mode documentation explains that these namespaces rely on subordinate UID and GID ranges. Bottle adds idmapped bind mounts so files written as root inside a container appear on disk as files owned by the service user.

The default boundary is useful:

  • The app receives its own data directories, not a mount of the whole server.
  • The router database, session state, and TLS keys are not mounted into app containers.
  • The primary HTTP port binds to loopback, so public requests must cross router policy.
  • The container receives the standard capability baseline plus no-new-privileges, not arbitrary host powers.
  • Client-supplied X-OpenHost-* headers are stripped before trusted identity headers are added.

These are containment layers, not a proof of safety. The project itself is explicit that catalog review is not a guarantee. App authors can update code after inclusion, elevated manifest permissions widen the boundary, and a vulnerability in the container runtime or the router could still matter.

That honesty is important. “Sandboxed” should mean that the default blast radius is smaller and privileges are reviewable. It should not mean that untrusted code has become risk-free.

Authentication belongs at the front door

By default, every route in every app requires the owner’s session. Authentication happens in the router before the request reaches the container. One domain-scoped session cookie covers the dashboard and app subdomains, which gives the system the feel of a single device rather than a folder of unrelated services.

Apps can declare public path prefixes for webhooks, shared pages, or their own login flows. On those routes, the app becomes responsible for access decisions. If a public route is visited by the owner, the router adds X-OpenHost-Is-Owner: true, allowing an app to expose a public view and still reveal editing controls to the authenticated owner.

The platform has separate credentials for separate actors:

Credential Principal Main use
Session cookie Human owner in a browser Dashboard and protected app routes
API token Owner-operated CLI, script, or agent Management API calls
App token One installed application Permissioned cross-app service calls

The separation prevents an app token from becoming a dashboard credential. The router also rejects owner sessions on cross-origin requests, reducing the chance that JavaScript in one app can silently act as the owner against another app.

Central authentication is convenient, but it also concentrates responsibility. The router is now a security boundary for every application. Session handling, host matching, proxy headers, cross-origin checks, and public-path matching deserve more scrutiny than they would in a simple launcher. The trade is sensible only if the shared boundary is kept small enough to inspect and test.

Apps cooperate through grants, not shared disks

Containerizing every app creates another problem: useful software often needs to cooperate. A mail summarizer may need a mail provider. A photo tool may need a library. A coding agent may need secrets without receiving the owner’s entire password store.

Cloud in a Bottle addresses this with cross-app services. A provider declares a versioned HTTP service. A consumer declares the service version it needs, a short local name, and the grants it requests. The owner approves those grants during installation. Calls then pass through the router using the consumer’s injected app token.

“A
The router authenticates and carries the grant; the provider still decides what that grant permits.

Service identity is a URL plus a semantic version. Consumers can request a compatible range; the router resolves an installed provider for each call. The current built-in examples include secrets and OAuth services, but the mechanism is general.

There is a subtle division of responsibility. The router authenticates the caller, selects a provider, and forwards only the grants the owner approved. The provider enforces the meaning of those grants. A mail provider, for example, might define scopes for a mailbox or folder and supply its own approval UI for data-dependent choices.

This is more work than mounting a shared directory, but it preserves boundaries. The consumer receives a narrow operation instead of ambient access to another app’s state. It also lets multiple implementations satisfy the same service contract, which is the beginning of a real personal-cloud ecosystem rather than a hard-coded bundle.

The design is still young. Service URLs are currently used mainly as comparable identifiers, and the documentation anticipates richer machine-readable specifications later. Version negotiation and provider-defined permissions will need discipline as the catalog grows. Still, the direction is sound: integration should be an explicit capability, not an accidental consequence of co-location.

Storage is split by semantics

Many self-hosted deployments treat storage as one volume and leave every application to invent its own backup story. Bottle exposes three tiers with different contracts:

Tier Backing Backup expectation Good fit
Permanent Local disk Included in normal backups SQLite, configuration, indexes, notes
Temporary Local scratch Recreatable, not guaranteed Thumbnails, transcodes, build artifacts
Archive JuiceFS over local disk or S3 Managed separately from normal backups Photos, video, attachments, model weights

The data guide draws an important technical line. Embedded databases need local storage with real fsync, locks, and strict filesystem behavior. They belong in permanent storage. The archive tier is network-shaped when backed by S3; it is appropriate for whole objects, not SQLite write-ahead logs or correctness-sensitive locks.

A media application might therefore keep metadata and its search index on permanent local disk while placing original videos in archive storage. Temporary transcodes can be discarded and rebuilt. The manifest communicates that intent to the platform and to the operator.

The abstraction also supports migration. Archive storage is always presented through JuiceFS, whether its objects live locally or in S3. Changing the backing store can preserve the application’s path layout. That portability is valuable, but it does not erase physics: uncached object reads can be much slower, and an archive stored only on the same local disk is not magically backed up.

Bottle includes a storage guard that stops apps when free disk space falls below a configured reserve, 500 MB by default. Stopping early is disruptive; filling the disk that contains databases, logs, and control-plane state is worse.

The remaining work is operational, not conceptual

Cloud in a Bottle removes many repetitive choices, but ownership still has obligations. A serious deployment needs a domain or a clear local-network plan, reachable ports, security updates, backups that have been restored in practice, and a recovery path if the only server fails.

Home hosting adds NAT, dynamic addresses, power loss, and residential-network constraints. A VPS simplifies reachability but returns hardware trust and a monthly bill to a provider. Managed hosting reduces setup but still requires the owner to understand where data lives and who can access the machine. The three deployment modes trade convenience, control, and operational burden; none wins every axis.

The project is also early. Its launch post says the core was built and privately tested for more than six months, while the site describes active development and a deliberately small curated catalog. Early adopters should expect rough edges, inspect requested permissions, pin or review application sources when risk matters, and avoid treating catalog inclusion as a security audit.

A practical evaluation can stay small:

  1. Start on a disposable VPS or virtual machine rather than the only copy of important data.
  2. Install one simple HTTP app with no elevated capabilities.
  3. Verify that protected routes reject unauthenticated requests and intended public paths remain reachable.
  4. Locate the app’s permanent, temporary, and archive directories on disk.
  5. Run a backup, destroy the test app or instance, and prove that restoration works.
  6. Review how an update changes the app manifest before applying it.
  7. Only then move a useful but recoverable workload onto the platform.

That sequence tests the ownership claim, not only the happy-path installer.

The right abstraction is a small personal platform

The strongest idea in Cloud in a Bottle is not one-click deployment. It is the decision to make personal infrastructure behave like a platform without pretending it is a hyperscale cloud.

The router creates a single front door. Rootless containers establish useful defaults. The manifest makes resources and privilege requests visible. Storage tiers force applications to distinguish durable state from scratch data and bulk objects. Cross-app services make cooperation permissioned instead of ambient. A shared login turns separate web tools into something closer to one owned device.

None of those mechanisms is individually novel. Their value comes from the product boundary they form together. If the implementation remains compact, the permissions remain legible, and recovery stays first-class, a personal cloud can be more than a hobbyist dashboard. It can become a credible place to run the growing amount of useful software that should not require surrendering the data, the machine, or the exit door.

Further reading

Top comments (0)