Openship: The Self-Hosted PaaS That Runs Your Docker Socket Like It Owns The Place
Hook
Most self-hosted PaaS platforms hide their complexity behind abstractions. Openship does something more honest—and more dangerous: it mounts your host's Docker socket directly, giving its control plane container god-mode privileges over your entire server.
Context
The modern deployment landscape splits cleanly into two camps: managed PaaS services like Heroku and Render that charge premium prices for zero-ops convenience, and self-hosted alternatives that trade operational complexity for cost savings and control. For solo developers and small teams running side projects, the economics are punishing—$25/month per dyno adds up fast when you're running five microservices. Docker Compose gets you partway there, but manually wiring up reverse proxies, SSL certificates, environment management, and deployment pipelines turns every project into an ops side quest.
Openship enters this gap with a bold architectural bet: a TypeScript-based control plane that adapts to your environment. On Linux with Docker, it deploys a full orchestration stack. On macOS or Windows, it becomes a lightweight desktop app that deploys remotely over SSH. This dual-mode approach attempts to solve the 'run anywhere' problem without forcing Docker on every developer machine. But this flexibility comes with sharp edges—particularly around privilege boundaries, licensing, and operational dependencies that only surface when you move from local tinkering to production deployments.
Technical Insight
The architecture pivots on a mode selector that fundamentally changes what Openship becomes. In Compose mode (Linux with Docker), you're running a multi-container stack: Postgres for state, Redis for caching/queues, an OpenResty reverse proxy on host networking, and the control plane API that orchestrates everything. The critical piece is how it gets deployment privileges:
# docker-compose.yml excerpt
services:
openship:
image: openship/openship:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./data:/app/data
environment:
- DOCKER_HOST=unix:///var/run/docker.sock
That Docker socket mount is the entire game. When you push code and trigger a deployment, Openship doesn't spawn containers as children—it spawns them as siblings by talking directly to the host Docker daemon. This means your user application containers run alongside the control plane container, all managed by the same Docker instance. The control plane becomes a privileged orchestrator with root-equivalent access to the host.
The routing layer reveals why this matters. OpenResty (an nginx variant with Lua scripting) runs on host networking, binding directly to ports 80 and 443. When you deploy an app, Openship configures it to listen on a loopback-only port (127.0.0.1:3000, for example), then updates OpenResty's config to reverse-proxy that port:
-- Generated OpenResty config for app 'my-app'
server {
listen 80;
server_name my-app.example.com;
location /.well-known/acme-challenge/ {
# Let's Encrypt HTTP-01 challenge handling
proxy_pass http://127.0.0.1:8080;
}
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
The clever part is when this happens. Openship handles TLS certificate provisioning after app startup, not before. If Let's Encrypt fails due to DNS propagation delays or rate limits, your app keeps running on HTTP and you get a non-fatal alert. Traditional PaaS platforms fail the entire deployment if certificates don't provision, which can take your service offline due to ephemeral DNS issues. This surfaces infrastructure problems without cascading them into application failures.
Build detection uses filesystem heuristics to avoid forcing developers into configuration files. The control plane scans for package.json, lockfile variants (package-lock.json, yarn.lock, pnpm-lock.yaml), and framework signatures:
// Simplified build detection logic
function detectFramework(projectPath: string): BuildConfig {
const packageJson = readPackageJson(projectPath);
if (packageJson.dependencies?.['next']) {
return {
framework: 'nextjs',
buildCommand: 'npm run build',
startCommand: 'npm start',
defaultPort: 3000
};
}
if (existsSync(path.join(projectPath, 'docker-compose.yml'))) {
return {
framework: 'docker-compose',
buildCommand: 'docker-compose build',
startCommand: 'docker-compose up',
defaultPort: null // Extract from compose file
};
}
// Fallback to custom commands
return promptUserForCommands();
}
Once detected, the config freezes into a deployment snapshot—a database record containing exact commands, environment variables, and ports. This enables atomic rollbacks: reverting to an old deployment just means swapping which snapshot is active and restarting containers with that frozen config.
The desktop app mode flips the architecture inside-out. Instead of an always-on server, you run the entire control plane locally while the app is open. Deployments happen by establishing SSH tunnels to remote servers and executing Docker commands over those channels. This eliminates attack surface for solo developers—no public-facing control plane to secure—but creates a hard ceiling: the moment you need GitHub webhooks for push-to-deploy or multi-user access, you must migrate to a self-hosted server.
The most controversial bundling decision is iRedMail, a full-featured GPL mail server with DKIM/SPF/DMARC support. It ships with Openship even if you never configure it, creating a licensing surface area most PaaS alternatives avoid by delegating to SendGrid or AWS SES. The appeal is clear—transactional email without per-message SaaS fees—but the GPL licensing means any modifications to the mail stack technically require you to open-source those changes. For a side project sending password reset emails, this is irrelevant. For a commercial product, it's a legal review ticket.
Gotcha
The Compose mode documentation makes it look self-contained, but there's a manual provisioning step that reveals architectural complexity: the control plane container needs SSH access back to the host for certain operations. Binding ports 80/443, configuring the mail engine, and providing terminal access to apps all require the container to SSH into the machine running it. The setup docs bury this:
# Required manual step after docker-compose up
ssh-copy-id -i /app/data/ssh/id_rsa.pub root@localhost
This creates a circular dependency—the container needs SSH keys provisioned on the host that's running the container. The automated installer handles this, but if you're deploying via raw docker-compose, you're editing ~/.ssh/authorized_keys by hand. It's a leaky abstraction that breaks the 'single command deployment' promise.
Multi-tenancy is conspicuously absent from the architecture. Since the control plane mounts the Docker socket, every user with deploy permissions can spawn arbitrary containers with arbitrary privileges. There's no namespace isolation or resource quotas. If you want to let junior developers deploy staging apps without giving them root access to the production database container, you're building that isolation layer yourself. This makes Openship fundamentally a single-team tool—you trust everyone with deployment access because they effectively have root.
The roadmap lists multi-node clustering as 'coming next,' which exposes a current limitation: everything runs on one machine. The 'deploy anywhere' marketing is really 'deploy to any single server.' If your app outgrows vertical scaling, you're migrating to Kubernetes or Nomad anyway. Openship doesn't bridge to distributed primitives—it's an alternative to them for workloads that fit on one box.
Verdict
Use if: You're a solo developer or small trusted team running 3-10 services on a single VPS, you're tired of manually wiring Docker Compose networks and nginx configs, you want push-to-deploy without $200/month in PaaS fees, and you're comfortable with the security model of a privileged control plane container. The desktop app is genuinely useful for local testing before committing to self-hosting. Skip if: You need multi-tenant isolation (untrusted users deploying code), you're already running Kubernetes (you're trading mature distributed primitives for simpler single-node ops), you can't accept GPL licensing for the bundled mail server, or you expect to horizontally scale beyond one machine in the next 12 months. This is Heroku for hackers who want to own the infrastructure—powerful and pragmatic, but with privilege boundaries that require operational maturity to manage safely.